erp-platform/ui/src/views/crm/components/OpportunityForm.tsx
2025-09-17 12:46:58 +03:00

423 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import { FaTimes, FaDollarSign, FaFileAlt } from 'react-icons/fa'
import {
CrmOpportunity,
OpportunityStageEnum,
LeadSourceEnum,
OpportunityStatusEnum,
} from '../../../types/crm'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { mockEmployees } from '../../../mocks/mockEmployees'
import { BusinessParty } from '../../../types/common'
import { getOpportunityLeadSourceText, getOpportunityStageText } from '@/utils/erp'
import { mockCurrencies } from '@/mocks/mockCurrencies'
interface OpportunityFormProps {
isOpen: boolean
onClose: () => void
onSave: (opportunity: CrmOpportunity) => void
opportunity?: CrmOpportunity | null
mode: 'create' | 'edit'
}
const OpportunityForm: React.FC<OpportunityFormProps> = ({
isOpen,
onClose,
onSave,
opportunity,
mode,
}) => {
const [formData, setFormData] = useState({
id: '',
opportunityNumber: '',
title: '',
description: '',
customerId: '',
contactId: '',
stage: OpportunityStageEnum.Qualification,
probability: 10,
estimatedValue: 0,
currency: 'TRY',
expectedCloseDate: '',
assignedTo: '',
teamId: '',
leadSource: LeadSourceEnum.Website,
campaignId: '',
})
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [errors, setErrors] = useState<Record<string, string>>({})
useEffect(() => {
if (opportunity && mode === 'edit') {
setFormData({
id: opportunity.id,
opportunityNumber: opportunity.opportunityNumber,
title: opportunity.title,
description: opportunity.description || '',
customerId: opportunity.customerId,
contactId: opportunity.contactId || '',
stage: opportunity.stage,
probability: opportunity.probability,
estimatedValue: opportunity.estimatedValue,
currency: opportunity.currency,
expectedCloseDate: opportunity.expectedCloseDate.toISOString().split('T')[0],
assignedTo: opportunity.assignedTo,
teamId: opportunity.teamId || '',
leadSource: opportunity.leadSource,
campaignId: opportunity.campaignId || '',
})
} else if (mode === 'create') {
// Generate new opportunity number
const newNumber = `OPP-${Date.now().toString().slice(-6)}`
setFormData({
id: `opp_${Date.now()}`,
opportunityNumber: newNumber,
title: '',
description: '',
customerId: '',
contactId: '',
stage: OpportunityStageEnum.Qualification,
probability: 10,
estimatedValue: 0,
currency: 'TRY',
expectedCloseDate: '',
assignedTo: 'Mevcut Kullanıcı',
teamId: '',
leadSource: LeadSourceEnum.Website,
campaignId: '',
})
}
}, [opportunity, mode, isOpen])
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target
setFormData((prev) => ({
...prev,
[name]: value,
}))
// Clear error when user starts typing
if (errors[name]) {
setErrors((prev) => ({
...prev,
[name]: '',
}))
}
}
const validateForm = () => {
const newErrors: Record<string, string> = {}
if (!formData.title.trim()) {
newErrors.title = 'Fırsat başlığı zorunludur'
}
if (!formData.customerId) {
newErrors.customerId = 'Müşteri seçimi zorunludur'
}
if (formData.estimatedValue <= 0) {
newErrors.estimatedValue = "Tahmini değer 0'dan büyük olmalıdır"
}
if (!formData.expectedCloseDate) {
newErrors.expectedCloseDate = 'Beklenen kapanış tarihi zorunludur'
}
if (formData.probability < 0 || formData.probability > 100) {
newErrors.probability = 'Olasılık 0-100 arasında olmalıdır'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!validateForm()) {
return
}
const opportunityData: CrmOpportunity = {
id: formData.id,
opportunityNumber: formData.opportunityNumber,
title: formData.title,
description: formData.description,
customerId: formData.customerId,
contactId: formData.contactId || undefined,
stage: formData.stage,
probability: formData.probability,
estimatedValue: formData.estimatedValue,
currency: formData.currency,
expectedCloseDate: new Date(formData.expectedCloseDate),
assignedTo: formData.assignedTo,
teamId: formData.teamId || undefined,
leadSource: formData.leadSource,
campaignId: formData.campaignId || undefined,
status:
formData.stage === OpportunityStageEnum.ClosedWon
? OpportunityStatusEnum.Won
: formData.stage === OpportunityStageEnum.ClosedLost
? OpportunityStatusEnum.Lost
: OpportunityStatusEnum.Open,
activities: [],
competitors: [],
creationTime: opportunity?.creationTime || new Date(),
lastModificationTime: new Date(),
}
onSave(opportunityData)
onClose()
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg w-full max-w-4xl max-h-[90vh] overflow-y-auto m-4">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold text-gray-900">
{mode === 'create' ? 'Yeni Fırsat Oluştur' : 'Fırsat Düzenle'}
</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<FaTimes className="w-5 h-5" />
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Basic Information */}
<div className="space-y-3">
<h3 className="text-base font-medium text-gray-900 flex items-center gap-2">
<FaFileAlt className="w-5 h-5" />
Temel Bilgiler
</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fırsat Numarası
</label>
<input
type="text"
name="opportunityNumber"
value={formData.opportunityNumber}
onChange={handleInputChange}
disabled
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md bg-gray-50 text-gray-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fırsat Başlığı *
</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleInputChange}
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.title ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Fırsat başlığını girin"
/>
{errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">ıklama</label>
<textarea
name="description"
value={formData.description}
onChange={handleInputChange}
rows={4}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Fırsat açıklaması"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Müşteri *</label>
<select
name="customerId"
value={formData.customerId}
onChange={handleInputChange}
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.customerId ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">Müşteri seçin</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
{errors.customerId && (
<p className="text-red-500 text-sm mt-1">{errors.customerId}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Sorumlu</label>
<select
value={formData.assignedTo || ''}
onChange={handleInputChange}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Sorumlu seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName}
</option>
))}
</select>
</div>
</div>
{/* Sales Information */}
<div className="space-y-3">
<h3 className="text-base font-medium text-gray-900 flex items-center gap-2">
<FaDollarSign className="w-5 h-5" />
Satış Bilgileri
</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Aşama</label>
<select
name="stage"
value={formData.stage}
onChange={handleInputChange}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{Object.values(OpportunityStageEnum).map((stage) => (
<option key={stage} value={stage}>
{getOpportunityStageText(stage)}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Olasılık (%) *
</label>
<input
type="number"
name="probability"
value={formData.probability}
onChange={handleInputChange}
min="0"
max="100"
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.probability ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.probability && (
<p className="text-red-500 text-sm mt-1">{errors.probability}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tahmini Değer *
</label>
<input
type="number"
name="estimatedValue"
value={formData.estimatedValue}
onChange={handleInputChange}
min="0"
step="0.01"
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.estimatedValue ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="0.00"
/>
{errors.estimatedValue && (
<p className="text-red-500 text-sm mt-1">{errors.estimatedValue}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Para Birimi</label>
<select
name="currency"
value={formData.currency}
onChange={handleInputChange}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{mockCurrencies.map((currency) => (
<option key={currency.value} value={currency.value}>
{currency.value} - {currency.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Beklenen Kapanış Tarihi *
</label>
<input
type="date"
name="expectedCloseDate"
value={formData.expectedCloseDate}
onChange={handleInputChange}
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.expectedCloseDate ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.expectedCloseDate && (
<p className="text-red-500 text-sm mt-1">{errors.expectedCloseDate}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kaynak</label>
<select
name="leadSource"
value={formData.leadSource}
onChange={handleInputChange}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{Object.values(LeadSourceEnum).map((source) => (
<option key={source} value={source}>
{getOpportunityLeadSourceText(source)}
</option>
))}
</select>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex justify-end gap-2 mt-6 pt-4 border-t">
<button
type="button"
onClick={onClose}
className="px-4 py-1.5 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
>
İptal
</button>
<button
type="submit"
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
{mode === 'create' ? 'Oluştur' : 'Güncelle'}
</button>
</div>
</form>
</div>
</div>
)
}
export default OpportunityForm