Container Crm Management

This commit is contained in:
Sedat ÖZTÜRK 2025-09-15 17:13:20 +03:00
parent ecb32cf6cf
commit 7d52573765
23 changed files with 6155 additions and 7366 deletions

View file

@ -1,4 +1,4 @@
import React from "react"; import React from 'react'
import { import {
FaTimes, FaTimes,
FaCalendar, FaCalendar,
@ -12,10 +12,10 @@ import {
FaFlag, FaFlag,
FaFileAlt, FaFileAlt,
FaHistory, FaHistory,
} from "react-icons/fa"; } from 'react-icons/fa'
import { CrmActivity } from "../../../types/crm"; import { CrmActivity } from '../../../types/crm'
import { mockEmployees } from "../../../mocks/mockEmployees"; import { mockEmployees } from '../../../mocks/mockEmployees'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { import {
getActivityStatusColor, getActivityStatusColor,
getActivityStatusText, getActivityStatusText,
@ -23,28 +23,22 @@ import {
getPsActivityTypeText, getPsActivityTypeText,
getPriorityColor, getPriorityColor,
getPriorityText, getPriorityText,
} from "../../../utils/erp"; getActivityTypeText,
} from '../../../utils/erp'
interface ActivityDetailsProps { interface ActivityDetailsProps {
isOpen: boolean; isOpen: boolean
onClose: () => void; onClose: () => void
onEdit: (activity: CrmActivity) => void; onEdit: (activity: CrmActivity) => void
activity: CrmActivity | null; activity: CrmActivity | null
} }
const ActivityDetails: React.FC<ActivityDetailsProps> = ({ const ActivityDetails: React.FC<ActivityDetailsProps> = ({ isOpen, onClose, onEdit, activity }) => {
isOpen, if (!isOpen || !activity) return null
onClose,
onEdit,
activity,
}) => {
if (!isOpen || !activity) return null;
const customer = mockBusinessParties.find( const customer = mockBusinessParties.find((c) => c.id === activity.customerId)
(c) => c.id === activity.customerId
);
const ActivityIcon = getActivityTypeIcon(activity.activityType); const ActivityIcon = getActivityTypeIcon(activity.activityType)
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
@ -56,12 +50,8 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<ActivityIcon className="w-6 h-6 text-blue-600" /> <ActivityIcon className="w-6 h-6 text-blue-600" />
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">{activity.subject}</h2>
{activity.subject} <p className="text-sm text-gray-600">{getActivityTypeText(activity.activityType)}</p>
</h2>
<p className="text-sm text-gray-600">
{getPsActivityTypeText(activity.activityType)}
</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -72,10 +62,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<FaEdit className="w-4 h-4" /> <FaEdit className="w-4 h-4" />
Düzenle Düzenle
</button> </button>
<button <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-2">
onClick={onClose}
className="text-gray-400 hover:text-gray-600 p-2"
>
<FaTimes className="w-5 h-5" /> <FaTimes className="w-5 h-5" />
</button> </button>
</div> </div>
@ -87,7 +74,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<div className="text-center"> <div className="text-center">
<div <div
className={`inline-flex px-3 py-1.5 rounded-full text-xs font-medium border ${getActivityStatusColor( className={`inline-flex px-3 py-1.5 rounded-full text-xs font-medium border ${getActivityStatusColor(
activity.status activity.status,
)}`} )}`}
> >
{getActivityStatusText(activity.status)} {getActivityStatusText(activity.status)}
@ -96,11 +83,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
</div> </div>
<div className="text-center"> <div className="text-center">
<div <div className={`text-base font-semibold ${getPriorityColor(activity.priority)}`}>
className={`text-base font-semibold ${getPriorityColor(
activity.priority
)}`}
>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<FaFlag className="w-4 h-4" /> <FaFlag className="w-4 h-4" />
{getPriorityText(activity.priority)} {getPriorityText(activity.priority)}
@ -112,7 +95,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<div className="text-center"> <div className="text-center">
<div className="flex items-center justify-center gap-2 text-base font-semibold text-gray-900"> <div className="flex items-center justify-center gap-2 text-base font-semibold text-gray-900">
<FaCalendar className="w-4 h-4 text-gray-400" /> <FaCalendar className="w-4 h-4 text-gray-400" />
{new Date(activity.activityDate).toLocaleDateString("tr-TR")} {new Date(activity.activityDate).toLocaleDateString('tr-TR')}
</div> </div>
<p className="text-xs text-gray-500 mt-1">Tarih</p> <p className="text-xs text-gray-500 mt-1">Tarih</p>
</div> </div>
@ -139,9 +122,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">Konu</label>
Konu
</label>
<p className="text-gray-900">{activity.subject}</p> <p className="text-gray-900">{activity.subject}</p>
</div> </div>
@ -150,9 +131,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">
ıklama ıklama
</label> </label>
<p className="text-gray-900 whitespace-pre-wrap"> <p className="text-gray-900 whitespace-pre-wrap">{activity.description}</p>
{activity.description}
</p>
</div> </div>
)} )}
@ -163,13 +142,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
Başlangıç Saati Başlangıç Saati
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">
{new Date(activity.startTime).toLocaleTimeString( {new Date(activity.startTime).toLocaleTimeString('tr-TR', {
"tr-TR", hour: '2-digit',
{ minute: '2-digit',
hour: "2-digit", })}
minute: "2-digit",
}
)}
</p> </p>
</div> </div>
)} )}
@ -180,13 +156,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
Bitiş Saati Bitiş Saati
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">
{new Date(activity.endTime).toLocaleTimeString( {new Date(activity.endTime).toLocaleTimeString('tr-TR', {
"tr-TR", hour: '2-digit',
{ minute: '2-digit',
hour: "2-digit", })}
minute: "2-digit",
}
)}
</p> </p>
</div> </div>
)} )}
@ -232,16 +205,12 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaEnvelope className="w-4 h-4 text-gray-400" /> <FaEnvelope className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">{customer.primaryContact?.email}</span>
{customer.primaryContact?.email}
</span>
</div> </div>
{customer.primaryContact?.phone && ( {customer.primaryContact?.phone && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaPhone className="w-4 h-4 text-gray-400" /> <FaPhone className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">{customer.primaryContact.phone}</span>
{customer.primaryContact.phone}
</span>
</div> </div>
)} )}
</div> </div>
@ -261,14 +230,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">Sorumlu</label>
Sorumlu
</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-400" /> <FaUser className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">{activity.assigned?.fullName}</span>
{activity.assigned?.fullName}
</span>
</div> </div>
</div> </div>
@ -279,20 +244,15 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
</label> </label>
<div className="space-y-1.5"> <div className="space-y-1.5">
{activity.participants.map((participantId, index) => { {activity.participants.map((participantId, index) => {
const employee = mockEmployees.find( const employee = mockEmployees.find((emp) => emp.id === participantId)
(emp) => emp.id === participantId
);
return ( return (
<div <div key={index} className="flex items-center gap-2">
key={index}
className="flex items-center gap-2"
>
<FaUsers className="w-4 h-4 text-gray-400" /> <FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">
{employee ? employee.fullName : participantId} {employee ? employee.fullName : participantId}
</span> </span>
</div> </div>
); )
})} })}
</div> </div>
</div> </div>
@ -316,9 +276,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaCalendar className="w-4 h-4 text-gray-400" /> <FaCalendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">
{new Date(activity.followUpDate).toLocaleDateString( {new Date(activity.followUpDate).toLocaleDateString('tr-TR')}
"tr-TR"
)}
</span> </span>
</div> </div>
</div> </div>
@ -329,9 +287,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">
Takip Aktivitesi Takip Aktivitesi
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">{activity.followUpActivity}</p>
{activity.followUpActivity}
</p>
</div> </div>
)} )}
</div> </div>
@ -351,9 +307,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">
Sonuç Sonuç
</label> </label>
<p className="text-gray-900 whitespace-pre-wrap"> <p className="text-gray-900 whitespace-pre-wrap">{activity.outcome}</p>
{activity.outcome}
</p>
</div> </div>
)} )}
@ -362,9 +316,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">
Sonraki Adımlar Sonraki Adımlar
</label> </label>
<p className="text-gray-900 whitespace-pre-wrap"> <p className="text-gray-900 whitespace-pre-wrap">{activity.nextSteps}</p>
{activity.nextSteps}
</p>
</div> </div>
)} )}
</div> </div>
@ -381,14 +333,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<div className="flex items-start gap-3 border-l-2 border-blue-200 pl-4"> <div className="flex items-start gap-3 border-l-2 border-blue-200 pl-4">
<div className="w-3 h-3 bg-blue-600 rounded-full mt-1.5 -ml-6 border-2 border-white"></div> <div className="w-3 h-3 bg-blue-600 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900">Aktivite Oluşturuldu</p>
Aktivite Oluşturuldu
</p>
<p className="text-xs text-gray-500 flex items-center gap-1"> <p className="text-xs text-gray-500 flex items-center gap-1">
<FaClock className="w-3 h-3" /> <FaClock className="w-3 h-3" />
{new Date(activity.creationTime).toLocaleDateString( {new Date(activity.creationTime).toLocaleDateString('tr-TR')}
"tr-TR"
)}
</p> </p>
</div> </div>
</div> </div>
@ -396,14 +344,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
<div className="flex items-start gap-3 border-l-2 border-gray-200 pl-4"> <div className="flex items-start gap-3 border-l-2 border-gray-200 pl-4">
<div className="w-3 h-3 bg-gray-400 rounded-full mt-1.5 -ml-6 border-2 border-white"></div> <div className="w-3 h-3 bg-gray-400 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900">Son Güncelleme</p>
Son Güncelleme
</p>
<p className="text-xs text-gray-500 flex items-center gap-1"> <p className="text-xs text-gray-500 flex items-center gap-1">
<FaClock className="w-3 h-3" /> <FaClock className="w-3 h-3" />
{new Date( {new Date(activity.lastModificationTime).toLocaleDateString('tr-TR')}
activity.lastModificationTime
).toLocaleDateString("tr-TR")}
</p> </p>
</div> </div>
</div> </div>
@ -414,7 +358,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
</div> </div>
</div> </div>
</div> </div>
); )
}; }
export default ActivityDetails; export default ActivityDetails

View file

@ -1,163 +1,140 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react'
import { FaTimes, FaCalendar, FaFileAlt, FaFlag } from "react-icons/fa"; import { FaTimes, FaCalendar, FaFileAlt, FaFlag } from 'react-icons/fa'
import { import { CrmActivity, CrmActivityTypeEnum, ActivityStatusEnum } from '../../../types/crm'
CrmActivity, import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
CrmActivityTypeEnum, import { mockEmployees } from '../../../mocks/mockEmployees'
ActivityStatusEnum, import MultiSelectEmployee from '../../../components/common/MultiSelectEmployee'
} from "../../../types/crm"; import { BusinessParty, PriorityEnum } from '../../../types/common'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { getActivityStatusText, getPriorityText, getActivityTypeText } from '../../../utils/erp'
import { mockEmployees } from "../../../mocks/mockEmployees";
import MultiSelectEmployee from "../../../components/common/MultiSelectEmployee";
import { BusinessParty, PriorityEnum } from "../../../types/common";
import {
getActivityStatusText,
getPriorityText,
getActivityTypeText,
} from "../../../utils/erp";
interface ActivityFormProps { interface ActivityFormProps {
isOpen: boolean; isOpen: boolean
onClose: () => void; onClose: () => void
onSave: (activity: CrmActivity) => void; onSave: (activity: CrmActivity) => void
activity?: CrmActivity | null; activity?: CrmActivity | null
mode: "create" | "edit"; mode: 'create' | 'edit'
} }
const ActivityForm: React.FC<ActivityFormProps> = ({ const ActivityForm: React.FC<ActivityFormProps> = ({ isOpen, onClose, onSave, activity, mode }) => {
isOpen,
onClose,
onSave,
activity,
mode,
}) => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
id: "", id: '',
activityType: CrmActivityTypeEnum.Call, activityType: CrmActivityTypeEnum.Call,
subject: "", subject: '',
description: "", description: '',
customerId: "", customerId: '',
contactId: "", contactId: '',
activityDate: "", activityDate: '',
startTime: "", startTime: '',
endTime: "", endTime: '',
duration: "", duration: '',
assignedTo: "", assignedTo: '',
participants: [] as string[], // Array of employee IDs participants: [] as string[], // Array of employee IDs
status: ActivityStatusEnum.Planned, status: ActivityStatusEnum.Planned,
priority: PriorityEnum.Normal, priority: PriorityEnum.Normal,
followUpDate: "", followUpDate: '',
followUpActivity: "", followUpActivity: '',
outcome: "", outcome: '',
nextSteps: "", nextSteps: '',
}); })
const [customers] = useState<BusinessParty[]>(mockBusinessParties); const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({})
useEffect(() => { useEffect(() => {
if (activity && mode === "edit") { if (activity && mode === 'edit') {
setFormData({ setFormData({
id: activity.id, id: activity.id,
activityType: activity.activityType, activityType: activity.activityType,
subject: activity.subject, subject: activity.subject,
description: activity.description || "", description: activity.description || '',
customerId: activity.customerId || "", customerId: activity.customerId || '',
contactId: activity.contactId || "", contactId: activity.contactId || '',
activityDate: activity.activityDate.toISOString().split("T")[0], activityDate: activity.activityDate.toISOString().split('T')[0],
startTime: activity.startTime startTime: activity.startTime ? activity.startTime.toISOString().slice(11, 16) : '',
? activity.startTime.toISOString().slice(11, 16) endTime: activity.endTime ? activity.endTime.toISOString().slice(11, 16) : '',
: "", duration: activity.duration?.toString() || '',
endTime: activity.endTime
? activity.endTime.toISOString().slice(11, 16)
: "",
duration: activity.duration?.toString() || "",
assignedTo: activity.assignedTo, assignedTo: activity.assignedTo,
participants: activity.participants || [], // Keep as array of IDs participants: activity.participants || [], // Keep as array of IDs
status: activity.status, status: activity.status,
priority: activity.priority, priority: activity.priority,
followUpDate: activity.followUpDate followUpDate: activity.followUpDate
? activity.followUpDate.toISOString().split("T")[0] ? activity.followUpDate.toISOString().split('T')[0]
: "", : '',
followUpActivity: activity.followUpActivity || "", followUpActivity: activity.followUpActivity || '',
outcome: activity.outcome || "", outcome: activity.outcome || '',
nextSteps: activity.nextSteps || "", nextSteps: activity.nextSteps || '',
}); })
} else if (mode === "create") { } else if (mode === 'create') {
const today = new Date().toISOString().split("T")[0]; const today = new Date().toISOString().split('T')[0]
setFormData({ setFormData({
id: `act_${Date.now()}`, id: `act_${Date.now()}`,
activityType: CrmActivityTypeEnum.Call, activityType: CrmActivityTypeEnum.Call,
subject: "", subject: '',
description: "", description: '',
customerId: "", customerId: '',
contactId: "", contactId: '',
activityDate: today, activityDate: today,
startTime: "", startTime: '',
endTime: "", endTime: '',
duration: "", duration: '',
assignedTo: "Mevcut Kullanıcı", assignedTo: 'Mevcut Kullanıcı',
participants: [], // Empty array for new activities participants: [], // Empty array for new activities
status: ActivityStatusEnum.Planned, status: ActivityStatusEnum.Planned,
priority: PriorityEnum.Normal, priority: PriorityEnum.Normal,
followUpDate: "", followUpDate: '',
followUpActivity: "", followUpActivity: '',
outcome: "", outcome: '',
nextSteps: "", nextSteps: '',
}); })
} }
}, [activity, mode, isOpen]); }, [activity, mode, isOpen])
const handleInputChange = ( const handleInputChange = (
e: React.ChangeEvent< e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>
) => { ) => {
const { name, value } = e.target; const { name, value } = e.target
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[name]: value, [name]: value,
})); }))
// Clear error when user starts typing // Clear error when user starts typing
if (errors[name]) { if (errors[name]) {
setErrors((prev) => ({ setErrors((prev) => ({
...prev, ...prev,
[name]: "", [name]: '',
})); }))
}
} }
};
const validateForm = () => { const validateForm = () => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {}
if (!formData.subject.trim()) { if (!formData.subject.trim()) {
newErrors.subject = "Aktivite konusu zorunludur"; newErrors.subject = 'Aktivite konusu zorunludur'
} }
if (!formData.activityDate) { if (!formData.activityDate) {
newErrors.activityDate = "Aktivite tarihi zorunludur"; newErrors.activityDate = 'Aktivite tarihi zorunludur'
} }
if (!formData.assignedTo.trim()) { if (!formData.assignedTo.trim()) {
newErrors.assignedTo = "Sorumlu kişi zorunludur"; newErrors.assignedTo = 'Sorumlu kişi zorunludur'
} }
if ( if (formData.duration && (isNaN(Number(formData.duration)) || Number(formData.duration) <= 0)) {
formData.duration && newErrors.duration = 'Süre geçerli bir sayı olmalıdır'
(isNaN(Number(formData.duration)) || Number(formData.duration) <= 0)
) {
newErrors.duration = "Süre geçerli bir sayı olmalıdır";
} }
setErrors(newErrors); setErrors(newErrors)
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0
}; }
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault()
if (!validateForm()) { if (!validateForm()) {
return; return
} }
const activityData: CrmActivity = { const activityData: CrmActivity = {
@ -179,21 +156,19 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
participants: formData.participants, // Already an array of IDs participants: formData.participants, // Already an array of IDs
status: formData.status, status: formData.status,
priority: formData.priority, priority: formData.priority,
followUpDate: formData.followUpDate followUpDate: formData.followUpDate ? new Date(formData.followUpDate) : undefined,
? new Date(formData.followUpDate)
: undefined,
followUpActivity: formData.followUpActivity || undefined, followUpActivity: formData.followUpActivity || undefined,
outcome: formData.outcome || undefined, outcome: formData.outcome || undefined,
nextSteps: formData.nextSteps || undefined, nextSteps: formData.nextSteps || undefined,
creationTime: activity?.creationTime || new Date(), creationTime: activity?.creationTime || new Date(),
lastModificationTime: new Date(), lastModificationTime: new Date(),
}; }
onSave(activityData); onSave(activityData)
onClose(); onClose()
}; }
if (!isOpen) return null; if (!isOpen) return null
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
@ -201,12 +176,9 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b"> <div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">
{mode === "create" ? "Yeni Aktivite Oluştur" : "Aktivite Düzenle"} {mode === 'create' ? 'Yeni Aktivite Oluştur' : 'Aktivite Düzenle'}
</h2> </h2>
<button <button onClick={onClose} className="text-gray-400 hover:text-gray-600">
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<FaTimes className="w-5 h-5" /> <FaTimes className="w-5 h-5" />
</button> </button>
</div> </div>
@ -240,28 +212,22 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Konu *</label>
Konu *
</label>
<input <input
type="text" type="text"
name="subject" name="subject"
value={formData.subject} value={formData.subject}
onChange={handleInputChange} 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 ${ className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.subject ? "border-red-500" : "border-gray-300" errors.subject ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="Aktivite konusunu girin" placeholder="Aktivite konusunu girin"
/> />
{errors.subject && ( {errors.subject && <p className="text-red-500 text-sm mt-1">{errors.subject}</p>}
<p className="text-red-500 text-sm mt-1">{errors.subject}</p>
)}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">ıklama</label>
ıklama
</label>
<textarea <textarea
name="description" name="description"
value={formData.description} value={formData.description}
@ -273,9 +239,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Müşteri</label>
Müşteri
</label>
<select <select
name="customerId" name="customerId"
value={formData.customerId} value={formData.customerId}
@ -292,9 +256,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Öncelik</label>
Öncelik
</label>
<select <select
name="priority" name="priority"
value={formData.priority} value={formData.priority}
@ -327,13 +289,11 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
value={formData.activityDate} value={formData.activityDate}
onChange={handleInputChange} 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 ${ className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.activityDate ? "border-red-500" : "border-gray-300" errors.activityDate ? 'border-red-500' : 'border-gray-300'
}`} }`}
/> />
{errors.activityDate && ( {errors.activityDate && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">{errors.activityDate}</p>
{errors.activityDate}
</p>
)} )}
</div> </div>
@ -376,19 +336,15 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
onChange={handleInputChange} onChange={handleInputChange}
min="1" min="1"
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.duration ? "border-red-500" : "border-gray-300" errors.duration ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="60" placeholder="60"
/> />
{errors.duration && ( {errors.duration && <p className="text-red-500 text-sm mt-1">{errors.duration}</p>}
<p className="text-red-500 text-sm mt-1">{errors.duration}</p>
)}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Sorumlu *</label>
Sorumlu *
</label>
<select <select
value={formData.assignedTo} value={formData.assignedTo}
onChange={handleInputChange} onChange={handleInputChange}
@ -402,16 +358,12 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
))} ))}
</select> </select>
{errors.assignedTo && ( {errors.assignedTo && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">{errors.assignedTo}</p>
{errors.assignedTo}
</p>
)} )}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Katılımcılar</label>
Katılımcılar
</label>
<MultiSelectEmployee <MultiSelectEmployee
selectedEmployees={formData.participants} selectedEmployees={formData.participants}
onChange={(employeeIds) => onChange={(employeeIds) =>
@ -426,9 +378,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Durum</label>
Durum
</label>
<select <select
name="status" name="status"
value={formData.status} value={formData.status}
@ -454,9 +404,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Takip Tarihi</label>
Takip Tarihi
</label>
<input <input
type="date" type="date"
name="followUpDate" name="followUpDate"
@ -482,9 +430,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Sonuç</label>
Sonuç
</label>
<textarea <textarea
name="outcome" name="outcome"
value={formData.outcome} value={formData.outcome}
@ -523,13 +469,13 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
type="submit" type="submit"
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" 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"} {mode === 'create' ? 'Oluştur' : 'Güncelle'}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
); )
}; }
export default ActivityForm; export default ActivityForm

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { import {
FaCalendar, FaCalendar,
FaPlus, FaPlus,
@ -10,93 +10,82 @@ import {
FaList, FaList,
FaTh, FaTh,
FaFlag, FaFlag,
} from "react-icons/fa"; } from 'react-icons/fa'
import { import { CrmActivity, CrmActivityTypeEnum, ActivityStatusEnum } from '../../../types/crm'
CrmActivity, import DataTable, { Column } from '../../../components/common/DataTable'
CrmActivityTypeEnum, import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
ActivityStatusEnum, import { mockActivities } from '../../../mocks/mockActivities'
} from "../../../types/crm"; import ActivityForm from './ActivityForm'
import DataTable, { Column } from "../../../components/common/DataTable"; import ActivityDetails from './ActivityDetails'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { BusinessParty, PriorityEnum } from '../../../types/common'
import { mockActivities } from "../../../mocks/mockActivities"; import Widget from '../../../components/common/Widget'
import ActivityForm from "./ActivityForm";
import ActivityDetails from "./ActivityDetails";
import { BusinessParty, PriorityEnum } from "../../../types/common";
import Widget from "../../../components/common/Widget";
import { import {
getActivityStatusColor, getActivityStatusColor,
getActivityStatusText, getActivityStatusText,
getActivityTypeIcon, getActivityTypeIcon,
getPsActivityTypeText, getActivityTypeText,
} from "../../../utils/erp"; } from '../../../utils/erp'
import { Container } from '@/components/shared'
const ActivityRecords: React.FC = () => { const ActivityRecords: React.FC = () => {
const [activities, setActivities] = useState<CrmActivity[]>(mockActivities); const [activities, setActivities] = useState<CrmActivity[]>(mockActivities)
const [customers] = useState<BusinessParty[]>(mockBusinessParties); const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState('')
const [selectedType, setSelectedType] = useState<CrmActivityTypeEnum | "all">( const [selectedType, setSelectedType] = useState<CrmActivityTypeEnum | 'all'>('all')
"all" const [selectedStatus, setSelectedStatus] = useState<ActivityStatusEnum | 'all'>('all')
); const [viewMode, setViewMode] = useState<'list' | 'cards'>('list')
const [selectedStatus, setSelectedStatus] = useState<
ActivityStatusEnum | "all"
>("all");
const [viewMode, setViewMode] = useState<"list" | "cards">("list");
// Modal states // Modal states
const [isFormOpen, setIsFormOpen] = useState(false); const [isFormOpen, setIsFormOpen] = useState(false)
const [isDetailsOpen, setIsDetailsOpen] = useState(false); const [isDetailsOpen, setIsDetailsOpen] = useState(false)
const [selectedActivity, setSelectedActivity] = useState<CrmActivity | null>( const [selectedActivity, setSelectedActivity] = useState<CrmActivity | null>(null)
null const [formMode, setFormMode] = useState<'create' | 'edit'>('create')
);
const [formMode, setFormMode] = useState<"create" | "edit">("create");
const handleAdd = () => { const handleAdd = () => {
setSelectedActivity(null); setSelectedActivity(null)
setFormMode("create"); setFormMode('create')
setIsFormOpen(true); setIsFormOpen(true)
}; }
const handleEdit = (activity: CrmActivity) => { const handleEdit = (activity: CrmActivity) => {
setSelectedActivity(activity); setSelectedActivity(activity)
setFormMode("edit"); setFormMode('edit')
setIsFormOpen(true); setIsFormOpen(true)
}; }
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
if (confirm("Bu aktiviteyi silmek istediğinizden emin misiniz?")) { if (confirm('Bu aktiviteyi silmek istediğinizden emin misiniz?')) {
setActivities((prev) => prev.filter((act) => act.id !== id)); setActivities((prev) => prev.filter((act) => act.id !== id))
}
} }
};
const handleViewDetails = (activity: CrmActivity) => { const handleViewDetails = (activity: CrmActivity) => {
setSelectedActivity(activity); setSelectedActivity(activity)
setIsDetailsOpen(true); setIsDetailsOpen(true)
}; }
const handleSaveActivity = (activity: CrmActivity) => { const handleSaveActivity = (activity: CrmActivity) => {
if (formMode === "create") { if (formMode === 'create') {
setActivities((prev) => [...prev, activity]); setActivities((prev) => [...prev, activity])
} else { } else {
setActivities((prev) => setActivities((prev) => prev.map((act) => (act.id === activity.id ? activity : act)))
prev.map((act) => (act.id === activity.id ? activity : act)) }
);
} }
};
const handleCloseForm = () => { const handleCloseForm = () => {
setIsFormOpen(false); setIsFormOpen(false)
setSelectedActivity(null); setSelectedActivity(null)
}; }
const handleCloseDetails = () => { const handleCloseDetails = () => {
setIsDetailsOpen(false); setIsDetailsOpen(false)
setSelectedActivity(null); setSelectedActivity(null)
}; }
const handleEditFromDetails = (activity: CrmActivity) => { const handleEditFromDetails = (activity: CrmActivity) => {
setIsDetailsOpen(false); setIsDetailsOpen(false)
handleEdit(activity); handleEdit(activity)
}; }
const filteredActivities = activities.filter((activity) => { const filteredActivities = activities.filter((activity) => {
if ( if (
@ -104,81 +93,77 @@ const ActivityRecords: React.FC = () => {
!activity.subject.toLowerCase().includes(searchTerm.toLowerCase()) && !activity.subject.toLowerCase().includes(searchTerm.toLowerCase()) &&
!activity.description?.toLowerCase().includes(searchTerm.toLowerCase()) !activity.description?.toLowerCase().includes(searchTerm.toLowerCase())
) { ) {
return false; return false
} }
if (selectedType !== "all" && activity.activityType !== selectedType) { if (selectedType !== 'all' && activity.activityType !== selectedType) {
return false; return false
} }
if (selectedStatus !== "all" && activity.status !== selectedStatus) { if (selectedStatus !== 'all' && activity.status !== selectedStatus) {
return false; return false
} }
return true; return true
}); })
const columns: Column<CrmActivity>[] = [ const columns: Column<CrmActivity>[] = [
{ {
key: "subject", key: 'subject',
header: "Aktivite", header: 'Aktivite',
sortable: true, sortable: true,
render: (activity: CrmActivity) => { render: (activity: CrmActivity) => {
const Icon = getActivityTypeIcon(activity.activityType); const Icon = getActivityTypeIcon(activity.activityType)
return ( return (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center"> <div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<Icon className="w-5 h-5 text-blue-600" /> <Icon className="w-5 h-5 text-blue-600" />
</div> </div>
<div> <div>
<div className="font-medium text-gray-900"> <div className="font-medium text-gray-900">{activity.subject}</div>
{activity.subject}
</div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{getPsActivityTypeText(activity.activityType)} {getActivityTypeText(activity.activityType)}
</div> </div>
</div> </div>
</div> </div>
); )
}, },
}, },
{ {
key: "customer", key: 'customer',
header: "Müşteri", header: 'Müşteri',
render: (activity: CrmActivity) => { render: (activity: CrmActivity) => {
const customer = customers.find((c) => c.id === activity.customerId); const customer = customers.find((c) => c.id === activity.customerId)
return customer ? ( return customer ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-400" /> <FaBuilding className="w-4 h-4 text-gray-400" />
<span>{customer.name}</span> <span>{customer.name}</span>
</div> </div>
) : ( ) : (
"-" '-'
); )
}, },
}, },
{ {
key: "activityDate", key: 'activityDate',
header: "Tarih & Saat", header: 'Tarih & Saat',
render: (activity: CrmActivity) => ( render: (activity: CrmActivity) => (
<div className="flex items-center gap-1 text-sm"> <div className="flex items-center gap-1 text-sm">
<FaCalendar className="w-3 h-3 text-gray-400" /> <FaCalendar className="w-3 h-3 text-gray-400" />
<div> <div>
<div> <div>{new Date(activity.activityDate).toLocaleDateString('tr-TR')}</div>
{new Date(activity.activityDate).toLocaleDateString("tr-TR")}
</div>
<div className="text-gray-500"> <div className="text-gray-500">
{activity.startTime {activity.startTime
? new Date(activity.startTime).toLocaleTimeString("tr-TR", { ? new Date(activity.startTime).toLocaleTimeString('tr-TR', {
hour: "2-digit", hour: '2-digit',
minute: "2-digit", minute: '2-digit',
}) })
: "-"} : '-'}
</div> </div>
</div> </div>
</div> </div>
), ),
}, },
{ {
key: "duration", key: 'duration',
header: "Süre", header: 'Süre',
render: (activity: CrmActivity) => render: (activity: CrmActivity) =>
activity.duration ? ( activity.duration ? (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@ -186,12 +171,12 @@ const ActivityRecords: React.FC = () => {
<span>{activity.duration} dk</span> <span>{activity.duration} dk</span>
</div> </div>
) : ( ) : (
"-" '-'
), ),
}, },
{ {
key: "assignedTo", key: 'assignedTo',
header: "Sorumlu", header: 'Sorumlu',
render: (activity: CrmActivity) => ( render: (activity: CrmActivity) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FaUser className="w-4 h-4 text-gray-400" /> <FaUser className="w-4 h-4 text-gray-400" />
@ -200,12 +185,12 @@ const ActivityRecords: React.FC = () => {
), ),
}, },
{ {
key: "status", key: 'status',
header: "Durum", header: 'Durum',
render: (activity: CrmActivity) => ( render: (activity: CrmActivity) => (
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor( className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor(
activity.status activity.status,
)}`} )}`}
> >
{getActivityStatusText(activity.status)} {getActivityStatusText(activity.status)}
@ -213,8 +198,8 @@ const ActivityRecords: React.FC = () => {
), ),
}, },
{ {
key: "actions", key: 'actions',
header: "İşlemler", header: 'İşlemler',
render: (activity: CrmActivity) => ( render: (activity: CrmActivity) => (
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@ -241,45 +226,39 @@ const ActivityRecords: React.FC = () => {
</div> </div>
), ),
}, },
]; ]
// Calculate statistics // Calculate statistics
const totalActivities = activities.length; const totalActivities = activities.length
const completedActivities = activities.filter( const completedActivities = activities.filter(
(a) => a.status === ActivityStatusEnum.Completed (a) => a.status === ActivityStatusEnum.Completed,
).length; ).length
const pendingActivities = activities.filter( const pendingActivities = activities.filter(
(a) => (a) => a.status === ActivityStatusEnum.Planned || a.status === ActivityStatusEnum.InProgress,
a.status === ActivityStatusEnum.Planned || ).length
a.status === ActivityStatusEnum.InProgress
).length;
const todayActivities = activities.filter((a) => { const todayActivities = activities.filter((a) => {
const today = new Date(); const today = new Date()
const activityDate = new Date(a.activityDate); const activityDate = new Date(a.activityDate)
return activityDate.toDateString() === today.toDateString(); return activityDate.toDateString() === today.toDateString()
}).length; }).length
// Activity type distribution // Activity type distribution
const typeDistribution = Object.values(CrmActivityTypeEnum).map((type) => ({ const typeDistribution = Object.values(CrmActivityTypeEnum).map((type) => ({
type, type,
count: activities.filter((a) => a.activityType === type).length, count: activities.filter((a) => a.activityType === type).length,
completed: activities.filter( completed: activities.filter(
(a) => (a) => a.activityType === type && a.status === ActivityStatusEnum.Completed,
a.activityType === type && a.status === ActivityStatusEnum.Completed
).length, ).length,
})); }))
return ( return (
<Container>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-xl font-bold text-gray-900"> <h2 className="text-xl font-bold text-gray-900">Görüşme / Aktivite Kayıtları</h2>
Görüşme / Aktivite Kayıtları <p className="text-sm text-gray-600 mt-1">Müşteri etkileşimleri ve aktivite takibi</p>
</h2>
<p className="text-sm text-gray-600 mt-1">
Müşteri etkileşimleri ve aktivite takibi
</p>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
@ -294,45 +273,22 @@ const ActivityRecords: React.FC = () => {
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Widget <Widget title="Toplam Aktivite" value={totalActivities} color="blue" icon="FaCalendar" />
title="Toplam Aktivite"
value={totalActivities}
color="blue"
icon="FaCalendar"
/>
<Widget <Widget title="Bekleyen" value={pendingActivities} color="orange" icon="FaClock" />
title="Bekleyen"
value={pendingActivities}
color="orange"
icon="FaClock"
/>
<Widget <Widget title="Tamamlanan" value={completedActivities} color="green" icon="FaUsers" />
title="Tamamlanan"
value={completedActivities}
color="green"
icon="FaUsers"
/>
<Widget <Widget title="Bugün" value={todayActivities} color="purple" icon="FaCalendar" />
title="Bugün"
value={todayActivities}
color="purple"
icon="FaCalendar"
/>
</div> </div>
{/* Activity Types Distribution */} {/* Activity Types Distribution */}
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3"> <h3 className="text-base font-semibold text-gray-900 mb-3">Aktivite Türü Dağılımı</h3>
Aktivite Türü Dağılımı
</h3>
<div className="grid grid-cols-2 md:grid-cols-7 gap-3"> <div className="grid grid-cols-2 md:grid-cols-7 gap-3">
{typeDistribution.map(({ type, count, completed }) => { {typeDistribution.map(({ type, count, completed }) => {
const Icon = getActivityTypeIcon(type); const Icon = getActivityTypeIcon(type)
const completionRate = const completionRate = count > 0 ? Math.round((completed / count) * 100) : 0
count > 0 ? Math.round((completed / count) * 100) : 0;
return ( return (
<div key={type} className="text-center p-4 border rounded-lg"> <div key={type} className="text-center p-4 border rounded-lg">
@ -340,38 +296,28 @@ const ActivityRecords: React.FC = () => {
<Icon className="w-5 h-5 text-blue-600" /> <Icon className="w-5 h-5 text-blue-600" />
</div> </div>
<div className="text-sm font-medium text-gray-600 mb-1"> <div className="text-sm font-medium text-gray-600 mb-1">
{getPsActivityTypeText(type)} {getActivityTypeText(type)}
</div> </div>
<div className="text-xl font-bold text-gray-900 mb-1"> <div className="text-xl font-bold text-gray-900 mb-1">{count}</div>
{count} <div className="text-xxs text-gray-500">%{completionRate} tamamlandı</div>
</div> </div>
<div className="text-xxs text-gray-500"> )
%{completionRate} tamamlandı
</div>
</div>
);
})} })}
</div> </div>
</div> </div>
{/* Recent Activities */} {/* Recent Activities */}
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3"> <h3 className="text-base font-semibold text-gray-900 mb-3">Son Aktiviteler</h3>
Son Aktiviteler
</h3>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
{activities {activities
.sort( .sort(
(a, b) => (a, b) => new Date(b.activityDate).getTime() - new Date(a.activityDate).getTime(),
new Date(b.activityDate).getTime() -
new Date(a.activityDate).getTime()
) )
.slice(0, 5) .slice(0, 5)
.map((activity) => { .map((activity) => {
const Icon = getActivityTypeIcon(activity.activityType); const Icon = getActivityTypeIcon(activity.activityType)
const customer = customers.find( const customer = customers.find((c) => c.id === activity.customerId)
(c) => c.id === activity.customerId
);
return ( return (
<div <div
@ -383,31 +329,26 @@ const ActivityRecords: React.FC = () => {
<Icon className="w-5 h-5 text-blue-600" /> <Icon className="w-5 h-5 text-blue-600" />
</div> </div>
<div> <div>
<h4 className="font-medium text-gray-900"> <h4 className="font-medium text-gray-900">{activity.subject}</h4>
{activity.subject}
</h4>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{customer?.name} {" "} {customer?.name} {getActivityTypeText(activity.activityType)}
{getPsActivityTypeText(activity.activityType)}
</p> </p>
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right">
<div className="text-sm text-gray-900"> <div className="text-sm text-gray-900">
{new Date(activity.activityDate).toLocaleDateString( {new Date(activity.activityDate).toLocaleDateString('tr-TR')}
"tr-TR"
)}
</div> </div>
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor( className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor(
activity.status activity.status,
)}`} )}`}
> >
{getActivityStatusText(activity.status)} {getActivityStatusText(activity.status)}
</span> </span>
</div> </div>
</div> </div>
); )
})} })}
</div> </div>
</div> </div>
@ -426,24 +367,20 @@ const ActivityRecords: React.FC = () => {
<select <select
value={selectedType} value={selectedType}
onChange={(e) => onChange={(e) => setSelectedType(e.target.value as CrmActivityTypeEnum | 'all')}
setSelectedType(e.target.value as CrmActivityTypeEnum | "all")
}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="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="all">Tüm Türler</option> <option value="all">Tüm Türler</option>
{Object.values(CrmActivityTypeEnum).map((type) => ( {Object.values(CrmActivityTypeEnum).map((type) => (
<option key={type} value={type}> <option key={type} value={type}>
{getPsActivityTypeText(type)} {getActivityTypeText(type)}
</option> </option>
))} ))}
</select> </select>
<select <select
value={selectedStatus} value={selectedStatus}
onChange={(e) => onChange={(e) => setSelectedStatus(e.target.value as ActivityStatusEnum | 'all')}
setSelectedStatus(e.target.value as ActivityStatusEnum | "all")
}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="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="all">Tüm Durumlar</option> <option value="all">Tüm Durumlar</option>
@ -456,21 +393,21 @@ const ActivityRecords: React.FC = () => {
<div className="flex bg-gray-100 rounded-md p-1"> <div className="flex bg-gray-100 rounded-md p-1">
<button <button
onClick={() => setViewMode("list")} onClick={() => setViewMode('list')}
className={`px-3 py-1.5 flex items-center gap-2 text-sm ${ className={`px-3 py-1.5 flex items-center gap-2 text-sm ${
viewMode === "list" viewMode === 'list'
? "bg-blue-600 text-white" ? 'bg-blue-600 text-white'
: "bg-white text-gray-700 hover:bg-gray-50" : 'bg-white text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<FaList className="w-4 h-4" /> <FaList className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => setViewMode("cards")} onClick={() => setViewMode('cards')}
className={`px-3 py-1.5 flex items-center gap-2 text-sm ${ className={`px-3 py-1.5 flex items-center gap-2 text-sm ${
viewMode === "cards" viewMode === 'cards'
? "bg-blue-600 text-white" ? 'bg-blue-600 text-white'
: "bg-white text-gray-700 hover:bg-gray-50" : 'bg-white text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<FaTh className="w-4 h-4" /> <FaTh className="w-4 h-4" />
@ -479,17 +416,15 @@ const ActivityRecords: React.FC = () => {
</div> </div>
{/* Data Display */} {/* Data Display */}
{viewMode === "list" ? ( {viewMode === 'list' ? (
<div className="bg-white rounded-lg shadow-sm border"> <div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredActivities} columns={columns} /> <DataTable data={filteredActivities} columns={columns} />
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredActivities.map((activity) => { {filteredActivities.map((activity) => {
const customer = customers.find( const customer = customers.find((c) => c.id === activity.customerId)
(c) => c.id === activity.customerId const ActivityIcon = getActivityTypeIcon(activity.activityType)
);
const ActivityIcon = getActivityTypeIcon(activity.activityType);
return ( return (
<div <div
@ -507,13 +442,13 @@ const ActivityRecords: React.FC = () => {
{activity.subject} {activity.subject}
</h3> </h3>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{getPsActivityTypeText(activity.activityType)} {getActivityTypeText(activity.activityType)}
</p> </p>
</div> </div>
</div> </div>
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor( className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor(
activity.status activity.status,
)}`} )}`}
> >
{getActivityStatusText(activity.status)} {getActivityStatusText(activity.status)}
@ -525,28 +460,21 @@ const ActivityRecords: React.FC = () => {
{customer && ( {customer && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<FaBuilding className="w-4 h-4 text-gray-400" /> <FaBuilding className="w-4 h-4 text-gray-400" />
<span className="text-gray-900 truncate"> <span className="text-gray-900 truncate">{customer.name}</span>
{customer.name}
</span>
</div> </div>
)} )}
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<FaCalendar className="w-4 h-4 text-gray-400" /> <FaCalendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">
{new Date(activity.activityDate).toLocaleDateString( {new Date(activity.activityDate).toLocaleDateString('tr-TR')}
"tr-TR"
)}
{activity.startTime && ( {activity.startTime && (
<span className="text-gray-500 ml-1"> <span className="text-gray-500 ml-1">
{" "} {' '}
{new Date(activity.startTime).toLocaleTimeString( {new Date(activity.startTime).toLocaleTimeString('tr-TR', {
"tr-TR", hour: '2-digit',
{ minute: '2-digit',
hour: "2-digit", })}
minute: "2-digit",
}
)}
</span> </span>
)} )}
</span> </span>
@ -560,9 +488,7 @@ const ActivityRecords: React.FC = () => {
{activity.duration && ( {activity.duration && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<FaClock className="w-4 h-4 text-gray-400" /> <FaClock className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">{activity.duration} dk</span>
{activity.duration} dk
</span>
</div> </div>
)} )}
@ -571,29 +497,29 @@ const ActivityRecords: React.FC = () => {
className={`w-4 h-4 ${ className={`w-4 h-4 ${
activity.priority === PriorityEnum.High || activity.priority === PriorityEnum.High ||
activity.priority === PriorityEnum.Urgent activity.priority === PriorityEnum.Urgent
? "text-red-500" ? 'text-red-500'
: activity.priority === PriorityEnum.Normal : activity.priority === PriorityEnum.Normal
? "text-blue-500" ? 'text-blue-500'
: "text-gray-400" : 'text-gray-400'
}`} }`}
/> />
<span <span
className={`${ className={`${
activity.priority === PriorityEnum.High || activity.priority === PriorityEnum.High ||
activity.priority === PriorityEnum.Urgent activity.priority === PriorityEnum.Urgent
? "text-red-600" ? 'text-red-600'
: activity.priority === PriorityEnum.Normal : activity.priority === PriorityEnum.Normal
? "text-blue-600" ? 'text-blue-600'
: "text-gray-600" : 'text-gray-600'
}`} }`}
> >
{activity.priority === PriorityEnum.Low {activity.priority === PriorityEnum.Low
? "Düşük" ? 'Düşük'
: activity.priority === PriorityEnum.Normal : activity.priority === PriorityEnum.Normal
? "Normal" ? 'Normal'
: activity.priority === PriorityEnum.High : activity.priority === PriorityEnum.High
? "Yüksek" ? 'Yüksek'
: "Acil"} : 'Acil'}
</span> </span>
</div> </div>
@ -601,11 +527,11 @@ const ActivityRecords: React.FC = () => {
<p <p
className="text-sm text-gray-600 overflow-hidden" className="text-sm text-gray-600 overflow-hidden"
style={{ style={{
display: "-webkit-box", display: '-webkit-box',
WebkitLineClamp: 2, WebkitLineClamp: 2,
WebkitBoxOrient: "vertical", WebkitBoxOrient: 'vertical',
lineHeight: "1.4em", lineHeight: '1.4em',
maxHeight: "2.8em", maxHeight: '2.8em',
}} }}
> >
{activity.description} {activity.description}
@ -635,7 +561,7 @@ const ActivityRecords: React.FC = () => {
</button> </button>
</div> </div>
</div> </div>
); )
})} })}
</div> </div>
)} )}
@ -643,12 +569,8 @@ const ActivityRecords: React.FC = () => {
{filteredActivities.length === 0 && ( {filteredActivities.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<FaCalendar className="w-12 h-12 text-gray-400 mx-auto mb-4" /> <FaCalendar className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">Aktivite bulunamadı</h3>
Aktivite bulunamadı <p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
</div> </div>
)} )}
@ -668,7 +590,8 @@ const ActivityRecords: React.FC = () => {
activity={selectedActivity} activity={selectedActivity}
/> />
</div> </div>
); </Container>
}; )
}
export default ActivityRecords; export default ActivityRecords

View file

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom'
import { useQuery } from "@tanstack/react-query"; import { useQuery } from '@tanstack/react-query'
import { import {
FaUserCheck, FaUserCheck,
FaPlus, FaPlus,
@ -19,62 +19,55 @@ import {
FaGlobe, FaGlobe,
FaUser, FaUser,
FaChartLine, FaChartLine,
} from "react-icons/fa"; } from 'react-icons/fa'
import classNames from "classnames"; import classNames from 'classnames'
import { CustomerSegmentEnum } from "../../../types/crm"; import { CustomerSegmentEnum } from '../../../types/crm'
import dayjs from "dayjs"; import dayjs from 'dayjs'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import Widget from "../../../components/common/Widget"; import Widget from '../../../components/common/Widget'
import { BusinessPartyStatusEnum, PartyType } from "../../../types/common"; import { BusinessPartyStatusEnum, PartyType } from '../../../types/common'
import { import {
getBusinessPartyStatusColor, getBusinessPartyStatusColor,
getBusinessPartyStatusName, getBusinessPartyStatusName,
getCustomerSegmentName, getCustomerSegmentName,
} from "../../../utils/erp"; } from '../../../utils/erp'
const CustomerCards: React.FC = () => { const CustomerCards: React.FC = () => {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState('')
const [filterStatus, setFilterStatus] = useState("all"); const [filterStatus, setFilterStatus] = useState('all')
const [filterSegment, setFilterSegment] = useState("all"); const [filterSegment, setFilterSegment] = useState('all')
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false)
const { const {
data: customers, data: customers,
isLoading, isLoading,
error, error,
} = useQuery({ } = useQuery({
queryKey: ["customers", searchTerm, filterStatus, filterSegment], queryKey: ['customers', searchTerm, filterStatus, filterSegment],
queryFn: async () => { queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500))
const mockCustomers = mockBusinessParties.filter( const mockCustomers = mockBusinessParties.filter(
(customer) => customer.partyType === PartyType.Customer (customer) => customer.partyType === PartyType.Customer,
); )
return mockCustomers.filter((customer) => { return mockCustomers.filter((customer) => {
const matchesSearch = const matchesSearch =
customer.code?.toLowerCase().includes(searchTerm.toLowerCase()) || customer.code?.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.name?.toLowerCase().includes(searchTerm.toLowerCase()) || customer.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.primaryContact?.fullName customer.primaryContact?.fullName?.toLowerCase().includes(searchTerm.toLowerCase())
?.toLowerCase() const matchesStatus = filterStatus === 'all' || customer.status === filterStatus
.includes(searchTerm.toLowerCase()); const matchesSegment = filterSegment === 'all' || customer.customerSegment === filterSegment
const matchesStatus = return matchesSearch && matchesStatus && matchesSegment
filterStatus === "all" || customer.status === filterStatus; })
const matchesSegment =
filterSegment === "all" || customer.customerSegment === filterSegment;
return matchesSearch && matchesStatus && matchesSegment;
});
}, },
}); })
if (isLoading) { if (isLoading) {
return ( return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
{[...Array(12)].map((_, index) => ( {[...Array(12)].map((_, index) => (
<div <div key={index} className="bg-white rounded-xl shadow-sm border animate-pulse">
key={index}
className="bg-white rounded-xl shadow-sm border animate-pulse"
>
<div className="p-6"> <div className="p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
@ -105,7 +98,7 @@ const CustomerCards: React.FC = () => {
</div> </div>
))} ))}
</div> </div>
); )
} }
if (error) { if (error) {
@ -113,12 +106,10 @@ const CustomerCards: React.FC = () => {
<div className="bg-red-50 border border-red-200 rounded-lg p-4"> <div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center"> <div className="flex items-center">
<FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" /> <FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" />
<span className="text-red-800"> <span className="text-red-800">Müşteriler yüklenirken hata oluştu.</span>
Müşteriler yüklenirken hata oluştu.
</span>
</div> </div>
</div> </div>
); )
} }
return ( return (
@ -143,10 +134,10 @@ const CustomerCards: React.FC = () => {
<button <button
onClick={() => setShowFilters(!showFilters)} onClick={() => setShowFilters(!showFilters)}
className={classNames( className={classNames(
"flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors", 'flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors',
showFilters showFilters
? "border-blue-500 bg-blue-50 text-blue-700" ? 'border-blue-500 bg-blue-50 text-blue-700'
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50" : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
)} )}
> >
<FaFilter size={14} className="mr-2" /> <FaFilter size={14} className="mr-2" />
@ -156,7 +147,7 @@ const CustomerCards: React.FC = () => {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
onClick={() => alert("Dışa aktarma özelliği yakında eklenecek")} onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
> >
<FaDownload size={14} className="mr-2" /> <FaDownload size={14} className="mr-2" />
@ -178,18 +169,14 @@ const CustomerCards: React.FC = () => {
<div className="bg-white border border-gray-200 rounded-lg p-3"> <div className="bg-white border border-gray-200 rounded-lg p-3">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
Durum
</label>
<select <select
value={filterStatus} value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)} onChange={(e) => setFilterStatus(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="all">Tümü</option> <option value="all">Tümü</option>
<option value={BusinessPartyStatusEnum.Prospect}> <option value={BusinessPartyStatusEnum.Prospect}>Potansiyel</option>
Potansiyel
</option>
<option value={BusinessPartyStatusEnum.Active}>Aktif</option> <option value={BusinessPartyStatusEnum.Active}>Aktif</option>
<option value={BusinessPartyStatusEnum.Inactive}>Pasif</option> <option value={BusinessPartyStatusEnum.Inactive}>Pasif</option>
<option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option> <option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option>
@ -197,9 +184,7 @@ const CustomerCards: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Segment</label>
Segment
</label>
<select <select
value={filterSegment} value={filterSegment}
onChange={(e) => setFilterSegment(e.target.value)} onChange={(e) => setFilterSegment(e.target.value)}
@ -216,9 +201,9 @@ const CustomerCards: React.FC = () => {
<div className="flex items-end"> <div className="flex items-end">
<button <button
onClick={() => { onClick={() => {
setFilterStatus("all"); setFilterStatus('all')
setFilterSegment("all"); setFilterSegment('all')
setSearchTerm(""); setSearchTerm('')
}} }}
className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
> >
@ -240,11 +225,7 @@ const CustomerCards: React.FC = () => {
<Widget <Widget
title="Aktif Müşteri" title="Aktif Müşteri"
value={ value={customers?.filter((c) => c.status === BusinessPartyStatusEnum.Active).length || 0}
customers?.filter(
(c) => c.status === BusinessPartyStatusEnum.Active
).length || 0
}
color="green" color="green"
icon="FaUserCheck" icon="FaUserCheck"
/> />
@ -252,9 +233,7 @@ const CustomerCards: React.FC = () => {
<Widget <Widget
title="Toplam Ciro" title="Toplam Ciro"
value={`${ value={`${
customers customers?.reduce((acc, c) => acc + (c.totalRevenue ?? 0), 0).toLocaleString() || 0
?.reduce((acc, c) => acc + (c.totalRevenue ?? 0), 0)
.toLocaleString() || 0
}`} }`}
color="purple" color="purple"
icon="FaDollarSign" icon="FaDollarSign"
@ -265,10 +244,8 @@ const CustomerCards: React.FC = () => {
value={`${ value={`${
customers?.length customers?.length
? Math.round( ? Math.round(
customers.reduce( customers.reduce((acc, c) => acc + (c.averageOrderValue ?? 0), 0) /
(acc, c) => acc + (c.averageOrderValue ?? 0), customers.length,
0
) / customers.length
).toLocaleString() ).toLocaleString()
: 0 : 0
}`} }`}
@ -288,14 +265,14 @@ const CustomerCards: React.FC = () => {
{/* Status Indicator */} {/* Status Indicator */}
<div <div
className={classNames( className={classNames(
"absolute top-0 left-0 w-full h-1", 'absolute top-0 left-0 w-full h-1',
customer.status === BusinessPartyStatusEnum.Active customer.status === BusinessPartyStatusEnum.Active
? "bg-green-500" ? 'bg-green-500'
: customer.status === BusinessPartyStatusEnum.Prospect : customer.status === BusinessPartyStatusEnum.Prospect
? "bg-blue-500" ? 'bg-blue-500'
: customer.status === BusinessPartyStatusEnum.Blocked : customer.status === BusinessPartyStatusEnum.Blocked
? "bg-red-500" ? 'bg-red-500'
: "bg-gray-400" : 'bg-gray-400',
)} )}
></div> ></div>
@ -317,8 +294,8 @@ const CustomerCards: React.FC = () => {
</div> </div>
<span <span
className={classNames( className={classNames(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border", 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border',
getBusinessPartyStatusColor(customer.status!) getBusinessPartyStatusColor(customer.status!),
)} )}
> >
{getBusinessPartyStatusName(customer.status!)} {getBusinessPartyStatusName(customer.status!)}
@ -331,9 +308,7 @@ const CustomerCards: React.FC = () => {
{customer.name} {customer.name}
</h2> </h2>
{customer.industry && ( {customer.industry && (
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">{customer.industry}</p>
{customer.industry}
</p>
)} )}
</div> </div>
@ -341,15 +316,11 @@ const CustomerCards: React.FC = () => {
<div className="space-y-1.5 mb-3 text-xs"> <div className="space-y-1.5 mb-3 text-xs">
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
<FaUser className="w-4 h-4 mr-2 text-gray-400" /> <FaUser className="w-4 h-4 mr-2 text-gray-400" />
<span className="truncate"> <span className="truncate">{customer.primaryContact?.fullName}</span>
{customer.primaryContact?.fullName}
</span>
</div> </div>
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
<FaEnvelope className="w-4 h-4 mr-2 text-gray-400" /> <FaEnvelope className="w-4 h-4 mr-2 text-gray-400" />
<span className="truncate"> <span className="truncate">{customer.primaryContact?.email}</span>
{customer.primaryContact?.email}
</span>
</div> </div>
{customer.primaryContact?.phone && ( {customer.primaryContact?.phone && (
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
@ -384,17 +355,13 @@ const CustomerCards: React.FC = () => {
<div className="text-base font-bold text-green-700"> <div className="text-base font-bold text-green-700">
{(customer.totalRevenue! / 1000).toLocaleString()}K {(customer.totalRevenue! / 1000).toLocaleString()}K
</div> </div>
<div className="text-xs text-green-600 font-medium"> <div className="text-xs text-green-600 font-medium">Toplam Ciro</div>
Toplam Ciro
</div>
</div> </div>
<div className="text-center p-2 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-lg border border-blue-100"> <div className="text-center p-2 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-lg border border-blue-100">
<div className="text-base font-bold text-blue-700"> <div className="text-base font-bold text-blue-700">
{(customer.averageOrderValue! / 1000).toLocaleString()}K {(customer.averageOrderValue! / 1000).toLocaleString()}K
</div> </div>
<div className="text-xs text-blue-600 font-medium"> <div className="text-xs text-blue-600 font-medium">Ort. Sipariş</div>
Ort. Sipariş
</div>
</div> </div>
</div> </div>
@ -402,16 +369,12 @@ const CustomerCards: React.FC = () => {
<div className="flex items-center justify-between text-xs text-gray-500 mb-3"> <div className="flex items-center justify-between text-xs text-gray-500 mb-3">
<div className="flex items-center"> <div className="flex items-center">
<FaStar className="w-4 h-4 text-yellow-500 mr-1" /> <FaStar className="w-4 h-4 text-yellow-500 mr-1" />
<span> <span>LTV: {(customer.lifetimeValue! / 1000000).toFixed(1)}M</span>
LTV: {(customer.lifetimeValue! / 1000000).toFixed(1)}M
</span>
</div> </div>
{customer.lastOrderDate && ( {customer.lastOrderDate && (
<div className="flex items-center"> <div className="flex items-center">
<FaCalendar className="w-4 h-4 mr-1" /> <FaCalendar className="w-4 h-4 mr-1" />
<span> <span>{dayjs(customer.lastOrderDate).format('DD.MM.YY')}</span>
{dayjs(customer.lastOrderDate).format("DD.MM.YY")}
</span>
</div> </div>
)} )}
</div> </div>
@ -419,9 +382,7 @@ const CustomerCards: React.FC = () => {
{/* Credit Limit Progress */} {/* Credit Limit Progress */}
<div className="mb-3"> <div className="mb-3">
<div className="flex justify-between text-xs mb-1"> <div className="flex justify-between text-xs mb-1">
<span className="text-gray-600 font-medium"> <span className="text-gray-600 font-medium">Kredi Kullanımı</span>
Kredi Kullanımı
</span>
<span className="font-semibold text-gray-900"> <span className="font-semibold text-gray-900">
{customer.creditLimit.toLocaleString()} {customer.creditLimit.toLocaleString()}
</span> </span>
@ -429,24 +390,17 @@ const CustomerCards: React.FC = () => {
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden"> <div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div <div
className={classNames( className={classNames(
"h-2.5 rounded-full transition-all duration-500", 'h-2.5 rounded-full transition-all duration-500',
Math.min( Math.min((customer.totalRevenue! / customer.creditLimit) * 100, 100) > 80
(customer.totalRevenue! / customer.creditLimit) * 100, ? 'bg-gradient-to-r from-red-500 to-red-600'
100 : Math.min((customer.totalRevenue! / customer.creditLimit) * 100, 100) > 60
) > 80 ? 'bg-gradient-to-r from-yellow-400 to-yellow-500'
? "bg-gradient-to-r from-red-500 to-red-600" : 'bg-gradient-to-r from-green-500 to-green-600',
: Math.min(
(customer.totalRevenue! / customer.creditLimit) *
100,
100
) > 60
? "bg-gradient-to-r from-yellow-400 to-yellow-500"
: "bg-gradient-to-r from-green-500 to-green-600"
)} )}
style={{ style={{
width: `${Math.min( width: `${Math.min(
(customer.totalRevenue! / customer.creditLimit) * 100, (customer.totalRevenue! / customer.creditLimit) * 100,
100 100,
)}%`, )}%`,
}} }}
></div> ></div>
@ -454,10 +408,7 @@ const CustomerCards: React.FC = () => {
<div className="flex justify-between text-xs text-gray-500 mt-1"> <div className="flex justify-between text-xs text-gray-500 mt-1">
<span>0</span> <span>0</span>
<span className="font-medium"> <span className="font-medium">
{Math.round( {Math.round((customer.totalRevenue! / customer.creditLimit) * 100)}% kullanıldı
(customer.totalRevenue! / customer.creditLimit) * 100
)}
% kullanıldı
</span> </span>
</div> </div>
</div> </div>
@ -485,17 +436,15 @@ const CustomerCards: React.FC = () => {
<div className="flex items-center text-xs"> <div className="flex items-center text-xs">
<div <div
className={classNames( className={classNames(
"flex items-center px-2 py-1 rounded-full text-xs font-medium", 'flex items-center px-2 py-1 rounded-full text-xs font-medium',
customer.status === BusinessPartyStatusEnum.Active customer.status === BusinessPartyStatusEnum.Active
? "bg-green-100 text-green-700" ? 'bg-green-100 text-green-700'
: "bg-gray-100 text-gray-600" : 'bg-gray-100 text-gray-600',
)} )}
> >
<FaChartLine className="w-3 h-3 mr-1" /> <FaChartLine className="w-3 h-3 mr-1" />
<span> <span>
{customer.status === BusinessPartyStatusEnum.Active {customer.status === BusinessPartyStatusEnum.Active ? 'Aktif' : 'Durgun'}
? "Aktif"
: "Durgun"}
</span> </span>
</div> </div>
</div> </div>
@ -509,12 +458,9 @@ const CustomerCards: React.FC = () => {
{(!customers || customers.length === 0) && ( {(!customers || customers.length === 0) && (
<div className="text-center py-10"> <div className="text-center py-10">
<FaUserCheck className="mx-auto h-12 w-12 text-gray-400 mb-3" /> <FaUserCheck className="mx-auto h-12 w-12 text-gray-400 mb-3" />
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">Müşteri bulunamadı</h3>
Müşteri bulunamadı
</h3>
<p className="text-sm text-gray-500 mb-4"> <p className="text-sm text-gray-500 mb-4">
Yeni müşteri ekleyerek başlayın veya arama kriterlerinizi Yeni müşteri ekleyerek başlayın veya arama kriterlerinizi değiştirin.
değiştirin.
</p> </p>
<Link <Link
to="/admin/crm/customers/new" to="/admin/crm/customers/new"
@ -526,7 +472,7 @@ const CustomerCards: React.FC = () => {
</div> </div>
)} )}
</div> </div>
); )
}; }
export default CustomerCards; export default CustomerCards

View file

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { useParams, useNavigate, Link } from "react-router-dom"; import { useParams, useNavigate, Link } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { import {
FaArrowLeft, FaArrowLeft,
FaSave, FaSave,
@ -11,117 +11,105 @@ import {
FaIdCard, FaIdCard,
FaCreditCard, FaCreditCard,
FaExclamationTriangle, FaExclamationTriangle,
} from "react-icons/fa"; } from 'react-icons/fa'
import classNames from "classnames"; import classNames from 'classnames'
import { CustomerSegmentEnum, CustomerTypeEnum } from "../../../types/crm"; import { CustomerSegmentEnum, CustomerTypeEnum } from '../../../types/crm'
import { import { mockBusinessParties, mockBusinessPartyNew } from '../../../mocks/mockBusinessParties'
mockBusinessParties, import { BusinessParty, BusinessPartyStatusEnum, PaymentTerms } from '../../../types/common'
mockBusinessPartyNew, import { Container } from '@/components/shared'
} from "../../../mocks/mockBusinessParties";
import {
BusinessParty,
BusinessPartyStatusEnum,
PaymentTerms,
} from "../../../types/common";
const CustomerEdit: React.FC = () => { const CustomerEdit: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>()
const navigate = useNavigate(); const navigate = useNavigate()
const queryClient = useQueryClient(); const queryClient = useQueryClient()
const [activeTab, setActiveTab] = useState("basic"); const [activeTab, setActiveTab] = useState('basic')
const [isDirty, setIsDirty] = useState(false); const [isDirty, setIsDirty] = useState(false)
const { const {
data: customer, data: customer,
isLoading, isLoading,
error, error,
} = useQuery({ } = useQuery({
queryKey: ["customer", id], queryKey: ['customer', id],
queryFn: async () => { queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300))
const found = mockBusinessParties.find((c) => c.id === id); const found = mockBusinessParties.find((c) => c.id === id)
if (!found) throw new Error("Müşteri bulunamadı"); if (!found) throw new Error('Müşteri bulunamadı')
return found; return found
}, },
}); })
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew); const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew)
// Initialize form data when customer is loaded // Initialize form data when customer is loaded
React.useEffect(() => { React.useEffect(() => {
if (customer) { if (customer) {
setFormData( setFormData(mockBusinessParties.find((c) => c.id === id) || mockBusinessPartyNew)
mockBusinessParties.find((c) => c.id === id) || mockBusinessPartyNew
);
} }
}, [customer, id]); }, [customer, id])
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: async (data: BusinessParty) => { mutationFn: async (data: BusinessParty) => {
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000))
// Simulate API call // Simulate API call
return { ...customer, ...data }; return { ...customer, ...data }
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["customers"] }); queryClient.invalidateQueries({ queryKey: ['customers'] })
queryClient.invalidateQueries({ queryKey: ["customer", id] }); queryClient.invalidateQueries({ queryKey: ['customer', id] })
setIsDirty(false); setIsDirty(false)
navigate(`/admin/crm/customers/${id}`); navigate(`/admin/crm/customers/${id}`)
}, },
}); })
const handleInputChange = (field: string, value: string | number) => { const handleInputChange = (field: string, value: string | number) => {
const keys = field.split("."); const keys = field.split('.')
if (keys.length === 2) { if (keys.length === 2) {
const [section, subField] = keys; const [section, subField] = keys
setFormData((prev: any) => { setFormData((prev: any) => {
if (section === "primaryContact") { if (section === 'primaryContact') {
return { return {
...prev, ...prev,
primaryContact: { primaryContact: {
...prev.primaryContact, ...prev.primaryContact,
[subField]: value, [subField]: value,
}, },
}; }
} else if (section === "address") { } else if (section === 'address') {
return { return {
...prev, ...prev,
address: { address: {
...prev.address, ...prev.address,
[subField]: value, [subField]: value,
}, },
};
} }
return prev; }
}); return prev
})
} else { } else {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[field]: value, [field]: value,
})); }))
}
setIsDirty(true)
} }
setIsDirty(true);
};
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault()
updateMutation.mutate(formData); updateMutation.mutate(formData)
}; }
const handleCancel = () => { const handleCancel = () => {
if (isDirty) { if (isDirty) {
if ( if (window.confirm('Değişiklikleriniz kaydedilmedi. Çıkmak istediğinizden emin misiniz?')) {
window.confirm( navigate(`/admin/crm/customers/${id}`)
"Değişiklikleriniz kaydedilmedi. Çıkmak istediğinizden emin misiniz?"
)
) {
navigate(`/admin/crm/customers/${id}`);
} }
} else { } else {
navigate(`/admin/crm/customers/${id}`); navigate(`/admin/crm/customers/${id}`)
}
} }
};
if (isLoading) { if (isLoading) {
return ( return (
@ -139,7 +127,7 @@ const CustomerEdit: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
); )
} }
if (error || !customer) { if (error || !customer) {
@ -150,17 +138,13 @@ const CustomerEdit: React.FC = () => {
<div className="flex items-center"> <div className="flex items-center">
<FaExclamationTriangle className="h-8 w-8 text-red-600 mr-3" /> <FaExclamationTriangle className="h-8 w-8 text-red-600 mr-3" />
<div> <div>
<h3 className="text-lg font-medium text-red-800"> <h3 className="text-lg font-medium text-red-800">Müşteri Bulunamadı</h3>
Müşteri Bulunamadı <p className="text-red-600">Düzenlemek istediğiniz müşteri mevcut değil.</p>
</h3>
<p className="text-red-600">
Düzenlemek istediğiniz müşteri mevcut değil.
</p>
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={() => navigate("/admin/crm/customers")} onClick={() => navigate('/admin/crm/customers')}
className="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors" className="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
> >
<FaArrowLeft className="w-4 h-4 mr-2" /> <FaArrowLeft className="w-4 h-4 mr-2" />
@ -170,17 +154,18 @@ const CustomerEdit: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
); )
} }
const tabs = [ const tabs = [
{ id: "basic", label: "Temel Bilgiler", icon: FaBuilding }, { id: 'basic', label: 'Temel Bilgiler', icon: FaBuilding },
{ id: "contact", label: "İletişim", icon: FaUser }, { id: 'contact', label: 'İletişim', icon: FaUser },
{ id: "business", label: "İş Bilgileri", icon: FaIdCard }, { id: 'business', label: 'İş Bilgileri', icon: FaIdCard },
{ id: "financial", label: "Finansal", icon: FaCreditCard }, { id: 'financial', label: 'Finansal', icon: FaCreditCard },
]; ]
return ( return (
<Container>
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
{/* Header */} {/* Header */}
@ -192,10 +177,7 @@ const CustomerEdit: React.FC = () => {
Müşteriler Müşteriler
</Link> </Link>
<span>/</span> <span>/</span>
<Link <Link to={`/admin/crm/customers/${id}`} className="hover:text-blue-600">
to={`/admin/crm/customers/${id}`}
className="hover:text-blue-600"
>
{customer.name} {customer.name}
</Link> </Link>
<span>/</span> <span>/</span>
@ -219,9 +201,7 @@ const CustomerEdit: React.FC = () => {
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-gray-900"> <h1 className="text-xl font-bold text-gray-900">Müşteri Düzenle</h1>
Müşteri Düzenle
</h1>
<p className="text-sm text-gray-600">{customer.name}</p> <p className="text-sm text-gray-600">{customer.name}</p>
</div> </div>
</div> </div>
@ -240,10 +220,10 @@ const CustomerEdit: React.FC = () => {
type="submit" type="submit"
disabled={!isDirty || updateMutation.isPending} disabled={!isDirty || updateMutation.isPending}
className={classNames( className={classNames(
"inline-flex items-center px-3 py-1.5 text-sm rounded-lg transition-colors", 'inline-flex items-center px-3 py-1.5 text-sm rounded-lg transition-colors',
isDirty && !updateMutation.isPending isDirty && !updateMutation.isPending
? "bg-blue-600 text-white hover:bg-blue-700" ? 'bg-blue-600 text-white hover:bg-blue-700'
: "bg-gray-300 text-gray-500 cursor-not-allowed" : 'bg-gray-300 text-gray-500 cursor-not-allowed',
)} )}
> >
{updateMutation.isPending ? ( {updateMutation.isPending ? (
@ -273,10 +253,10 @@ const CustomerEdit: React.FC = () => {
type="button" type="button"
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={classNames( className={classNames(
"flex items-center space-x-2 py-3 px-1 border-b-2 font-medium text-sm transition-colors", 'flex items-center space-x-2 py-3 px-1 border-b-2 font-medium text-sm transition-colors',
activeTab === tab.id activeTab === tab.id
? "border-blue-500 text-blue-600" ? 'border-blue-500 text-blue-600'
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
)} )}
> >
<tab.icon className="w-4 h-4" /> <tab.icon className="w-4 h-4" />
@ -289,7 +269,7 @@ const CustomerEdit: React.FC = () => {
{/* Form Content */} {/* Form Content */}
<div className="mx-auto py-4"> <div className="mx-auto py-4">
{activeTab === "basic" && ( {activeTab === 'basic' && (
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center"> <h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaBuilding className="w-5 h-5 mr-2" /> <FaBuilding className="w-5 h-5 mr-2" />
@ -304,7 +284,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.code} value={formData.code}
onChange={(e) => handleInputChange("code", e.target.value)} onChange={(e) => handleInputChange('code', e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required required
/> />
@ -317,7 +297,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.name} value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)} onChange={(e) => handleInputChange('name', e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required required
/> />
@ -329,47 +309,33 @@ const CustomerEdit: React.FC = () => {
</label> </label>
<select <select
value={formData.customerType} value={formData.customerType}
onChange={(e) => onChange={(e) => handleInputChange('customerType', e.target.value)}
handleInputChange("customerType", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value={CustomerTypeEnum.Company}>Şirket</option> <option value={CustomerTypeEnum.Company}>Şirket</option>
<option value={CustomerTypeEnum.Individual}> <option value={CustomerTypeEnum.Individual}>Bireysel</option>
Bireysel
</option>
<option value={CustomerTypeEnum.Government}>Kamu</option> <option value={CustomerTypeEnum.Government}>Kamu</option>
<option value={CustomerTypeEnum.NonProfit}> <option value={CustomerTypeEnum.NonProfit}>Kar Amacı Gütmeyen</option>
Kar Amacı Gütmeyen
</option>
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Sektör</label>
Sektör
</label>
<input <input
type="text" type="text"
value={formData.industry} value={formData.industry}
onChange={(e) => onChange={(e) => handleInputChange('industry', e.target.value)}
handleInputChange("industry", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Teknoloji, İmalat, Hizmet vb." placeholder="Teknoloji, İmalat, Hizmet vb."
/> />
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Website</label>
Website
</label>
<input <input
type="url" type="url"
value={formData.website} value={formData.website}
onChange={(e) => onChange={(e) => handleInputChange('website', e.target.value)}
handleInputChange("website", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="https://www.ornek.com" placeholder="https://www.ornek.com"
/> />
@ -381,23 +347,13 @@ const CustomerEdit: React.FC = () => {
</label> </label>
<select <select
value={formData.status} value={formData.status}
onChange={(e) => onChange={(e) => handleInputChange('status', e.target.value)}
handleInputChange("status", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value={BusinessPartyStatusEnum.Prospect}> <option value={BusinessPartyStatusEnum.Prospect}>Potansiyel</option>
Potansiyel <option value={BusinessPartyStatusEnum.Active}>Aktif</option>
</option> <option value={BusinessPartyStatusEnum.Inactive}>Pasif</option>
<option value={BusinessPartyStatusEnum.Active}> <option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option>
Aktif
</option>
<option value={BusinessPartyStatusEnum.Inactive}>
Pasif
</option>
<option value={BusinessPartyStatusEnum.Blocked}>
Blokeli
</option>
</select> </select>
</div> </div>
@ -407,14 +363,10 @@ const CustomerEdit: React.FC = () => {
</label> </label>
<select <select
value={formData.customerSegment} value={formData.customerSegment}
onChange={(e) => onChange={(e) => handleInputChange('customerSegment', e.target.value)}
handleInputChange("customerSegment", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value={CustomerSegmentEnum.Enterprise}> <option value={CustomerSegmentEnum.Enterprise}>Kurumsal</option>
Kurumsal
</option>
<option value={CustomerSegmentEnum.SMB}>KOBİ</option> <option value={CustomerSegmentEnum.SMB}>KOBİ</option>
<option value={CustomerSegmentEnum.Startup}>Girişim</option> <option value={CustomerSegmentEnum.Startup}>Girişim</option>
<option value={CustomerSegmentEnum.Government}>Kamu</option> <option value={CustomerSegmentEnum.Government}>Kamu</option>
@ -424,7 +376,7 @@ const CustomerEdit: React.FC = () => {
</div> </div>
)} )}
{activeTab === "contact" && ( {activeTab === 'contact' && (
<div className="space-y-4"> <div className="space-y-4">
{/* Primary Contact */} {/* Primary Contact */}
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
@ -435,17 +387,12 @@ const CustomerEdit: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Ad *</label>
Ad *
</label>
<input <input
type="text" type="text"
value={formData.primaryContact?.firstName} value={formData.primaryContact?.firstName}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('primaryContact.firstName', e.target.value)
"primaryContact.firstName",
e.target.value
)
} }
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required required
@ -460,10 +407,7 @@ const CustomerEdit: React.FC = () => {
type="text" type="text"
value={formData.primaryContact?.lastName} value={formData.primaryContact?.lastName}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('primaryContact.lastName', e.target.value)
"primaryContact.lastName",
e.target.value
)
} }
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required required
@ -471,18 +415,11 @@ const CustomerEdit: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Ünvan</label>
Ünvan
</label>
<input <input
type="text" type="text"
value={formData.primaryContact?.title} value={formData.primaryContact?.title}
onChange={(e) => onChange={(e) => handleInputChange('primaryContact.title', e.target.value)}
handleInputChange(
"primaryContact.title",
e.target.value
)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Genel Müdür, Satış Direktörü vb." placeholder="Genel Müdür, Satış Direktörü vb."
/> />
@ -496,10 +433,7 @@ const CustomerEdit: React.FC = () => {
type="text" type="text"
value={formData.primaryContact?.department} value={formData.primaryContact?.department}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('primaryContact.department', e.target.value)
"primaryContact.department",
e.target.value
)
} }
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Satış, Pazarlama, İnsan Kaynakları vb." placeholder="Satış, Pazarlama, İnsan Kaynakları vb."
@ -513,12 +447,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="email" type="email"
value={formData.primaryContact?.email} value={formData.primaryContact?.email}
onChange={(e) => onChange={(e) => handleInputChange('primaryContact.email', e.target.value)}
handleInputChange(
"primaryContact.email",
e.target.value
)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required required
/> />
@ -531,12 +460,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="tel" type="tel"
value={formData.primaryContact?.phone} value={formData.primaryContact?.phone}
onChange={(e) => onChange={(e) => handleInputChange('primaryContact.phone', e.target.value)}
handleInputChange(
"primaryContact.phone",
e.target.value
)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="+90 (212) 555 0123" placeholder="+90 (212) 555 0123"
/> />
@ -549,12 +473,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="tel" type="tel"
value={formData.primaryContact?.mobile} value={formData.primaryContact?.mobile}
onChange={(e) => onChange={(e) => handleInputChange('primaryContact.mobile', e.target.value)}
handleInputChange(
"primaryContact.mobile",
e.target.value
)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="+90 (555) 123 4567" placeholder="+90 (555) 123 4567"
/> />
@ -576,9 +495,7 @@ const CustomerEdit: React.FC = () => {
</label> </label>
<textarea <textarea
value={formData.address?.street} value={formData.address?.street}
onChange={(e) => onChange={(e) => handleInputChange('address.street', e.target.value)}
handleInputChange("address.street", e.target.value)
}
rows={3} rows={3}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Sokak, cadde, mahalle, bina no vb." placeholder="Sokak, cadde, mahalle, bina no vb."
@ -594,9 +511,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.address?.city} value={formData.address?.city}
onChange={(e) => onChange={(e) => handleInputChange('address.city', e.target.value)}
handleInputChange("address.city", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required required
/> />
@ -609,9 +524,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.address?.state} value={formData.address?.state}
onChange={(e) => onChange={(e) => handleInputChange('address.state', e.target.value)}
handleInputChange("address.state", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
@ -623,12 +536,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.address?.postalCode} value={formData.address?.postalCode}
onChange={(e) => onChange={(e) => handleInputChange('address.postalCode', e.target.value)}
handleInputChange(
"address.postalCode",
e.target.value
)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
@ -640,9 +548,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.address?.country} value={formData.address?.country}
onChange={(e) => onChange={(e) => handleInputChange('address.country', e.target.value)}
handleInputChange("address.country", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required required
/> />
@ -653,7 +559,7 @@ const CustomerEdit: React.FC = () => {
</div> </div>
)} )}
{activeTab === "business" && ( {activeTab === 'business' && (
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center"> <h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaIdCard className="w-5 h-5 mr-2" /> <FaIdCard className="w-5 h-5 mr-2" />
@ -668,9 +574,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.taxNumber} value={formData.taxNumber}
onChange={(e) => onChange={(e) => handleInputChange('taxNumber', e.target.value)}
handleInputChange("taxNumber", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="1234567890" placeholder="1234567890"
/> />
@ -683,9 +587,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.registrationNumber} value={formData.registrationNumber}
onChange={(e) => onChange={(e) => handleInputChange('registrationNumber', e.target.value)}
handleInputChange("registrationNumber", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="123456789" placeholder="123456789"
/> />
@ -698,9 +600,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.assignedSalesRep} value={formData.assignedSalesRep}
onChange={(e) => onChange={(e) => handleInputChange('assignedSalesRep', e.target.value)}
handleInputChange("assignedSalesRep", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Satış temsilcisi adı" placeholder="Satış temsilcisi adı"
/> />
@ -709,7 +609,7 @@ const CustomerEdit: React.FC = () => {
</div> </div>
)} )}
{activeTab === "financial" && ( {activeTab === 'financial' && (
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center"> <h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaCreditCard className="w-5 h-5 mr-2" /> <FaCreditCard className="w-5 h-5 mr-2" />
@ -724,9 +624,7 @@ const CustomerEdit: React.FC = () => {
<input <input
type="number" type="number"
value={formData.creditLimit} value={formData.creditLimit}
onChange={(e) => onChange={(e) => handleInputChange('creditLimit', Number(e.target.value))}
handleInputChange("creditLimit", Number(e.target.value))
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min="0" min="0"
step="1000" step="1000"
@ -739,9 +637,7 @@ const CustomerEdit: React.FC = () => {
</label> </label>
<select <select
value={formData.currency} value={formData.currency}
onChange={(e) => onChange={(e) => handleInputChange('currency', e.target.value)}
handleInputChange("currency", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="TRY">Türk Lirası (TRY)</option> <option value="TRY">Türk Lirası (TRY)</option>
@ -757,9 +653,7 @@ const CustomerEdit: React.FC = () => {
</label> </label>
<select <select
value={formData.paymentTerms} value={formData.paymentTerms}
onChange={(e) => onChange={(e) => handleInputChange('paymentTerms', e.target.value)}
handleInputChange("paymentTerms", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value={PaymentTerms.Prepaid}>Peşin</option> <option value={PaymentTerms.Prepaid}>Peşin</option>
@ -775,7 +669,8 @@ const CustomerEdit: React.FC = () => {
</div> </div>
</form> </form>
</div> </div>
); </Container>
}; )
}
export default CustomerEdit; export default CustomerEdit

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from 'react'
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from 'react-router-dom'
import { import {
FaSave, FaSave,
FaTimes, FaTimes,
@ -8,142 +8,130 @@ import {
FaMapMarkerAlt, FaMapMarkerAlt,
FaCreditCard, FaCreditCard,
FaEnvelope, FaEnvelope,
} from "react-icons/fa"; } from 'react-icons/fa'
import LoadingSpinner from "../../../components/common/LoadingSpinner"; import LoadingSpinner from '../../../components/common/LoadingSpinner'
import { import { mockBusinessParties, mockBusinessPartyNew } from '../../../mocks/mockBusinessParties'
mockBusinessParties, import { BusinessParty } from '../../../types/common'
mockBusinessPartyNew, import { Container } from '@/components/shared'
} from "../../../mocks/mockBusinessParties";
import { BusinessParty } from "../../../types/common";
interface ValidationErrors { interface ValidationErrors {
[key: string]: string; [key: string]: string
} }
const CustomerForm: React.FC = () => { const CustomerForm: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>()
const isEdit = Boolean(id); const isEdit = Boolean(id)
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false)
const [errors, setErrors] = useState<ValidationErrors>({}); const [errors, setErrors] = useState<ValidationErrors>({})
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew); const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew)
const loadFormData = useCallback(async () => { const loadFormData = useCallback(async () => {
setLoading(true); setLoading(true)
try { try {
if (isEdit && id) { if (isEdit && id) {
// Simulate API call // Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000))
// Mock customer data // Mock customer data
const mockCustomer = mockBusinessParties.find( const mockCustomer = mockBusinessParties.find((cust) => cust.id === id)!
(cust) => cust.id === id setFormData(mockCustomer)
)!;
setFormData(mockCustomer);
} }
} catch (error) { } catch (error) {
console.error("Error loading form data:", error); console.error('Error loading form data:', error)
} finally { } finally {
setLoading(false); setLoading(false)
} }
}, [isEdit, id]); }, [isEdit, id])
useEffect(() => { useEffect(() => {
loadFormData(); loadFormData()
}, [loadFormData]); }, [loadFormData])
const validateForm = (): boolean => { const validateForm = (): boolean => {
const newErrors: ValidationErrors = {}; const newErrors: ValidationErrors = {}
if (!formData.code.trim()) { if (!formData.code.trim()) {
newErrors.code = "Müşteri kodu zorunludur"; newErrors.code = 'Müşteri kodu zorunludur'
} }
if (!formData.name.trim()) { if (!formData.name.trim()) {
newErrors.name = "Şirket adı zorunludur"; newErrors.name = 'Şirket adı zorunludur'
} }
if (formData.creditLimit < 0) { if (formData.creditLimit < 0) {
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz"; newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz"
} }
setErrors(newErrors); setErrors(newErrors)
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0
}; }
const handleInputChange = ( const handleInputChange = (field: keyof BusinessParty, value: string | number | boolean) => {
field: keyof BusinessParty,
value: string | number | boolean
) => {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[field]: value, [field]: value,
})); }))
// Clear error when user starts typing // Clear error when user starts typing
if (errors[field]) { if (errors[field]) {
setErrors((prev) => ({ setErrors((prev) => ({
...prev, ...prev,
[field]: "", [field]: '',
})); }))
}
} }
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault()
if (!validateForm()) { if (!validateForm()) {
return; return
} }
setSaving(true); setSaving(true)
try { try {
// Simulate API call // Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000))
console.log("Customer data:", { console.log('Customer data:', {
...formData, ...formData,
id: isEdit ? id : undefined, id: isEdit ? id : undefined,
}); })
// Show success message // Show success message
alert( alert(isEdit ? 'Müşteri başarıyla güncellendi!' : 'Müşteri başarıyla oluşturuldu!')
isEdit
? "Müşteri başarıyla güncellendi!"
: "Müşteri başarıyla oluşturuldu!"
);
// Navigate back to list // Navigate back to list
navigate("/admin/crm"); navigate('/admin/crm')
} catch (error) { } catch (error) {
console.error("Error saving customer:", error); console.error('Error saving customer:', error)
alert("Bir hata oluştu. Lütfen tekrar deneyin."); alert('Bir hata oluştu. Lütfen tekrar deneyin.')
} finally { } finally {
setSaving(false); setSaving(false)
}
} }
};
const handleCancel = () => { const handleCancel = () => {
navigate("/admin/crm"); navigate('/admin/crm')
}; }
if (loading) { if (loading) {
return <LoadingSpinner />; return <LoadingSpinner />
} }
return ( return (
<Container>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">
{isEdit ? "Müşteri Düzenle" : "Yeni Müşteri"} {isEdit ? 'Müşteri Düzenle' : 'Yeni Müşteri'}
</h2> </h2>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{isEdit {isEdit ? 'Mevcut müşteri bilgilerini güncelleyin' : 'Yeni müşteri bilgilerini girin'}
? "Mevcut müşteri bilgilerini güncelleyin"
: "Yeni müşteri bilgilerini girin"}
</p> </p>
</div> </div>
</div> </div>
@ -168,18 +156,16 @@ const CustomerForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.code} value={formData.code}
onChange={(e) => handleInputChange("code", e.target.value)} onChange={(e) => handleInputChange('code', e.target.value)}
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.code errors.code
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="Örn: MST001" placeholder="Örn: MST001"
/> />
{errors.customerCode && ( {errors.customerCode && (
<p className="mt-1 text-sm text-red-600"> <p className="mt-1 text-sm text-red-600">{errors.customerCode}</p>
{errors.customerCode}
</p>
)} )}
</div> </div>
@ -190,10 +176,7 @@ const CustomerForm: React.FC = () => {
<select <select
value={formData.customerType} value={formData.customerType}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('customerType', e.target.value as 'INDIVIDUAL' | 'COMPANY')
"customerType",
e.target.value as "INDIVIDUAL" | "COMPANY"
)
} }
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
@ -211,30 +194,24 @@ const CustomerForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.name} value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)} onChange={(e) => handleInputChange('name', e.target.value)}
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.name errors.name
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="Şirket adını girin" placeholder="Şirket adını girin"
/> />
{errors.companyName && ( {errors.companyName && (
<p className="mt-1 text-sm text-red-600"> <p className="mt-1 text-sm text-red-600">{errors.companyName}</p>
{errors.companyName}
</p>
)} )}
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Sektör</label>
Sektör
</label>
<select <select
value={formData.industry} value={formData.industry}
onChange={(e) => onChange={(e) => handleInputChange('industry', e.target.value)}
handleInputChange("industry", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="">Sektör seçin</option> <option value="">Sektör seçin</option>
@ -251,13 +228,11 @@ const CustomerForm: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Website</label>
Website
</label>
<input <input
type="url" type="url"
value={formData.website} value={formData.website}
onChange={(e) => handleInputChange("website", e.target.value)} onChange={(e) => handleInputChange('website', e.target.value)}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
placeholder="https://www.ornek.com" placeholder="https://www.ornek.com"
/> />
@ -277,43 +252,35 @@ const CustomerForm: React.FC = () => {
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Email *</label>
Email *
</label>
<input <input
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)} onChange={(e) => handleInputChange('email', e.target.value)}
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.email errors.email
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="email@ornek.com" placeholder="email@ornek.com"
/> />
{errors.email && ( {errors.email && <p className="mt-1 text-sm text-red-600">{errors.email}</p>}
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Telefon *</label>
Telefon *
</label>
<input <input
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)} onChange={(e) => handleInputChange('phone', e.target.value)}
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.phone errors.phone
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="+90 212 555 0123" placeholder="+90 212 555 0123"
/> />
{errors.phone && ( {errors.phone && <p className="mt-1 text-sm text-red-600">{errors.phone}</p>}
<p className="mt-1 text-sm text-red-600">{errors.phone}</p>
)}
</div> </div>
</div> </div>
</div> </div>
@ -347,9 +314,7 @@ const CustomerForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.taxNumber} value={formData.taxNumber}
onChange={(e) => onChange={(e) => handleInputChange('taxNumber', e.target.value)}
handleInputChange("taxNumber", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
placeholder="1234567890" placeholder="1234567890"
/> />
@ -362,9 +327,7 @@ const CustomerForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.registrationNumber} value={formData.registrationNumber}
onChange={(e) => onChange={(e) => handleInputChange('registrationNumber', e.target.value)}
handleInputChange("registrationNumber", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
placeholder="98765" placeholder="98765"
/> />
@ -382,22 +345,17 @@ const CustomerForm: React.FC = () => {
step="0.01" step="0.01"
value={formData.creditLimit} value={formData.creditLimit}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('creditLimit', parseFloat(e.target.value) || 0)
"creditLimit",
parseFloat(e.target.value) || 0
)
} }
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.creditLimit errors.creditLimit
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="0.00" placeholder="0.00"
/> />
{errors.creditLimit && ( {errors.creditLimit && (
<p className="mt-1 text-sm text-red-600"> <p className="mt-1 text-sm text-red-600">{errors.creditLimit}</p>
{errors.creditLimit}
</p>
)} )}
</div> </div>
@ -407,9 +365,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.paymentTerms} value={formData.paymentTerms}
onChange={(e) => onChange={(e) => handleInputChange('paymentTerms', e.target.value)}
handleInputChange("paymentTerms", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="CASH">Peşin</option> <option value="CASH">Peşin</option>
@ -426,9 +382,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.currency} value={formData.currency}
onChange={(e) => onChange={(e) => handleInputChange('currency', e.target.value)}
handleInputChange("currency", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="TRY">Türk Lirası (TRY)</option> <option value="TRY">Türk Lirası (TRY)</option>
@ -457,7 +411,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.status} value={formData.status}
onChange={(e) => handleInputChange("status", e.target.value)} onChange={(e) => handleInputChange('status', e.target.value)}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="ACTIVE">Aktif</option> <option value="ACTIVE">Aktif</option>
@ -473,9 +427,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.customerSegment} value={formData.customerSegment}
onChange={(e) => onChange={(e) => handleInputChange('customerSegment', e.target.value)}
handleInputChange("customerSegment", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="STANDARD">Standart</option> <option value="STANDARD">Standart</option>
@ -491,9 +443,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.assignedSalesRep} value={formData.assignedSalesRep}
onChange={(e) => onChange={(e) => handleInputChange('assignedSalesRep', e.target.value)}
handleInputChange("assignedSalesRep", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="">Temsilci seçin</option> <option value="">Temsilci seçin</option>
@ -509,15 +459,10 @@ const CustomerForm: React.FC = () => {
type="checkbox" type="checkbox"
id="isActive" id="isActive"
checked={formData.isActive} checked={formData.isActive}
onChange={(e) => onChange={(e) => handleInputChange('isActive', e.target.checked)}
handleInputChange("isActive", e.target.checked)
}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/> />
<label <label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
htmlFor="isActive"
className="ml-2 block text-sm text-gray-900"
>
Aktif Aktif
</label> </label>
</div> </div>
@ -547,14 +492,15 @@ const CustomerForm: React.FC = () => {
) : ( ) : (
<> <>
<FaSave className="w-4 h-4 mr-2" /> <FaSave className="w-4 h-4 mr-2" />
{isEdit ? "Güncelle" : "Kaydet"} {isEdit ? 'Güncelle' : 'Kaydet'}
</> </>
)} )}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
); </Container>
}; )
}
export default CustomerForm; export default CustomerForm

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from 'react'
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from 'react-router-dom'
import { import {
FaSave, FaSave,
FaTimes, FaTimes,
@ -8,135 +8,128 @@ import {
FaMapMarkerAlt, FaMapMarkerAlt,
FaCreditCard, FaCreditCard,
FaEnvelope, FaEnvelope,
} from "react-icons/fa"; } from 'react-icons/fa'
import LoadingSpinner from "../../../components/common/LoadingSpinner"; import LoadingSpinner from '../../../components/common/LoadingSpinner'
import { BusinessParty } from "../../../types/common"; import { BusinessParty } from '../../../types/common'
import { mockBusinessPartyNew } from "../../../mocks/mockBusinessParties"; import { mockBusinessPartyNew } from '../../../mocks/mockBusinessParties'
import { Container } from '@/components/shared'
interface ValidationErrors { interface ValidationErrors {
[key: string]: string; [key: string]: string
} }
const CustomerForm: React.FC = () => { const CustomerForm: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>()
const isEdit = Boolean(id); const isEdit = Boolean(id)
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false)
const [errors, setErrors] = useState<ValidationErrors>({}); const [errors, setErrors] = useState<ValidationErrors>({})
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew); const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew)
const loadFormData = useCallback(async () => { const loadFormData = useCallback(async () => {
setLoading(true); setLoading(true)
try { try {
if (isEdit && id) { if (isEdit && id) {
// Simulate API call // Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000))
setFormData(mockBusinessPartyNew); setFormData(mockBusinessPartyNew)
} }
} catch (error) { } catch (error) {
console.error("Error loading form data:", error); console.error('Error loading form data:', error)
} finally { } finally {
setLoading(false); setLoading(false)
} }
}, [isEdit, id]); }, [isEdit, id])
useEffect(() => { useEffect(() => {
loadFormData(); loadFormData()
}, [loadFormData]); }, [loadFormData])
const validateForm = (): boolean => { const validateForm = (): boolean => {
const newErrors: ValidationErrors = {}; const newErrors: ValidationErrors = {}
if (!formData.code.trim()) { if (!formData.code.trim()) {
newErrors.code = "Müşteri kodu zorunludur"; newErrors.code = 'Müşteri kodu zorunludur'
} }
if (!formData.name.trim()) { if (!formData.name.trim()) {
newErrors.name = "Şirket adı zorunludur"; newErrors.name = 'Şirket adı zorunludur'
} }
if (formData.creditLimit < 0) { if (formData.creditLimit < 0) {
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz"; newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz"
} }
setErrors(newErrors); setErrors(newErrors)
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0
}; }
const handleInputChange = ( const handleInputChange = (field: keyof BusinessParty, value: string | number | boolean) => {
field: keyof BusinessParty,
value: string | number | boolean
) => {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[field]: value, [field]: value,
})); }))
// Clear error when user starts typing // Clear error when user starts typing
if (errors[field]) { if (errors[field]) {
setErrors((prev) => ({ setErrors((prev) => ({
...prev, ...prev,
[field]: "", [field]: '',
})); }))
}
} }
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault()
if (!validateForm()) { if (!validateForm()) {
return; return
} }
setSaving(true); setSaving(true)
try { try {
// Simulate API call // Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000))
console.log("Customer data:", { console.log('Customer data:', {
...formData, ...formData,
id: isEdit ? id : undefined, id: isEdit ? id : undefined,
}); })
// Show success message // Show success message
alert( alert(isEdit ? 'Müşteri başarıyla güncellendi!' : 'Müşteri başarıyla oluşturuldu!')
isEdit
? "Müşteri başarıyla güncellendi!"
: "Müşteri başarıyla oluşturuldu!"
);
// Navigate back to list // Navigate back to list
navigate("/admin/crm"); navigate('/admin/crm')
} catch (error) { } catch (error) {
console.error("Error saving customer:", error); console.error('Error saving customer:', error)
alert("Bir hata oluştu. Lütfen tekrar deneyin."); alert('Bir hata oluştu. Lütfen tekrar deneyin.')
} finally { } finally {
setSaving(false); setSaving(false)
}
} }
};
const handleCancel = () => { const handleCancel = () => {
navigate("/admin/crm"); navigate('/admin/crm')
}; }
if (loading) { if (loading) {
return <LoadingSpinner />; return <LoadingSpinner />
} }
return ( return (
<Container>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">
{isEdit ? "Müşteri Düzenle" : "Yeni Müşteri"} {isEdit ? 'Müşteri Düzenle' : 'Yeni Müşteri'}
</h2> </h2>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{isEdit {isEdit ? 'Mevcut müşteri bilgilerini güncelleyin' : 'Yeni müşteri bilgilerini girin'}
? "Mevcut müşteri bilgilerini güncelleyin"
: "Yeni müşteri bilgilerini girin"}
</p> </p>
</div> </div>
</div> </div>
@ -161,18 +154,16 @@ const CustomerForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.code} value={formData.code}
onChange={(e) => handleInputChange("code", e.target.value)} onChange={(e) => handleInputChange('code', e.target.value)}
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.code errors.code
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="Örn: MST001" placeholder="Örn: MST001"
/> />
{errors.customerCode && ( {errors.customerCode && (
<p className="mt-1 text-sm text-red-600"> <p className="mt-1 text-sm text-red-600">{errors.customerCode}</p>
{errors.customerCode}
</p>
)} )}
</div> </div>
@ -183,10 +174,7 @@ const CustomerForm: React.FC = () => {
<select <select
value={formData.customerType} value={formData.customerType}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('customerType', e.target.value as 'INDIVIDUAL' | 'COMPANY')
"customerType",
e.target.value as "INDIVIDUAL" | "COMPANY"
)
} }
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
@ -204,30 +192,24 @@ const CustomerForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.name} value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)} onChange={(e) => handleInputChange('name', e.target.value)}
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.name errors.name
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="Şirket adını girin" placeholder="Şirket adını girin"
/> />
{errors.companyName && ( {errors.companyName && (
<p className="mt-1 text-sm text-red-600"> <p className="mt-1 text-sm text-red-600">{errors.companyName}</p>
{errors.companyName}
</p>
)} )}
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Sektör</label>
Sektör
</label>
<select <select
value={formData.industry} value={formData.industry}
onChange={(e) => onChange={(e) => handleInputChange('industry', e.target.value)}
handleInputChange("industry", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="">Sektör seçin</option> <option value="">Sektör seçin</option>
@ -244,13 +226,11 @@ const CustomerForm: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Website</label>
Website
</label>
<input <input
type="url" type="url"
value={formData.website} value={formData.website}
onChange={(e) => handleInputChange("website", e.target.value)} onChange={(e) => handleInputChange('website', e.target.value)}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
placeholder="https://www.ornek.com" placeholder="https://www.ornek.com"
/> />
@ -270,43 +250,35 @@ const CustomerForm: React.FC = () => {
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Email *</label>
Email *
</label>
<input <input
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)} onChange={(e) => handleInputChange('email', e.target.value)}
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.email errors.email
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="email@ornek.com" placeholder="email@ornek.com"
/> />
{errors.email && ( {errors.email && <p className="mt-1 text-sm text-red-600">{errors.email}</p>}
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Telefon *</label>
Telefon *
</label>
<input <input
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)} onChange={(e) => handleInputChange('phone', e.target.value)}
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.phone errors.phone
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="+90 212 555 0123" placeholder="+90 212 555 0123"
/> />
{errors.phone && ( {errors.phone && <p className="mt-1 text-sm text-red-600">{errors.phone}</p>}
<p className="mt-1 text-sm text-red-600">{errors.phone}</p>
)}
</div> </div>
</div> </div>
</div> </div>
@ -340,9 +312,7 @@ const CustomerForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.taxNumber} value={formData.taxNumber}
onChange={(e) => onChange={(e) => handleInputChange('taxNumber', e.target.value)}
handleInputChange("taxNumber", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
placeholder="1234567890" placeholder="1234567890"
/> />
@ -355,9 +325,7 @@ const CustomerForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.registrationNumber} value={formData.registrationNumber}
onChange={(e) => onChange={(e) => handleInputChange('registrationNumber', e.target.value)}
handleInputChange("registrationNumber", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
placeholder="98765" placeholder="98765"
/> />
@ -375,22 +343,17 @@ const CustomerForm: React.FC = () => {
step="0.01" step="0.01"
value={formData.creditLimit} value={formData.creditLimit}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('creditLimit', parseFloat(e.target.value) || 0)
"creditLimit",
parseFloat(e.target.value) || 0
)
} }
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${ className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
errors.creditLimit errors.creditLimit
? "border-red-300 focus:border-red-500 focus:ring-red-500" ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500" : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`} }`}
placeholder="0.00" placeholder="0.00"
/> />
{errors.creditLimit && ( {errors.creditLimit && (
<p className="mt-1 text-sm text-red-600"> <p className="mt-1 text-sm text-red-600">{errors.creditLimit}</p>
{errors.creditLimit}
</p>
)} )}
</div> </div>
@ -400,9 +363,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.paymentTerms} value={formData.paymentTerms}
onChange={(e) => onChange={(e) => handleInputChange('paymentTerms', e.target.value)}
handleInputChange("paymentTerms", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="CASH">Peşin</option> <option value="CASH">Peşin</option>
@ -419,9 +380,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.currency} value={formData.currency}
onChange={(e) => onChange={(e) => handleInputChange('currency', e.target.value)}
handleInputChange("currency", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="TRY">Türk Lirası (TRY)</option> <option value="TRY">Türk Lirası (TRY)</option>
@ -450,7 +409,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.status} value={formData.status}
onChange={(e) => handleInputChange("status", e.target.value)} onChange={(e) => handleInputChange('status', e.target.value)}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="ACTIVE">Aktif</option> <option value="ACTIVE">Aktif</option>
@ -466,9 +425,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.customerSegment} value={formData.customerSegment}
onChange={(e) => onChange={(e) => handleInputChange('customerSegment', e.target.value)}
handleInputChange("customerSegment", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="STANDARD">Standart</option> <option value="STANDARD">Standart</option>
@ -484,9 +441,7 @@ const CustomerForm: React.FC = () => {
</label> </label>
<select <select
value={formData.assignedSalesRep} value={formData.assignedSalesRep}
onChange={(e) => onChange={(e) => handleInputChange('assignedSalesRep', e.target.value)}
handleInputChange("assignedSalesRep", e.target.value)
}
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500" className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
> >
<option value="">Temsilci seçin</option> <option value="">Temsilci seçin</option>
@ -502,15 +457,10 @@ const CustomerForm: React.FC = () => {
type="checkbox" type="checkbox"
id="isActive" id="isActive"
checked={formData.isActive} checked={formData.isActive}
onChange={(e) => onChange={(e) => handleInputChange('isActive', e.target.checked)}
handleInputChange("isActive", e.target.checked)
}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/> />
<label <label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
htmlFor="isActive"
className="ml-2 block text-sm text-gray-900"
>
Aktif Aktif
</label> </label>
</div> </div>
@ -540,14 +490,15 @@ const CustomerForm: React.FC = () => {
) : ( ) : (
<> <>
<FaSave className="w-4 h-4 mr-2" /> <FaSave className="w-4 h-4 mr-2" />
{isEdit ? "Güncelle" : "Kaydet"} {isEdit ? 'Güncelle' : 'Kaydet'}
</> </>
)} )}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
); </Container>
}; )
}
export default CustomerForm; export default CustomerForm

View file

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom'
import { useQuery } from "@tanstack/react-query"; import { useQuery } from '@tanstack/react-query'
import { import {
FaUserCheck, FaUserCheck,
FaPlus, FaPlus,
@ -16,54 +16,50 @@ import {
FaExclamationTriangle, FaExclamationTriangle,
FaStar, FaStar,
FaCalendar, FaCalendar,
} from "react-icons/fa"; } from 'react-icons/fa'
import classNames from "classnames"; import classNames from 'classnames'
import { CustomerSegmentEnum } from "../../../types/crm"; import { CustomerSegmentEnum } from '../../../types/crm'
import dayjs from "dayjs"; import dayjs from 'dayjs'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { BusinessPartyStatusEnum, PartyType } from "../../../types/common"; import { BusinessPartyStatusEnum, PartyType } from '../../../types/common'
import Widget from "../../../components/common/Widget"; import Widget from '../../../components/common/Widget'
import { import {
getBusinessPartyStatusColor, getBusinessPartyStatusColor,
getBusinessPartyStatusName, getBusinessPartyStatusName,
getCustomerSegmentColor, getCustomerSegmentColor,
getCustomerSegmentName, getCustomerSegmentName,
} from "../../../utils/erp"; } from '../../../utils/erp'
const CustomerList: React.FC = () => { const CustomerList: React.FC = () => {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState('')
const [filterStatus, setFilterStatus] = useState("all"); const [filterStatus, setFilterStatus] = useState('all')
const [filterSegment, setFilterSegment] = useState("all"); const [filterSegment, setFilterSegment] = useState('all')
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false)
const { const {
data: customers, data: customers,
isLoading, isLoading,
error, error,
} = useQuery({ } = useQuery({
queryKey: ["customers", searchTerm, filterStatus, filterSegment], queryKey: ['customers', searchTerm, filterStatus, filterSegment],
queryFn: async () => { queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500))
const mockCustomers = mockBusinessParties.filter( const mockCustomers = mockBusinessParties.filter(
(customer) => customer.partyType === PartyType.Customer (customer) => customer.partyType === PartyType.Customer,
); )
return mockCustomers.filter((customer) => { return mockCustomers.filter((customer) => {
const matchesSearch = const matchesSearch =
customer.code.toLowerCase().includes(searchTerm.toLowerCase()) || customer.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.name.toLowerCase().includes(searchTerm.toLowerCase()) || customer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.primaryContact?.fullName customer.primaryContact?.fullName.toLowerCase().includes(searchTerm.toLowerCase())
.toLowerCase() const matchesStatus = filterStatus === 'all' || customer.status === filterStatus
.includes(searchTerm.toLowerCase()); const matchesSegment = filterSegment === 'all' || customer.customerSegment === filterSegment
const matchesStatus = return matchesSearch && matchesStatus && matchesSegment
filterStatus === "all" || customer.status === filterStatus; })
const matchesSegment =
filterSegment === "all" || customer.customerSegment === filterSegment;
return matchesSearch && matchesStatus && matchesSegment;
});
}, },
}); })
if (isLoading) { if (isLoading) {
return ( return (
@ -71,7 +67,7 @@ const CustomerList: React.FC = () => {
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Müşteriler yükleniyor...</span> <span className="ml-3 text-gray-600">Müşteriler yükleniyor...</span>
</div> </div>
); )
} }
if (error) { if (error) {
@ -79,12 +75,10 @@ const CustomerList: React.FC = () => {
<div className="bg-red-50 border border-red-200 rounded-lg p-4"> <div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center"> <div className="flex items-center">
<FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" /> <FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" />
<span className="text-red-800"> <span className="text-red-800">Müşteriler yüklenirken hata oluştu.</span>
Müşteriler yüklenirken hata oluştu.
</span>
</div> </div>
</div> </div>
); )
} }
return ( return (
@ -109,10 +103,10 @@ const CustomerList: React.FC = () => {
<button <button
onClick={() => setShowFilters(!showFilters)} onClick={() => setShowFilters(!showFilters)}
className={classNames( className={classNames(
"flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors", 'flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors',
showFilters showFilters
? "border-blue-500 bg-blue-50 text-blue-700" ? 'border-blue-500 bg-blue-50 text-blue-700'
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50" : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
)} )}
> >
<FaFilter size={14} className="mr-2" /> <FaFilter size={14} className="mr-2" />
@ -122,7 +116,7 @@ const CustomerList: React.FC = () => {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
onClick={() => alert("Dışa aktarma özelliği yakında eklenecek")} onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
> >
<FaDownload size={14} className="mr-2" /> <FaDownload size={14} className="mr-2" />
@ -144,18 +138,14 @@ const CustomerList: React.FC = () => {
<div className="bg-white border border-gray-200 rounded-lg p-3"> <div className="bg-white border border-gray-200 rounded-lg p-3">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
Durum
</label>
<select <select
value={filterStatus} value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)} onChange={(e) => setFilterStatus(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="all">Tümü</option> <option value="all">Tümü</option>
<option value={BusinessPartyStatusEnum.Prospect}> <option value={BusinessPartyStatusEnum.Prospect}>Potansiyel</option>
Potansiyel
</option>
<option value={BusinessPartyStatusEnum.Active}>Aktif</option> <option value={BusinessPartyStatusEnum.Active}>Aktif</option>
<option value={BusinessPartyStatusEnum.Inactive}>Pasif</option> <option value={BusinessPartyStatusEnum.Inactive}>Pasif</option>
<option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option> <option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option>
@ -163,9 +153,7 @@ const CustomerList: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Segment</label>
Segment
</label>
<select <select
value={filterSegment} value={filterSegment}
onChange={(e) => setFilterSegment(e.target.value)} onChange={(e) => setFilterSegment(e.target.value)}
@ -182,9 +170,9 @@ const CustomerList: React.FC = () => {
<div className="flex items-end"> <div className="flex items-end">
<button <button
onClick={() => { onClick={() => {
setFilterStatus("all"); setFilterStatus('all')
setFilterSegment("all"); setFilterSegment('all')
setSearchTerm(""); setSearchTerm('')
}} }}
className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
> >
@ -206,11 +194,7 @@ const CustomerList: React.FC = () => {
<Widget <Widget
title="Aktif Müşteri" title="Aktif Müşteri"
value={ value={customers?.filter((c) => c.status === BusinessPartyStatusEnum.Active).length || 0}
customers?.filter(
(c) => c.status === BusinessPartyStatusEnum.Active
).length || 0
}
color="green" color="green"
icon="FaUserCheck" icon="FaUserCheck"
/> />
@ -218,9 +202,7 @@ const CustomerList: React.FC = () => {
<Widget <Widget
title="Toplam Ciro" title="Toplam Ciro"
value={`${ value={`${
customers customers?.reduce((acc, c) => acc + (c.totalRevenue ?? 0), 0).toLocaleString() || 0
?.reduce((acc, c) => acc + (c.totalRevenue ?? 0), 0)
.toLocaleString() || 0
}`} }`}
color="purple" color="purple"
icon="FaDollarSign" icon="FaDollarSign"
@ -231,10 +213,8 @@ const CustomerList: React.FC = () => {
value={`${ value={`${
customers?.length customers?.length
? Math.round( ? Math.round(
customers.reduce( customers.reduce((acc, c) => acc + (c.averageOrderValue ?? 0), 0) /
(acc, c) => acc + (c.averageOrderValue ?? 0), customers.length,
0
) / customers.length
).toLocaleString() ).toLocaleString()
: 0 : 0
}`} }`}
@ -279,10 +259,7 @@ const CustomerList: React.FC = () => {
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{customers?.map((customer) => ( {customers?.map((customer) => (
<tr <tr key={customer.id} className="hover:bg-gray-50 transition-colors">
key={customer.id}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-shrink-0 h-8 w-8"> <div className="flex-shrink-0 h-8 w-8">
@ -291,16 +268,10 @@ const CustomerList: React.FC = () => {
</div> </div>
</div> </div>
<div className="ml-4"> <div className="ml-4">
<div className="text-xs font-medium text-gray-900"> <div className="text-xs font-medium text-gray-900">{customer.code}</div>
{customer.code} <div className="text-sm text-gray-500">{customer.name}</div>
</div>
<div className="text-sm text-gray-500">
{customer.name}
</div>
{customer.industry && ( {customer.industry && (
<div className="text-xs text-gray-400 mt-1"> <div className="text-xs text-gray-400 mt-1">{customer.industry}</div>
{customer.industry}
</div>
)} )}
</div> </div>
</div> </div>
@ -332,26 +303,26 @@ const CustomerList: React.FC = () => {
<div className="space-y-2"> <div className="space-y-2">
<div <div
className={classNames( className={classNames(
"text-sm font-medium", 'text-sm font-medium',
getCustomerSegmentColor( getCustomerSegmentColor(
customer.customerSegment || CustomerSegmentEnum.SMB customer.customerSegment || CustomerSegmentEnum.SMB,
) ),
)} )}
> >
{getCustomerSegmentName( {getCustomerSegmentName(
customer.customerSegment || CustomerSegmentEnum.SMB customer.customerSegment || CustomerSegmentEnum.SMB,
)} )}
</div> </div>
<span <span
className={classNames( className={classNames(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium", 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
getBusinessPartyStatusColor( getBusinessPartyStatusColor(
customer.status ?? BusinessPartyStatusEnum.Prospect customer.status ?? BusinessPartyStatusEnum.Prospect,
) ),
)} )}
> >
{getBusinessPartyStatusName( {getBusinessPartyStatusName(
customer.status ?? BusinessPartyStatusEnum.Prospect customer.status ?? BusinessPartyStatusEnum.Prospect,
)} )}
</span> </span>
</div> </div>
@ -363,15 +334,12 @@ const CustomerList: React.FC = () => {
{(customer.totalRevenue ?? 0).toLocaleString()} {(customer.totalRevenue ?? 0).toLocaleString()}
</div> </div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
Ort. Sipariş: Ort. Sipariş: {(customer.averageOrderValue ?? 0).toLocaleString()}
{(customer.averageOrderValue ?? 0).toLocaleString()}
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<FaStar size={14} className="text-yellow-500 mr-1" /> <FaStar size={14} className="text-yellow-500 mr-1" />
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
LTV: LTV: {((customer.lifetimeValue ?? 0) / 1000000).toFixed(1)}M
{((customer.lifetimeValue ?? 0) / 1000000).toFixed(1)}
M
</span> </span>
</div> </div>
</div> </div>
@ -381,9 +349,7 @@ const CustomerList: React.FC = () => {
<div className="text-sm font-medium text-gray-900"> <div className="text-sm font-medium text-gray-900">
{customer.creditLimit.toLocaleString()} {customer.creditLimit.toLocaleString()}
</div> </div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">{customer.paymentTerms}</div>
{customer.paymentTerms}
</div>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
@ -391,7 +357,7 @@ const CustomerList: React.FC = () => {
{customer.lastOrderDate && ( {customer.lastOrderDate && (
<div className="flex items-center text-sm text-gray-900"> <div className="flex items-center text-sm text-gray-900">
<FaCalendar size={14} className="mr-1" /> <FaCalendar size={14} className="mr-1" />
{dayjs(customer.lastOrderDate).format("DD.MM.YYYY")} {dayjs(customer.lastOrderDate).format('DD.MM.YYYY')}
</div> </div>
)} )}
<div className="text-sm text-gray-500">Son sipariş</div> <div className="text-sm text-gray-500">Son sipariş</div>
@ -426,12 +392,8 @@ const CustomerList: React.FC = () => {
{(!customers || customers.length === 0) && ( {(!customers || customers.length === 0) && (
<div className="text-center py-12"> <div className="text-center py-12">
<FaUserCheck className="mx-auto h-10 w-10 text-gray-400" /> <FaUserCheck className="mx-auto h-10 w-10 text-gray-400" />
<h3 className="mt-2 text-xs font-medium text-gray-900"> <h3 className="mt-2 text-xs font-medium text-gray-900">Müşteri bulunamadı</h3>
Müşteri bulunamadı <p className="mt-1 text-xs text-gray-500">Yeni müşteri ekleyerek başlayın.</p>
</h3>
<p className="mt-1 text-xs text-gray-500">
Yeni müşteri ekleyerek başlayın.
</p>
<div className="mt-6"> <div className="mt-6">
<Link <Link
to="/admin/crm/customers/new" to="/admin/crm/customers/new"
@ -445,7 +407,7 @@ const CustomerList: React.FC = () => {
)} )}
</div> </div>
</div> </div>
); )
}; }
export default CustomerList; export default CustomerList

View file

@ -1,46 +1,46 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { FaList, FaTh } from "react-icons/fa"; import { FaList, FaTh } from 'react-icons/fa'
import classNames from "classnames"; import classNames from 'classnames'
import CustomerList from "./CustomerList"; import CustomerList from './CustomerList'
import CustomerCards from "./CustomerCards"; import CustomerCards from './CustomerCards'
import { Container } from '@/components/shared'
export type ViewMode = "list" | "cards"; export type ViewMode = 'list' | 'cards'
const CustomerListWithToggle: React.FC = () => { const CustomerListWithToggle: React.FC = () => {
const [viewMode, setViewMode] = useState<ViewMode>("list"); const [viewMode, setViewMode] = useState<ViewMode>('list')
return ( return (
<Container>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
{/* Header with View Toggle */} {/* Header with View Toggle */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div> <div>
<h1 className="text-xl font-bold text-gray-900">Müşteri Listesi</h1> <h1 className="text-xl font-bold text-gray-900">Müşteri Listesi</h1>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">Müşteri listesi ve detay bilgileri</p>
Müşteri listesi ve detay bilgileri
</p>
</div> </div>
{/* View Mode Toggle */} {/* View Mode Toggle */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="flex bg-gray-100 rounded-lg p-1"> <div className="flex bg-gray-100 rounded-lg p-1">
<button <button
onClick={() => setViewMode("list")} onClick={() => setViewMode('list')}
className={classNames( className={classNames(
"flex items-center space-x-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors", 'flex items-center space-x-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors',
viewMode === "list" viewMode === 'list'
? "bg-white text-gray-900 shadow-sm" ? 'bg-white text-gray-900 shadow-sm'
: "text-gray-600 hover:text-gray-900" : 'text-gray-600 hover:text-gray-900',
)} )}
> >
<FaList className="w-4 h-4" /> <FaList className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => setViewMode("cards")} onClick={() => setViewMode('cards')}
className={classNames( className={classNames(
"flex items-center space-x-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors", 'flex items-center space-x-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors',
viewMode === "cards" viewMode === 'cards'
? "bg-white text-gray-900 shadow-sm" ? 'bg-white text-gray-900 shadow-sm'
: "text-gray-600 hover:text-gray-900" : 'text-gray-600 hover:text-gray-900',
)} )}
> >
<FaTh className="w-4 h-4" /> <FaTh className="w-4 h-4" />
@ -51,10 +51,11 @@ const CustomerListWithToggle: React.FC = () => {
{/* Content */} {/* Content */}
<div className="transition-all duration-200"> <div className="transition-all duration-200">
{viewMode === "list" ? <CustomerList /> : <CustomerCards />} {viewMode === 'list' ? <CustomerList /> : <CustomerCards />}
</div> </div>
</div> </div>
); </Container>
}; )
}
export default CustomerListWithToggle; export default CustomerListWithToggle

View file

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { useParams, Link, useNavigate } from "react-router-dom"; import { useParams, Link, useNavigate } from 'react-router-dom'
import { useQuery } from "@tanstack/react-query"; import { useQuery } from '@tanstack/react-query'
import { import {
FaArrowLeft, FaArrowLeft,
FaEdit, FaEdit,
@ -23,36 +23,37 @@ import {
FaUsers, FaUsers,
FaChartBar, FaChartBar,
FaArrowUp, FaArrowUp,
} from "react-icons/fa"; } from 'react-icons/fa'
import classNames from "classnames"; import classNames from 'classnames'
import { CustomerSegmentEnum } from "../../../types/crm"; import { CustomerSegmentEnum } from '../../../types/crm'
import dayjs from "dayjs"; import dayjs from 'dayjs'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { import {
getBusinessPartyStatusColor, getBusinessPartyStatusColor,
getBusinessPartyStatusName, getBusinessPartyStatusName,
getCustomerSegmentName, getCustomerSegmentName,
} from "../../../utils/erp"; } from '../../../utils/erp'
import { BusinessPartyStatusEnum } from "../../../types/common"; import { BusinessPartyStatusEnum } from '../../../types/common'
import { Container } from '@/components/shared'
const CustomerView: React.FC = () => { const CustomerView: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>()
const navigate = useNavigate(); const navigate = useNavigate()
const [activeTab, setActiveTab] = useState("overview"); const [activeTab, setActiveTab] = useState('overview')
const { const {
data: customer, data: customer,
isLoading, isLoading,
error, error,
} = useQuery({ } = useQuery({
queryKey: ["customer", id], queryKey: ['customer', id],
queryFn: async () => { queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300))
const found = mockBusinessParties.find((c) => c.id === id); const found = mockBusinessParties.find((c) => c.id === id)
if (!found) throw new Error("Müşteri bulunamadı"); if (!found) throw new Error('Müşteri bulunamadı')
return found; return found
}, },
}); })
if (isLoading) { if (isLoading) {
return ( return (
@ -85,7 +86,7 @@ const CustomerView: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
); )
} }
if (error || !customer) { if (error || !customer) {
@ -96,18 +97,15 @@ const CustomerView: React.FC = () => {
<div className="flex items-center"> <div className="flex items-center">
<FaExclamationTriangle className="h-8 w-8 text-red-600 mr-3" /> <FaExclamationTriangle className="h-8 w-8 text-red-600 mr-3" />
<div> <div>
<h3 className="text-lg font-medium text-red-800"> <h3 className="text-lg font-medium text-red-800">Müşteri Bulunamadı</h3>
Müşteri Bulunamadı
</h3>
<p className="text-red-600"> <p className="text-red-600">
Aradığınız müşteri mevcut değil veya erişim izniniz Aradığınız müşteri mevcut değil veya erişim izniniz bulunmuyor.
bulunmuyor.
</p> </p>
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={() => navigate("/admin/crm/customers")} onClick={() => navigate('/admin/crm/customers')}
className="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors" className="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
> >
<FaArrowLeft className="w-4 h-4 mr-2" /> <FaArrowLeft className="w-4 h-4 mr-2" />
@ -117,18 +115,19 @@ const CustomerView: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
); )
} }
const tabs = [ const tabs = [
{ id: "overview", label: "Genel Bakış", icon: FaChartLine }, { id: 'overview', label: 'Genel Bakış', icon: FaChartLine },
{ id: "contact", label: "İletişim", icon: FaUser }, { id: 'contact', label: 'İletişim', icon: FaUser },
{ id: "business", label: "İş Bilgileri", icon: FaBuilding }, { id: 'business', label: 'İş Bilgileri', icon: FaBuilding },
{ id: "financial", label: "Finansal", icon: FaDollarSign }, { id: 'financial', label: 'Finansal', icon: FaDollarSign },
{ id: "history", label: "Geçmiş", icon: FaHistory }, { id: 'history', label: 'Geçmiş', icon: FaHistory },
]; ]
return ( return (
<Container>
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<div className="bg-white border-b border-gray-200"> <div className="bg-white border-b border-gray-200">
<div className="mx-auto px-4 py-3"> <div className="mx-auto px-4 py-3">
@ -145,7 +144,7 @@ const CustomerView: React.FC = () => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<button <button
onClick={() => navigate("/admin/crm/customers")} onClick={() => navigate('/admin/crm/customers')}
className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
> >
<FaArrowLeft className="w-5 h-5" /> <FaArrowLeft className="w-5 h-5" />
@ -158,38 +157,32 @@ const CustomerView: React.FC = () => {
<div> <div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<h1 className="text-xl font-bold text-gray-900"> <h1 className="text-xl font-bold text-gray-900">{customer.name}</h1>
{customer.name}
</h1>
<span <span
className={classNames( className={classNames(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border", 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border',
getBusinessPartyStatusColor( getBusinessPartyStatusColor(
customer.status || BusinessPartyStatusEnum.Active customer.status || BusinessPartyStatusEnum.Active,
) ),
)} )}
> >
{getBusinessPartyStatusName( {getBusinessPartyStatusName(
customer.status || BusinessPartyStatusEnum.Active customer.status || BusinessPartyStatusEnum.Active,
)} )}
</span> </span>
</div> </div>
<div className="flex items-center space-x-4 mt-1"> <div className="flex items-center space-x-4 mt-1">
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">{customer.code}</span>
{customer.code}
</span>
<span className="text-sm text-gray-400"></span> <span className="text-sm text-gray-400"></span>
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
{getCustomerSegmentName( {getCustomerSegmentName(
customer.customerSegment || CustomerSegmentEnum.Startup customer.customerSegment || CustomerSegmentEnum.Startup,
)} )}
</span> </span>
{customer.industry && ( {customer.industry && (
<> <>
<span className="text-sm text-gray-400"></span> <span className="text-sm text-gray-400"></span>
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">{customer.industry}</span>
{customer.industry}
</span>
</> </>
)} )}
</div> </div>
@ -213,9 +206,7 @@ const CustomerView: React.FC = () => {
<div className="bg-gray-50 rounded-lg p-3"> <div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-600"> <p className="text-sm font-medium text-gray-600">Toplam Ciro</p>
Toplam Ciro
</p>
<p className="text-lg font-bold text-gray-900"> <p className="text-lg font-bold text-gray-900">
{customer.totalRevenue?.toLocaleString()} {customer.totalRevenue?.toLocaleString()}
</p> </p>
@ -227,9 +218,7 @@ const CustomerView: React.FC = () => {
<div className="bg-gray-50 rounded-lg p-3"> <div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-600"> <p className="text-sm font-medium text-gray-600">Ort. Sipariş</p>
Ort. Sipariş
</p>
<p className="text-lg font-bold text-gray-900"> <p className="text-lg font-bold text-gray-900">
{customer.averageOrderValue?.toLocaleString()} {customer.averageOrderValue?.toLocaleString()}
</p> </p>
@ -241,9 +230,7 @@ const CustomerView: React.FC = () => {
<div className="bg-gray-50 rounded-lg p-3"> <div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-600"> <p className="text-sm font-medium text-gray-600">Yaşam Boyu Değer</p>
Yaşam Boyu Değer
</p>
<p className="text-lg font-bold text-gray-900"> <p className="text-lg font-bold text-gray-900">
{((customer.lifetimeValue ?? 0) / 1000000).toFixed(1)}M {((customer.lifetimeValue ?? 0) / 1000000).toFixed(1)}M
</p> </p>
@ -255,9 +242,7 @@ const CustomerView: React.FC = () => {
<div className="bg-gray-50 rounded-lg p-3"> <div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-600"> <p className="text-sm font-medium text-gray-600">Kredi Limiti</p>
Kredi Limiti
</p>
<p className="text-lg font-bold text-gray-900"> <p className="text-lg font-bold text-gray-900">
{customer.creditLimit.toLocaleString()} {customer.creditLimit.toLocaleString()}
</p> </p>
@ -278,10 +263,10 @@ const CustomerView: React.FC = () => {
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={classNames( className={classNames(
"flex items-center space-x-2 py-3 px-1 border-b-2 font-medium text-sm transition-colors", 'flex items-center space-x-2 py-3 px-1 border-b-2 font-medium text-sm transition-colors',
activeTab === tab.id activeTab === tab.id
? "border-blue-500 text-blue-600" ? 'border-blue-500 text-blue-600'
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
)} )}
> >
<tab.icon className="w-4 h-4" /> <tab.icon className="w-4 h-4" />
@ -294,7 +279,7 @@ const CustomerView: React.FC = () => {
{/* Tab Content */} {/* Tab Content */}
<div className="mx-auto py-4"> <div className="mx-auto py-4">
{activeTab === "overview" && ( {activeTab === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Main Overview */} {/* Main Overview */}
<div className="lg:col-span-2 space-y-4"> <div className="lg:col-span-2 space-y-4">
@ -306,9 +291,7 @@ const CustomerView: React.FC = () => {
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<h4 className="font-medium text-gray-900 mb-3"> <h4 className="font-medium text-gray-900 mb-3">Temel Bilgiler</h4>
Temel Bilgiler
</h4>
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Müşteri Kodu:</span> <span className="text-gray-600">Müşteri Kodu:</span>
@ -321,15 +304,14 @@ const CustomerView: React.FC = () => {
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Sektör:</span> <span className="text-gray-600">Sektör:</span>
<span className="font-medium"> <span className="font-medium">
{customer.industry || "Belirtilmemiş"} {customer.industry || 'Belirtilmemiş'}
</span> </span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Segment:</span> <span className="text-gray-600">Segment:</span>
<span className="font-medium"> <span className="font-medium">
{getCustomerSegmentName( {getCustomerSegmentName(
customer.customerSegment || customer.customerSegment || CustomerSegmentEnum.Startup,
CustomerSegmentEnum.Startup
)} )}
</span> </span>
</div> </div>
@ -337,23 +319,19 @@ const CustomerView: React.FC = () => {
</div> </div>
<div> <div>
<h4 className="font-medium text-gray-900 mb-3"> <h4 className="font-medium text-gray-900 mb-3">Performans</h4>
Performans
</h4>
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Son Sipariş:</span> <span className="text-gray-600">Son Sipariş:</span>
<span className="font-medium"> <span className="font-medium">
{customer.lastOrderDate {customer.lastOrderDate
? dayjs(customer.lastOrderDate).format("DD.MM.YYYY") ? dayjs(customer.lastOrderDate).format('DD.MM.YYYY')
: "Henüz sipariş yok"} : 'Henüz sipariş yok'}
</span> </span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Ödeme Koşulları:</span> <span className="text-gray-600">Ödeme Koşulları:</span>
<span className="font-medium"> <span className="font-medium">{customer.paymentTerms}</span>
{customer.paymentTerms}
</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Para Birimi:</span> <span className="text-gray-600">Para Birimi:</span>
@ -362,7 +340,7 @@ const CustomerView: React.FC = () => {
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Kayıt Tarihi:</span> <span className="text-gray-600">Kayıt Tarihi:</span>
<span className="font-medium"> <span className="font-medium">
{dayjs(customer.creationTime).format("DD.MM.YYYY")} {dayjs(customer.creationTime).format('DD.MM.YYYY')}
</span> </span>
</div> </div>
</div> </div>
@ -393,9 +371,7 @@ const CustomerView: React.FC = () => {
<div className="text-xl font-bold text-purple-600"> <div className="text-xl font-bold text-purple-600">
{((customer.averageOrderValue ?? 0) / 1000).toFixed(0)}K {((customer.averageOrderValue ?? 0) / 1000).toFixed(0)}K
</div> </div>
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">Ortalama Sipariş</div>
Ortalama Sipariş
</div>
</div> </div>
</div> </div>
@ -405,12 +381,7 @@ const CustomerView: React.FC = () => {
<div className="flex justify-between text-sm mb-1"> <div className="flex justify-between text-sm mb-1">
<span>Kredi Kullanımı</span> <span>Kredi Kullanımı</span>
<span> <span>
{Math.round( {Math.round(((customer.totalRevenue ?? 0) / customer.creditLimit) * 100)}%
((customer.totalRevenue ?? 0) /
customer.creditLimit) *
100
)}
%
</span> </span>
</div> </div>
<div className="w-full bg-gray-200 rounded-full h-1.5"> <div className="w-full bg-gray-200 rounded-full h-1.5">
@ -418,10 +389,8 @@ const CustomerView: React.FC = () => {
className="bg-blue-600 h-1.5 rounded-full" className="bg-blue-600 h-1.5 rounded-full"
style={{ style={{
width: `${Math.min( width: `${Math.min(
((customer.totalRevenue ?? 0) / ((customer.totalRevenue ?? 0) / (customer.creditLimit ?? 1)) * 100,
(customer.creditLimit ?? 1)) *
100, 100,
100
)}%`, )}%`,
}} }}
></div> ></div>
@ -447,7 +416,7 @@ const CustomerView: React.FC = () => {
{customer.primaryContact?.fullName} {customer.primaryContact?.fullName}
</h4> </h4>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{customer.primaryContact?.title || "Pozisyon belirtilmemiş"} {customer.primaryContact?.title || 'Pozisyon belirtilmemiş'}
</p> </p>
</div> </div>
@ -477,9 +446,7 @@ const CustomerView: React.FC = () => {
{/* Quick Actions */} {/* Quick Actions */}
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3"> <h3 className="text-base font-semibold text-gray-900 mb-3">Hızlı İşlemler</h3>
Hızlı İşlemler
</h3>
<div className="space-y-2"> <div className="space-y-2">
<button className="w-full flex items-center justify-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"> <button className="w-full flex items-center justify-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<FaShoppingCart className="w-4 h-4 mr-2" /> <FaShoppingCart className="w-4 h-4 mr-2" />
@ -506,11 +473,9 @@ const CustomerView: React.FC = () => {
<div className="flex items-start space-x-3"> <div className="flex items-start space-x-3">
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div> <div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-900"> <p className="text-sm text-gray-900">Yeni sipariş oluşturuldu</p>
Yeni sipariş oluşturuldu
</p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{dayjs().subtract(2, "hour").format("HH:mm")} {dayjs().subtract(2, 'hour').format('HH:mm')}
</p> </p>
</div> </div>
</div> </div>
@ -519,7 +484,7 @@ const CustomerView: React.FC = () => {
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-900">Teklif onaylandı</p> <p className="text-sm text-gray-900">Teklif onaylandı</p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{dayjs().subtract(1, "day").format("DD.MM.YYYY")} {dayjs().subtract(1, 'day').format('DD.MM.YYYY')}
</p> </p>
</div> </div>
</div> </div>
@ -528,7 +493,7 @@ const CustomerView: React.FC = () => {
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-900">Görüşme yapıldı</p> <p className="text-sm text-gray-900">Görüşme yapıldı</p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{dayjs().subtract(3, "day").format("DD.MM.YYYY")} {dayjs().subtract(3, 'day').format('DD.MM.YYYY')}
</p> </p>
</div> </div>
</div> </div>
@ -538,7 +503,7 @@ const CustomerView: React.FC = () => {
</div> </div>
)} )}
{activeTab === "contact" && ( {activeTab === 'contact' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Primary Contact */} {/* Primary Contact */}
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
@ -557,8 +522,7 @@ const CustomerView: React.FC = () => {
{customer.primaryContact?.fullName} {customer.primaryContact?.fullName}
</h4> </h4>
<p className="text-gray-600"> <p className="text-gray-600">
{customer.primaryContact?.title || {customer.primaryContact?.title || 'Pozisyon belirtilmemiş'}
"Pozisyon belirtilmemiş"}
</p> </p>
</div> </div>
</div> </div>
@ -605,16 +569,12 @@ const CustomerView: React.FC = () => {
<div className="space-y-3"> <div className="space-y-3">
<div className="p-4 bg-gray-50 rounded-lg"> <div className="p-4 bg-gray-50 rounded-lg">
<div className="space-y-2"> <div className="space-y-2">
<p className="font-medium text-gray-900"> <p className="font-medium text-gray-900">{customer.address?.street}</p>
{customer.address?.street}
</p>
<p className="text-gray-600">{customer.address?.city}</p> <p className="text-gray-600">{customer.address?.city}</p>
<p className="text-gray-600"> <p className="text-gray-600">
{customer.address?.state} {customer.address?.postalCode} {customer.address?.state} {customer.address?.postalCode}
</p> </p>
<p className="text-gray-600 font-medium"> <p className="text-gray-600 font-medium">{customer.address?.country}</p>
{customer.address?.country}
</p>
</div> </div>
</div> </div>
@ -647,20 +607,15 @@ const CustomerView: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{customer.contacts.map((contact, index) => ( {customer.contacts.map((contact, index) => (
<div <div key={index} className="p-3 border border-gray-200 rounded-lg">
key={index}
className="p-3 border border-gray-200 rounded-lg"
>
<div className="flex items-start space-x-3"> <div className="flex items-start space-x-3">
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center"> <div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
<FaUser className="w-5 h-5 text-gray-500" /> <FaUser className="w-5 h-5 text-gray-500" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h4 className="font-medium text-gray-900"> <h4 className="font-medium text-gray-900">{contact.fullName}</h4>
{contact.fullName}
</h4>
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">
{contact.title || "Pozisyon belirtilmemiş"} {contact.title || 'Pozisyon belirtilmemiş'}
</p> </p>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
@ -694,7 +649,7 @@ const CustomerView: React.FC = () => {
</div> </div>
)} )}
{activeTab === "business" && ( {activeTab === 'business' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Company Information */} {/* Company Information */}
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
@ -721,12 +676,8 @@ const CustomerView: React.FC = () => {
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">Sektör</label>
Sektör <p className="text-gray-900">{customer.industry || 'Belirtilmemiş'}</p>
</label>
<p className="text-gray-900">
{customer.industry || "Belirtilmemiş"}
</p>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">
@ -741,9 +692,7 @@ const CustomerView: React.FC = () => {
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">
Vergi Numarası Vergi Numarası
</label> </label>
<p className="text-gray-900 font-mono"> <p className="text-gray-900 font-mono">{customer.taxNumber}</p>
{customer.taxNumber}
</p>
</div> </div>
)} )}
@ -752,9 +701,7 @@ const CustomerView: React.FC = () => {
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">
Sicil Numarası Sicil Numarası
</label> </label>
<p className="text-gray-900 font-mono"> <p className="text-gray-900 font-mono">{customer.registrationNumber}</p>
{customer.registrationNumber}
</p>
</div> </div>
)} )}
</div> </div>
@ -775,14 +722,14 @@ const CustomerView: React.FC = () => {
</label> </label>
<span <span
className={classNames( className={classNames(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-sm font-medium border", 'inline-flex items-center px-2.5 py-0.5 rounded-full text-sm font-medium border',
getBusinessPartyStatusColor( getBusinessPartyStatusColor(
customer.status || BusinessPartyStatusEnum.Active customer.status || BusinessPartyStatusEnum.Active,
) ),
)} )}
> >
{getBusinessPartyStatusName( {getBusinessPartyStatusName(
customer.status || BusinessPartyStatusEnum.Active customer.status || BusinessPartyStatusEnum.Active,
)} )}
</span> </span>
</div> </div>
@ -792,7 +739,7 @@ const CustomerView: React.FC = () => {
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">
{getCustomerSegmentName( {getCustomerSegmentName(
customer.customerSegment || CustomerSegmentEnum.Startup customer.customerSegment || CustomerSegmentEnum.Startup,
)} )}
</p> </p>
</div> </div>
@ -813,7 +760,7 @@ const CustomerView: React.FC = () => {
Kayıt Tarihi Kayıt Tarihi
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">
{dayjs(customer.creationTime).format("DD.MM.YYYY")} {dayjs(customer.creationTime).format('DD.MM.YYYY')}
</p> </p>
</div> </div>
<div> <div>
@ -821,9 +768,7 @@ const CustomerView: React.FC = () => {
Son Güncelleme Son Güncelleme
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">
{dayjs(customer.lastModificationTime).format( {dayjs(customer.lastModificationTime).format('DD.MM.YYYY')}
"DD.MM.YYYY"
)}
</p> </p>
</div> </div>
</div> </div>
@ -832,7 +777,7 @@ const CustomerView: React.FC = () => {
</div> </div>
)} )}
{activeTab === "financial" && ( {activeTab === 'financial' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Financial Overview */} {/* Financial Overview */}
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
@ -845,9 +790,7 @@ const CustomerView: React.FC = () => {
<div className="grid grid-cols-1 gap-3"> <div className="grid grid-cols-1 gap-3">
<div className="p-4 bg-green-50 rounded-lg"> <div className="p-4 bg-green-50 rounded-lg">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-green-800"> <span className="text-sm font-medium text-green-800">Toplam Ciro</span>
Toplam Ciro
</span>
<FaArrowUp className="w-4 h-4 text-green-600" /> <FaArrowUp className="w-4 h-4 text-green-600" />
</div> </div>
<div className="text-2xl font-bold text-green-900"> <div className="text-2xl font-bold text-green-900">
@ -857,9 +800,7 @@ const CustomerView: React.FC = () => {
<div className="p-4 bg-blue-50 rounded-lg"> <div className="p-4 bg-blue-50 rounded-lg">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-blue-800"> <span className="text-sm font-medium text-blue-800">Ortalama Sipariş</span>
Ortalama Sipariş
</span>
<FaShoppingCart className="w-4 h-4 text-blue-600" /> <FaShoppingCart className="w-4 h-4 text-blue-600" />
</div> </div>
<div className="text-2xl font-bold text-blue-900"> <div className="text-2xl font-bold text-blue-900">
@ -882,38 +823,25 @@ const CustomerView: React.FC = () => {
<div> <div>
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700"> <span className="text-sm font-medium text-gray-700">Kredi Kullanımı</span>
Kredi Kullanımı
</span>
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
{Math.round( {Math.round(((customer.totalRevenue ?? 0) / customer.creditLimit) * 100)}%
((customer.totalRevenue ?? 0) / customer.creditLimit) *
100
)}
%
</span> </span>
</div> </div>
<div className="w-full bg-gray-200 rounded-full h-2"> <div className="w-full bg-gray-200 rounded-full h-2">
<div <div
className={classNames( className={classNames(
"h-3 rounded-full", 'h-3 rounded-full',
((customer.totalRevenue ?? 0) / customer.creditLimit) * ((customer.totalRevenue ?? 0) / customer.creditLimit) * 100 > 80
100 > ? 'bg-red-500'
80 : ((customer.totalRevenue ?? 0) / customer.creditLimit) * 100 > 60
? "bg-red-500" ? 'bg-yellow-500'
: ((customer.totalRevenue ?? 0) / : 'bg-green-500',
customer.creditLimit) *
100 >
60
? "bg-yellow-500"
: "bg-green-500"
)} )}
style={{ style={{
width: `${Math.min( width: `${Math.min(
((customer.totalRevenue ?? 0) / ((customer.totalRevenue ?? 0) / customer.creditLimit) * 100,
customer.creditLimit) *
100, 100,
100
)}%`, )}%`,
}} }}
></div> ></div>
@ -947,9 +875,7 @@ const CustomerView: React.FC = () => {
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">
Para Birimi Para Birimi
</label> </label>
<p className="text-lg font-medium text-gray-900"> <p className="text-lg font-medium text-gray-900">{customer.currency}</p>
{customer.currency}
</p>
</div> </div>
</div> </div>
@ -966,7 +892,7 @@ const CustomerView: React.FC = () => {
Son Sipariş Tarihi Son Sipariş Tarihi
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">
{dayjs(customer.lastOrderDate).format("DD.MM.YYYY")} {dayjs(customer.lastOrderDate).format('DD.MM.YYYY')}
</p> </p>
</div> </div>
)} )}
@ -975,7 +901,7 @@ const CustomerView: React.FC = () => {
</div> </div>
)} )}
{activeTab === "history" && ( {activeTab === 'history' && (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center"> <h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
@ -988,13 +914,9 @@ const CustomerView: React.FC = () => {
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div> <div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900"> <h4 className="font-medium text-gray-900">Müşteri Kaydı Oluşturuldu</h4>
Müşteri Kaydı Oluşturuldu
</h4>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
{dayjs(customer.creationTime).format( {dayjs(customer.creationTime).format('DD.MM.YYYY HH:mm')}
"DD.MM.YYYY HH:mm"
)}
</span> </span>
</div> </div>
</div> </div>
@ -1004,18 +926,12 @@ const CustomerView: React.FC = () => {
<div className="w-2 h-2 bg-green-500 rounded-full mt-2"></div> <div className="w-2 h-2 bg-green-500 rounded-full mt-2"></div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900"> <h4 className="font-medium text-gray-900">Son Güncelleme</h4>
Son Güncelleme
</h4>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
{dayjs(customer.lastModificationTime).format( {dayjs(customer.lastModificationTime).format('DD.MM.YYYY HH:mm')}
"DD.MM.YYYY HH:mm"
)}
</span> </span>
</div> </div>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">Müşteri bilgileri güncellendi.</p>
Müşteri bilgileri güncellendi.
</p>
</div> </div>
</div> </div>
@ -1024,16 +940,13 @@ const CustomerView: React.FC = () => {
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2"></div> <div className="w-2 h-2 bg-purple-500 rounded-full mt-2"></div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900"> <h4 className="font-medium text-gray-900">Son Sipariş</h4>
Son Sipariş
</h4>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
{dayjs(customer.lastOrderDate).format("DD.MM.YYYY")} {dayjs(customer.lastOrderDate).format('DD.MM.YYYY')}
</span> </span>
</div> </div>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">
Son sipariş tarihi:{" "} Son sipariş tarihi: {dayjs(customer.lastOrderDate).format('DD.MM.YYYY')}
{dayjs(customer.lastOrderDate).format("DD.MM.YYYY")}
</p> </p>
</div> </div>
</div> </div>
@ -1044,7 +957,8 @@ const CustomerView: React.FC = () => {
)} )}
</div> </div>
</div> </div>
); </Container>
}; )
}
export default CustomerView; export default CustomerView

View file

@ -1,17 +1,14 @@
import React from "react"; import React from 'react'
import { FaEdit, FaTrash, FaEye, FaBullseye } from "react-icons/fa"; import { FaEdit, FaTrash, FaEye, FaBullseye } from 'react-icons/fa'
import { CrmLostReason, CrmOpportunity } from "../../../types/crm"; import { CrmLostReason, CrmOpportunity } from '../../../types/crm'
import { import { getLostReasonCategoryColor, getLostReasonCategoryText } from '../../../utils/erp'
getLostReasonCategoryColor,
getLostReasonCategoryText,
} from "../../../utils/erp";
interface LossReasonCardViewProps { interface LossReasonCardViewProps {
lossReasons: CrmLostReason[]; lossReasons: CrmLostReason[]
opportunities: CrmOpportunity[]; opportunities: CrmOpportunity[]
onEdit: (reason: CrmLostReason) => void; onEdit: (reason: CrmLostReason) => void
onDelete: (id: string) => void; onDelete: (id: string) => void
onView: (reason: CrmLostReason) => void; onView: (reason: CrmLostReason) => void
} }
const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
@ -22,15 +19,14 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
onView, onView,
}) => { }) => {
const getUsageCount = (reasonId: string) => { const getUsageCount = (reasonId: string) => {
return opportunities.filter((opp) => opp.lostReason?.id === reasonId) return opportunities.filter((opp) => opp.lostReason?.id === reasonId).length
.length; }
};
const getLostValue = (reasonId: string) => { const getLostValue = (reasonId: string) => {
return opportunities return opportunities
.filter((opp) => opp.lostReason?.id === reasonId) .filter((opp) => opp.lostReason?.id === reasonId)
.reduce((sum, opp) => sum + opp.estimatedValue, 0); .reduce((sum, opp) => sum + opp.estimatedValue, 0)
}; }
if (lossReasons.length === 0) { if (lossReasons.length === 0) {
return ( return (
@ -38,21 +34,17 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4"> <div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<FaBullseye className="w-8 h-8 text-gray-400" /> <FaBullseye className="w-8 h-8 text-gray-400" />
</div> </div>
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">Kayıp nedeni bulunamadı</h3>
Kayıp nedeni bulunamadı <p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
</div> </div>
); )
} }
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{lossReasons.map((reason) => { {lossReasons.map((reason) => {
const usageCount = getUsageCount(reason.id); const usageCount = getUsageCount(reason.id)
const lostValue = getLostValue(reason.id); const lostValue = getLostValue(reason.id)
return ( return (
<div <div
@ -63,21 +55,17 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
<div className="p-3 border-b"> <div className="p-3 border-b">
<div className="flex items-start justify-between mb-1"> <div className="flex items-start justify-between mb-1">
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold text-gray-900 text-sm"> <h3 className="font-semibold text-gray-900 text-sm">{reason.name}</h3>
{reason.name}
</h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-gray-500 font-mono"> <span className="text-xs text-gray-500 font-mono">{reason.code}</span>
{reason.code}
</span>
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${ className={`px-2 py-1 text-xs font-medium rounded-full ${
reason.isActive reason.isActive
? "bg-green-100 text-green-800" ? 'bg-green-100 text-green-800'
: "bg-gray-100 text-gray-800" : 'bg-gray-100 text-gray-800'
}`} }`}
> >
{reason.isActive ? "Aktif" : "Pasif"} {reason.isActive ? 'Aktif' : 'Pasif'}
</span> </span>
</div> </div>
</div> </div>
@ -86,7 +74,7 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
{/* Category */} {/* Category */}
<span <span
className={`inline-block px-2 py-1 text-xs font-medium rounded-full ${getLostReasonCategoryColor( className={`inline-block px-2 py-1 text-xs font-medium rounded-full ${getLostReasonCategoryColor(
reason.category reason.category,
)}`} )}`}
> >
{getLostReasonCategoryText(reason.category)} {getLostReasonCategoryText(reason.category)}
@ -100,9 +88,9 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
<p <p
className="text-xs text-gray-600 overflow-hidden flex-grow" className="text-xs text-gray-600 overflow-hidden flex-grow"
style={{ style={{
display: "-webkit-box", display: '-webkit-box',
WebkitLineClamp: 2, WebkitLineClamp: 2,
WebkitBoxOrient: "vertical", WebkitBoxOrient: 'vertical',
}} }}
> >
{reason.description} {reason.description}
@ -116,9 +104,7 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
<FaBullseye className="w-3 h-3 text-gray-400" /> <FaBullseye className="w-3 h-3 text-gray-400" />
<span className="text-xs text-gray-500">Kullanım</span> <span className="text-xs text-gray-500">Kullanım</span>
</div> </div>
<div className="text-sm font-bold text-gray-900"> <div className="text-sm font-bold text-gray-900">{usageCount}</div>
{usageCount}
</div>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-xs text-gray-500 mb-1">Kayıp Değer</div> <div className="text-xs text-gray-500 mb-1">Kayıp Değer</div>
@ -154,10 +140,10 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
</button> </button>
</div> </div>
</div> </div>
); )
})} })}
</div> </div>
); )
}; }
export default LossReasonCardView; export default LossReasonCardView

View file

@ -1,14 +1,14 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react'
import { FaTimes, FaSave } from "react-icons/fa"; import { FaTimes, FaSave } from 'react-icons/fa'
import { CrmLostReason, LostReasonCategoryEnum } from "../../../types/crm"; import { CrmLostReason, LostReasonCategoryEnum } from '../../../types/crm'
import { getLostReasonCategoryText } from "../../../utils/erp"; import { getLostReasonCategoryText } from '../../../utils/erp'
interface LossReasonModalProps { interface LossReasonModalProps {
isOpen: boolean; isOpen: boolean
onClose: () => void; onClose: () => void
onSave: (reason: Partial<CrmLostReason>) => void; onSave: (reason: Partial<CrmLostReason>) => void
editingReason?: CrmLostReason | null; editingReason?: CrmLostReason | null
mode: "add" | "edit" | "view"; mode: 'add' | 'edit' | 'view'
} }
const LossReasonModal: React.FC<LossReasonModalProps> = ({ const LossReasonModal: React.FC<LossReasonModalProps> = ({
@ -19,102 +19,97 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
mode, mode,
}) => { }) => {
const [formData, setFormData] = useState<Partial<CrmLostReason>>({ const [formData, setFormData] = useState<Partial<CrmLostReason>>({
code: "", code: '',
name: "", name: '',
description: "", description: '',
category: LostReasonCategoryEnum.Price, category: LostReasonCategoryEnum.Price,
isActive: true, isActive: true,
}); })
const [errors, setErrors] = useState<{ [key: string]: string }>({}); const [errors, setErrors] = useState<{ [key: string]: string }>({})
useEffect(() => { useEffect(() => {
if (editingReason) { if (editingReason) {
setFormData(editingReason); setFormData(editingReason)
} else { } else {
setFormData({ setFormData({
code: "", code: '',
name: "", name: '',
description: "", description: '',
category: LostReasonCategoryEnum.Price, category: LostReasonCategoryEnum.Price,
isActive: true, isActive: true,
}); })
} }
setErrors({}); setErrors({})
}, [editingReason, isOpen]); }, [editingReason, isOpen])
const validateForm = () => { const validateForm = () => {
const newErrors: { [key: string]: string } = {}; const newErrors: { [key: string]: string } = {}
if (!formData.code?.trim()) { if (!formData.code?.trim()) {
newErrors.code = "Kod zorunludur"; newErrors.code = 'Kod zorunludur'
} }
if (!formData.name?.trim()) { if (!formData.name?.trim()) {
newErrors.name = "Kayıp nedeni adı zorunludur"; newErrors.name = 'Kayıp nedeni adı zorunludur'
} }
if (!formData.category) { if (!formData.category) {
newErrors.category = "Kategori seçimi zorunludur"; newErrors.category = 'Kategori seçimi zorunludur'
} }
setErrors(newErrors); setErrors(newErrors)
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0
}; }
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault()
if (mode === "view") { if (mode === 'view') {
onClose(); onClose()
return; return
} }
if (validateForm()) { if (validateForm()) {
onSave(formData); onSave(formData)
onClose(); onClose()
}
} }
};
const handleInputChange = ( const handleInputChange = (
field: keyof CrmLostReason, field: keyof CrmLostReason,
value: string | boolean | LostReasonCategoryEnum value: string | boolean | LostReasonCategoryEnum,
) => { ) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }))
if (errors[field]) { if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" })); setErrors((prev) => ({ ...prev, [field]: '' }))
}
} }
};
if (!isOpen) return null; if (!isOpen) return null
const getModalTitle = () => { const getModalTitle = () => {
switch (mode) { switch (mode) {
case "add": case 'add':
return "Yeni Kayıp Nedeni"; return 'Yeni Kayıp Nedeni'
case "edit": case 'edit':
return "Kayıp Nedeni Düzenle"; return 'Kayıp Nedeni Düzenle'
case "view": case 'view':
return "Kayıp Nedeni Detayları"; return 'Kayıp Nedeni Detayları'
default: default:
return ""; return ''
}
} }
};
const isReadOnly = mode === "view"; const isReadOnly = mode === 'view'
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4"> <div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-3 border-b"> <div className="flex items-center justify-between p-3 border-b">
<h3 className="text-sm font-semibold text-gray-900"> <h3 className="text-sm font-semibold text-gray-900">{getModalTitle()}</h3>
{getModalTitle()} <button onClick={onClose} className="text-gray-400 hover:text-gray-600">
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<FaTimes className="w-5 h-5" /> <FaTimes className="w-5 h-5" />
</button> </button>
</div> </div>
@ -124,60 +119,47 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
<div className="space-y-2"> <div className="space-y-2">
{/* Code */} {/* Code */}
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Kod *</label>
Kod *
</label>
<input <input
type="text" type="text"
value={formData.code || ""} value={formData.code || ''}
onChange={(e) => handleInputChange("code", e.target.value)} onChange={(e) => handleInputChange('code', e.target.value)}
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.code ? "border-red-500" : "border-gray-300" errors.code ? 'border-red-500' : 'border-gray-300'
} ${isReadOnly ? "bg-gray-100" : ""}`} } ${isReadOnly ? 'bg-gray-100' : ''}`}
placeholder="Örn: LR001" placeholder="Örn: LR001"
readOnly={isReadOnly} readOnly={isReadOnly}
/> />
{errors.code && ( {errors.code && <p className="text-red-500 text-sm mt-1">{errors.code}</p>}
<p className="text-red-500 text-sm mt-1">{errors.code}</p>
)}
</div> </div>
{/* Name */} {/* Name */}
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Kayıp Nedeni *</label>
Kayıp Nedeni *
</label>
<input <input
type="text" type="text"
value={formData.name || ""} value={formData.name || ''}
onChange={(e) => handleInputChange("name", e.target.value)} onChange={(e) => handleInputChange('name', e.target.value)}
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.name ? "border-red-500" : "border-gray-300" errors.name ? 'border-red-500' : 'border-gray-300'
} ${isReadOnly ? "bg-gray-100" : ""}`} } ${isReadOnly ? 'bg-gray-100' : ''}`}
placeholder="Kayıp nedeni adını girin" placeholder="Kayıp nedeni adını girin"
readOnly={isReadOnly} readOnly={isReadOnly}
/> />
{errors.name && ( {errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</div> </div>
{/* Category */} {/* Category */}
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Kategori *</label>
Kategori *
</label>
<select <select
value={formData.category || ""} value={formData.category || ''}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('category', e.target.value as LostReasonCategoryEnum)
"category",
e.target.value as LostReasonCategoryEnum
)
} }
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.category ? "border-red-500" : "border-gray-300" errors.category ? 'border-red-500' : 'border-gray-300'
} ${isReadOnly ? "bg-gray-100" : ""}`} } ${isReadOnly ? 'bg-gray-100' : ''}`}
disabled={isReadOnly} disabled={isReadOnly}
> >
{Object.values(LostReasonCategoryEnum).map((category) => ( {Object.values(LostReasonCategoryEnum).map((category) => (
@ -186,24 +168,18 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
</option> </option>
))} ))}
</select> </select>
{errors.category && ( {errors.category && <p className="text-red-500 text-sm mt-1">{errors.category}</p>}
<p className="text-red-500 text-sm mt-1">{errors.category}</p>
)}
</div> </div>
{/* Description */} {/* Description */}
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">ıklama</label>
ıklama
</label>
<textarea <textarea
value={formData.description || ""} value={formData.description || ''}
onChange={(e) => onChange={(e) => handleInputChange('description', e.target.value)}
handleInputChange("description", e.target.value)
}
rows={2} rows={2}
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
isReadOnly ? "bg-gray-100" : "border-gray-300" isReadOnly ? 'bg-gray-100' : 'border-gray-300'
}`} }`}
placeholder="Kayıp nedeni hakkında detaylııklama" placeholder="Kayıp nedeni hakkında detaylııklama"
readOnly={isReadOnly} readOnly={isReadOnly}
@ -216,16 +192,11 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
type="checkbox" type="checkbox"
id="isActive" id="isActive"
checked={formData.isActive || false} checked={formData.isActive || false}
onChange={(e) => onChange={(e) => handleInputChange('isActive', e.target.checked)}
handleInputChange("isActive", e.target.checked)
}
className="mr-2" className="mr-2"
disabled={isReadOnly} disabled={isReadOnly}
/> />
<label <label htmlFor="isActive" className="text-sm font-medium text-gray-700">
htmlFor="isActive"
className="text-sm font-medium text-gray-700"
>
Aktif Aktif
</label> </label>
</div> </div>
@ -238,9 +209,9 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
onClick={onClose} onClick={onClose}
className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors" className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
> >
{mode === "view" ? "Kapat" : "İptal"} {mode === 'view' ? 'Kapat' : 'İptal'}
</button> </button>
{mode !== "view" && ( {mode !== 'view' && (
<button <button
type="submit" type="submit"
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" className="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
@ -253,7 +224,7 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
</form> </form>
</div> </div>
</div> </div>
); )
}; }
export default LossReasonModal; export default LossReasonModal

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { import {
FaPlus, FaPlus,
FaEdit, FaEdit,
@ -9,104 +9,86 @@ import {
FaTh, FaTh,
FaList, FaList,
FaTimes, FaTimes,
} from "react-icons/fa"; } from 'react-icons/fa'
import { import { CrmLostReason, LostReasonCategoryEnum, CrmOpportunity } from '../../../types/crm'
CrmLostReason, import DataTable, { Column } from '../../../components/common/DataTable'
LostReasonCategoryEnum, import LossReasonModal from './LossReasonModal'
CrmOpportunity, import LossReasonCardView from './LossReasonCardView'
} from "../../../types/crm"; import { mockLossReasons } from '../../../mocks/mockLossReasons'
import DataTable, { Column } from "../../../components/common/DataTable"; import { mockOpportunities } from '../../../mocks/mockOpportunities'
import LossReasonModal from "./LossReasonModal"; import Widget from '../../../components/common/Widget'
import LossReasonCardView from "./LossReasonCardView"; import { getLostReasonCategoryColor, getLostReasonCategoryText } from '../../../utils/erp'
import { mockLossReasons } from "../../../mocks/mockLossReasons"; import { Container } from '@/components/shared'
import { mockOpportunities } from "../../../mocks/mockOpportunities";
import Widget from "../../../components/common/Widget";
import {
getLostReasonCategoryColor,
getLostReasonCategoryText,
} from "../../../utils/erp";
const LossReasons: React.FC = () => { const LossReasons: React.FC = () => {
const [lossReasons, setLossReasons] = const [lossReasons, setLossReasons] = useState<CrmLostReason[]>(mockLossReasons)
useState<CrmLostReason[]>(mockLossReasons); const [opportunities] = useState<CrmOpportunity[]>(mockOpportunities)
const [opportunities] = useState<CrmOpportunity[]>(mockOpportunities); const [searchTerm, setSearchTerm] = useState('')
const [searchTerm, setSearchTerm] = useState(""); const [selectedCategory, setSelectedCategory] = useState<LostReasonCategoryEnum | 'all'>('all')
const [selectedCategory, setSelectedCategory] = useState< const [viewMode, setViewMode] = useState<'table' | 'card'>('table')
LostReasonCategoryEnum | "all"
>("all");
const [viewMode, setViewMode] = useState<"table" | "card">("table");
// Modal states // Modal states
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false)
const [modalMode, setModalMode] = useState<"add" | "edit" | "view">("add"); const [modalMode, setModalMode] = useState<'add' | 'edit' | 'view'>('add')
const [editingReason, setEditingReason] = useState<CrmLostReason | null>( const [editingReason, setEditingReason] = useState<CrmLostReason | null>(null)
null
);
// Delete modal states // Delete modal states
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [reasonToDelete, setReasonToDelete] = useState<CrmLostReason | null>( const [reasonToDelete, setReasonToDelete] = useState<CrmLostReason | null>(null)
null
);
const handleAdd = () => { const handleAdd = () => {
setModalMode("add"); setModalMode('add')
setEditingReason(null); setEditingReason(null)
setIsModalOpen(true); setIsModalOpen(true)
}; }
const handleEdit = (reason: CrmLostReason) => { const handleEdit = (reason: CrmLostReason) => {
setModalMode("edit"); setModalMode('edit')
setEditingReason(reason); setEditingReason(reason)
setIsModalOpen(true); setIsModalOpen(true)
}; }
const handleView = (reason: CrmLostReason) => { const handleView = (reason: CrmLostReason) => {
setModalMode("view"); setModalMode('view')
setEditingReason(reason); setEditingReason(reason)
setIsModalOpen(true); setIsModalOpen(true)
}; }
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
const reason = lossReasons.find((r) => r.id === id); const reason = lossReasons.find((r) => r.id === id)
if (reason) { if (reason) {
setReasonToDelete(reason); setReasonToDelete(reason)
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true)
}
} }
};
const handleSave = (reasonData: Partial<CrmLostReason>) => { const handleSave = (reasonData: Partial<CrmLostReason>) => {
if (modalMode === "add") { if (modalMode === 'add') {
const newReason: CrmLostReason = { const newReason: CrmLostReason = {
id: Date.now().toString(), id: Date.now().toString(),
code: reasonData.code || "", code: reasonData.code || '',
name: reasonData.name || "", name: reasonData.name || '',
description: reasonData.description || "", description: reasonData.description || '',
category: reasonData.category || LostReasonCategoryEnum.Price, category: reasonData.category || LostReasonCategoryEnum.Price,
isActive: isActive: reasonData.isActive !== undefined ? reasonData.isActive : true,
reasonData.isActive !== undefined ? reasonData.isActive : true, }
}; setLossReasons((prev) => [...prev, newReason])
setLossReasons((prev) => [...prev, newReason]); } else if (modalMode === 'edit' && editingReason) {
} else if (modalMode === "edit" && editingReason) {
setLossReasons((prev) => setLossReasons((prev) =>
prev.map((reason) => prev.map((reason) =>
reason.id === editingReason.id reason.id === editingReason.id ? ({ ...reason, ...reasonData } as CrmLostReason) : reason,
? ({ ...reason, ...reasonData } as CrmLostReason) ),
: reason
) )
);
} }
}; }
const confirmDelete = () => { const confirmDelete = () => {
if (reasonToDelete) { if (reasonToDelete) {
setLossReasons((prev) => setLossReasons((prev) => prev.filter((reason) => reason.id !== reasonToDelete.id))
prev.filter((reason) => reason.id !== reasonToDelete.id) setIsDeleteModalOpen(false)
); setReasonToDelete(null)
setIsDeleteModalOpen(false); }
setReasonToDelete(null);
} }
};
const filteredReasons = lossReasons.filter((reason) => { const filteredReasons = lossReasons.filter((reason) => {
if ( if (
@ -114,26 +96,26 @@ const LossReasons: React.FC = () => {
!reason.name.toLowerCase().includes(searchTerm.toLowerCase()) && !reason.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
!reason.code.toLowerCase().includes(searchTerm.toLowerCase()) !reason.code.toLowerCase().includes(searchTerm.toLowerCase())
) { ) {
return false; return false
} }
if (selectedCategory !== "all" && reason.category !== selectedCategory) { if (selectedCategory !== 'all' && reason.category !== selectedCategory) {
return false; return false
} }
return true; return true
}); })
const columns: Column<CrmLostReason>[] = [ const columns: Column<CrmLostReason>[] = [
{ {
key: "code", key: 'code',
header: "Kod", header: 'Kod',
sortable: true, sortable: true,
render: (reason: CrmLostReason) => ( render: (reason: CrmLostReason) => (
<div className="font-medium text-gray-900">{reason.code}</div> <div className="font-medium text-gray-900">{reason.code}</div>
), ),
}, },
{ {
key: "name", key: 'name',
header: "Kayıp Nedeni", header: 'Kayıp Nedeni',
sortable: true, sortable: true,
render: (reason: CrmLostReason) => ( render: (reason: CrmLostReason) => (
<div> <div>
@ -143,12 +125,12 @@ const LossReasons: React.FC = () => {
), ),
}, },
{ {
key: "category", key: 'category',
header: "Kategori", header: 'Kategori',
render: (reason: CrmLostReason) => ( render: (reason: CrmLostReason) => (
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${getLostReasonCategoryColor( className={`px-2 py-1 text-xs font-medium rounded-full ${getLostReasonCategoryColor(
reason.category reason.category,
)}`} )}`}
> >
{getLostReasonCategoryText(reason.category)} {getLostReasonCategoryText(reason.category)}
@ -156,38 +138,34 @@ const LossReasons: React.FC = () => {
), ),
}, },
{ {
key: "usage", key: 'usage',
header: "Kullanım", header: 'Kullanım',
render: (reason: CrmLostReason) => { render: (reason: CrmLostReason) => {
const usageCount = opportunities.filter( const usageCount = opportunities.filter((opp) => opp.lostReason?.id === reason.id).length
(opp) => opp.lostReason?.id === reason.id
).length;
return ( return (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FaBullseye className="w-4 h-4 text-gray-400" /> <FaBullseye className="w-4 h-4 text-gray-400" />
<span>{usageCount} Fırsat</span> <span>{usageCount} Fırsat</span>
</div> </div>
); )
}, },
}, },
{ {
key: "status", key: 'status',
header: "Durum", header: 'Durum',
render: (reason: CrmLostReason) => ( render: (reason: CrmLostReason) => (
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${ className={`px-2 py-1 text-xs font-medium rounded-full ${
reason.isActive reason.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
}`} }`}
> >
{reason.isActive ? "Aktif" : "Pasif"} {reason.isActive ? 'Aktif' : 'Pasif'}
</span> </span>
), ),
}, },
{ {
key: "actions", key: 'actions',
header: "İşlemler", header: 'İşlemler',
render: (reason: CrmLostReason) => ( render: (reason: CrmLostReason) => (
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@ -214,48 +192,41 @@ const LossReasons: React.FC = () => {
</div> </div>
), ),
}, },
]; ]
// Calculate statistics // Calculate statistics
const totalReasons = lossReasons.length; const totalReasons = lossReasons.length
const activeReasons = lossReasons.filter((r) => r.isActive).length; const activeReasons = lossReasons.filter((r) => r.isActive).length
const categoryDistribution = Object.values(LostReasonCategoryEnum).map( const categoryDistribution = Object.values(LostReasonCategoryEnum).map((category) => ({
(category) => ({
category, category,
count: lossReasons.filter((r) => r.category === category).length, count: lossReasons.filter((r) => r.category === category).length,
usage: opportunities.filter( usage: opportunities.filter(
(opp) => (opp) =>
opp.lostReason && opp.lostReason &&
lossReasons.find( lossReasons.find((r) => r.id === opp.lostReason?.id && r.category === category),
(r) => r.id === opp.lostReason?.id && r.category === category
)
).length, ).length,
}) }))
);
// Top loss reasons by usage // Top loss reasons by usage
const topLossReasons = lossReasons const topLossReasons = lossReasons
.map((reason) => ({ .map((reason) => ({
...reason, ...reason,
usageCount: opportunities.filter( usageCount: opportunities.filter((opp) => opp.lostReason?.id === reason.id).length,
(opp) => opp.lostReason?.id === reason.id
).length,
lostValue: opportunities lostValue: opportunities
.filter((opp) => opp.lostReason?.id === reason.id) .filter((opp) => opp.lostReason?.id === reason.id)
.reduce((sum, opp) => sum + opp.estimatedValue, 0), .reduce((sum, opp) => sum + opp.estimatedValue, 0),
})) }))
.sort((a, b) => b.usageCount - a.usageCount) .sort((a, b) => b.usageCount - a.usageCount)
.slice(0, 5); .slice(0, 5)
return ( return (
<Container>
<div className="space-y-3 pt-2"> <div className="space-y-3 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-lg font-bold text-gray-900">Kayıp Nedenleri</h2> <h2 className="text-lg font-bold text-gray-900">Kayıp Nedenleri</h2>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">Fırsat kaybı nedenlerini analiz edin</p>
Fırsat kaybı nedenlerini analiz edin
</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
@ -277,12 +248,7 @@ const LossReasons: React.FC = () => {
icon="FaExclamationTriangle" icon="FaExclamationTriangle"
/> />
<Widget <Widget title="Aktif Neden" value={activeReasons} color="green" icon="FaArrowDown" />
title="Aktif Neden"
value={activeReasons}
color="green"
icon="FaArrowDown"
/>
<Widget <Widget
title="Kayıp Fırsat" title="Kayıp Fırsat"
@ -293,7 +259,7 @@ const LossReasons: React.FC = () => {
<Widget <Widget
title="En Çok Kullanılan" title="En Çok Kullanılan"
value={topLossReasons[0]?.name || "-"} value={topLossReasons[0]?.name || '-'}
color="purple" color="purple"
icon="FaChartBar" icon="FaChartBar"
valueClassName="text-lg" valueClassName="text-lg"
@ -302,22 +268,18 @@ const LossReasons: React.FC = () => {
{/* Category Distribution */} {/* Category Distribution */}
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3"> <h3 className="text-base font-semibold text-gray-900 mb-3">Kategori Dağılımı</h3>
Kategori Dağılımı
</h3>
<div className="grid grid-cols-2 md:grid-cols-6 gap-3"> <div className="grid grid-cols-2 md:grid-cols-6 gap-3">
{categoryDistribution.map(({ category, count, usage }) => ( {categoryDistribution.map(({ category, count, usage }) => (
<div key={category} className="text-center p-4 border rounded-lg"> <div key={category} className="text-center p-4 border rounded-lg">
<div <div
className={`inline-block px-2 py-1 text-xs font-medium rounded-full mb-2 ${getLostReasonCategoryColor( className={`inline-block px-2 py-1 text-xs font-medium rounded-full mb-2 ${getLostReasonCategoryColor(
category category,
)}`} )}`}
> >
{getLostReasonCategoryText(category)} {getLostReasonCategoryText(category)}
</div> </div>
<div className="text-lg font-bold text-gray-900 mb-1"> <div className="text-lg font-bold text-gray-900 mb-1">{count}</div>
{count}
</div>
<div className="text-xs text-gray-500">{usage} Kullanım</div> <div className="text-xs text-gray-500">{usage} Kullanım</div>
</div> </div>
))} ))}
@ -342,8 +304,7 @@ const LossReasons: React.FC = () => {
<div> <div>
<h4 className="font-medium text-gray-900">{reason.name}</h4> <h4 className="font-medium text-gray-900">{reason.name}</h4>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{getLostReasonCategoryText(reason.category)} {" "} {getLostReasonCategoryText(reason.category)} {reason.usageCount} fırsat
{reason.usageCount} fırsat
</p> </p>
</div> </div>
</div> </div>
@ -372,11 +333,7 @@ const LossReasons: React.FC = () => {
<select <select
value={selectedCategory} value={selectedCategory}
onChange={(e) => onChange={(e) => setSelectedCategory(e.target.value as LostReasonCategoryEnum | 'all')}
setSelectedCategory(
e.target.value as LostReasonCategoryEnum | "all"
)
}
className="px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="all">Tüm Kategoriler</option> <option value="all">Tüm Kategoriler</option>
@ -390,21 +347,21 @@ const LossReasons: React.FC = () => {
{/* View Mode Toggle */} {/* View Mode Toggle */}
<div className="flex bg-gray-100 rounded-lg p-1"> <div className="flex bg-gray-100 rounded-lg p-1">
<button <button
onClick={() => setViewMode("table")} onClick={() => setViewMode('table')}
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${ className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
viewMode === "table" viewMode === 'table'
? "bg-white text-gray-900 shadow-sm" ? 'bg-white text-gray-900 shadow-sm'
: "text-gray-600 hover:text-gray-900" : 'text-gray-600 hover:text-gray-900'
}`} }`}
> >
<FaList className="w-4 h-4" /> <FaList className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => setViewMode("card")} onClick={() => setViewMode('card')}
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${ className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
viewMode === "card" viewMode === 'card'
? "bg-white text-gray-900 shadow-sm" ? 'bg-white text-gray-900 shadow-sm'
: "text-gray-600 hover:text-gray-900" : 'text-gray-600 hover:text-gray-900'
}`} }`}
> >
<FaTh className="w-4 h-4" /> <FaTh className="w-4 h-4" />
@ -413,7 +370,7 @@ const LossReasons: React.FC = () => {
</div> </div>
{/* Data Display */} {/* Data Display */}
{viewMode === "table" ? ( {viewMode === 'table' ? (
<div className="bg-white rounded-lg shadow-sm border"> <div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredReasons} columns={columns} /> <DataTable data={filteredReasons} columns={columns} />
</div> </div>
@ -427,15 +384,11 @@ const LossReasons: React.FC = () => {
/> />
)} )}
{filteredReasons.length === 0 && viewMode === "table" && ( {filteredReasons.length === 0 && viewMode === 'table' && (
<div className="text-center py-12"> <div className="text-center py-12">
<FaExclamationTriangle className="w-8 h-8 text-gray-400 mx-auto mb-2" /> <FaExclamationTriangle className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<h3 className="text-sm font-medium text-gray-900 mb-2"> <h3 className="text-sm font-medium text-gray-900 mb-2">Kayıp nedeni bulunamadı</h3>
Kayıp nedeni bulunamadı <p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
</div> </div>
)} )}
@ -443,8 +396,8 @@ const LossReasons: React.FC = () => {
<LossReasonModal <LossReasonModal
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={() => { onClose={() => {
setIsModalOpen(false); setIsModalOpen(false)
setEditingReason(null); setEditingReason(null)
}} }}
onSave={handleSave} onSave={handleSave}
editingReason={editingReason} editingReason={editingReason}
@ -459,14 +412,12 @@ const LossReasons: React.FC = () => {
<div className="flex items-center justify-between p-3 border-b"> <div className="flex items-center justify-between p-3 border-b">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<FaExclamationTriangle className="w-6 h-6 text-red-600" /> <FaExclamationTriangle className="w-6 h-6 text-red-600" />
<h3 className="text-sm font-semibold text-gray-900"> <h3 className="text-sm font-semibold text-gray-900">Silme Onayı</h3>
Silme Onayı
</h3>
</div> </div>
<button <button
onClick={() => { onClick={() => {
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false)
setReasonToDelete(null); setReasonToDelete(null)
}} }}
className="text-gray-400 hover:text-gray-600" className="text-gray-400 hover:text-gray-600"
> >
@ -484,8 +435,8 @@ const LossReasons: React.FC = () => {
Kayıp nedeni silinsin mi? Kayıp nedeni silinsin mi?
</h3> </h3>
<p className="text-sm text-gray-500 mb-6"> <p className="text-sm text-gray-500 mb-6">
<span className="font-medium">"{reasonToDelete?.name}"</span>{" "} <span className="font-medium">"{reasonToDelete?.name}"</span> kayıp nedenini
kayıp nedenini silmek üzeresiniz. Bu işlem geri alınamaz. silmek üzeresiniz. Bu işlem geri alınamaz.
</p> </p>
</div> </div>
@ -493,8 +444,8 @@ const LossReasons: React.FC = () => {
<div className="flex justify-end gap-1.5"> <div className="flex justify-end gap-1.5">
<button <button
onClick={() => { onClick={() => {
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false)
setReasonToDelete(null); setReasonToDelete(null)
}} }}
className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors" className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
> >
@ -513,7 +464,8 @@ const LossReasons: React.FC = () => {
</div> </div>
)} )}
</div> </div>
); </Container>
}; )
}
export default LossReasons; export default LossReasons

View file

@ -1,4 +1,4 @@
import React from "react"; import React from 'react'
import { import {
FaTimes, FaTimes,
FaBullseye, FaBullseye,
@ -13,21 +13,21 @@ import {
FaClock, FaClock,
FaPhone, FaPhone,
FaEnvelope, FaEnvelope,
} from "react-icons/fa"; } from 'react-icons/fa'
import { CrmOpportunity, OpportunityStageEnum } from "../../../types/crm"; import { CrmOpportunity, OpportunityStageEnum } from '../../../types/crm'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { import {
getOpportunityLeadSourceText, getOpportunityLeadSourceText,
getOpportunityProbabilityColor, getOpportunityProbabilityColor,
getOpportunityStageColor, getOpportunityStageColor,
getOpportunityStageText, getOpportunityStageText,
} from "../../../utils/erp"; } from '../../../utils/erp'
interface OpportunityDetailsProps { interface OpportunityDetailsProps {
isOpen: boolean; isOpen: boolean
onClose: () => void; onClose: () => void
onEdit: (opportunity: CrmOpportunity) => void; onEdit: (opportunity: CrmOpportunity) => void
opportunity: CrmOpportunity | null; opportunity: CrmOpportunity | null
} }
const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
@ -36,11 +36,9 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
onEdit, onEdit,
opportunity, opportunity,
}) => { }) => {
if (!isOpen || !opportunity) return null; if (!isOpen || !opportunity) return null
const customer = mockBusinessParties.find( const customer = mockBusinessParties.find((c) => c.id === opportunity.customerId)
(c) => c.id === opportunity.customerId
);
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
@ -52,12 +50,8 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<FaBullseye className="w-6 h-6 text-blue-600" /> <FaBullseye className="w-6 h-6 text-blue-600" />
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">{opportunity.title}</h2>
{opportunity.title} <p className="text-sm text-gray-600">{opportunity.opportunityNumber}</p>
</h2>
<p className="text-sm text-gray-600">
{opportunity.opportunityNumber}
</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -68,10 +62,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<FaEdit className="w-4 h-4" /> <FaEdit className="w-4 h-4" />
Düzenle Düzenle
</button> </button>
<button <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-2">
onClick={onClose}
className="text-gray-400 hover:text-gray-600 p-2"
>
<FaTimes className="w-5 h-5" /> <FaTimes className="w-5 h-5" />
</button> </button>
</div> </div>
@ -83,7 +74,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="text-center"> <div className="text-center">
<div <div
className={`inline-flex px-3 py-1.5 rounded-full text-xs font-medium border ${getOpportunityStageColor( className={`inline-flex px-3 py-1.5 rounded-full text-xs font-medium border ${getOpportunityStageColor(
opportunity.stage opportunity.stage,
)}`} )}`}
> >
{getOpportunityStageText(opportunity.stage)} {getOpportunityStageText(opportunity.stage)}
@ -102,7 +93,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="text-center"> <div className="text-center">
<div <div
className={`flex items-center justify-center gap-2 text-xl font-bold ${getOpportunityProbabilityColor( className={`flex items-center justify-center gap-2 text-xl font-bold ${getOpportunityProbabilityColor(
opportunity.probability opportunity.probability,
)}`} )}`}
> >
<FaPercentage className="w-5 h-5" /> <FaPercentage className="w-5 h-5" />
@ -114,9 +105,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="text-center"> <div className="text-center">
<div className="flex items-center justify-center gap-2 text-base font-semibold text-gray-900"> <div className="flex items-center justify-center gap-2 text-base font-semibold text-gray-900">
<FaCalendar className="w-4 h-4 text-gray-400" /> <FaCalendar className="w-4 h-4 text-gray-400" />
{new Date(opportunity.expectedCloseDate).toLocaleDateString( {new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}
"tr-TR"
)}
</div> </div>
<p className="text-xs text-gray-500 mt-1">Beklenen Kapanış</p> <p className="text-xs text-gray-500 mt-1">Beklenen Kapanış</p>
</div> </div>
@ -133,9 +122,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">Başlık</label>
Başlık
</label>
<p className="text-gray-900">{opportunity.title}</p> <p className="text-gray-900">{opportunity.title}</p>
</div> </div>
@ -144,16 +131,12 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">
ıklama ıklama
</label> </label>
<p className="text-gray-900 whitespace-pre-wrap"> <p className="text-gray-900 whitespace-pre-wrap">{opportunity.description}</p>
{opportunity.description}
</p>
</div> </div>
)} )}
<div> <div>
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">Kaynak</label>
Kaynak
</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaTags className="w-4 h-4 text-gray-400" /> <FaTags className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">
@ -209,16 +192,12 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaEnvelope className="w-4 h-4 text-gray-400" /> <FaEnvelope className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">{customer.primaryContact?.email}</span>
{customer.primaryContact?.email}
</span>
</div> </div>
{customer.primaryContact?.phone && ( {customer.primaryContact?.phone && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaPhone className="w-4 h-4 text-gray-400" /> <FaPhone className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">{customer.primaryContact?.phone}</span>
{customer.primaryContact?.phone}
</span>
</div> </div>
)} )}
</div> </div>
@ -238,14 +217,10 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="block text-sm font-medium text-gray-600 mb-1"> <label className="block text-sm font-medium text-gray-600 mb-1">Sorumlu</label>
Sorumlu
</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-400" /> <FaUser className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">{opportunity.assigned?.fullName}</span>
{opportunity.assigned?.fullName}
</span>
</div> </div>
</div> </div>
@ -256,9 +231,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaCalendar className="w-4 h-4 text-gray-400" /> <FaCalendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">
{new Date( {new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}
opportunity.expectedCloseDate
).toLocaleDateString("tr-TR")}
</span> </span>
</div> </div>
</div> </div>
@ -271,9 +244,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaCalendar className="w-4 h-4 text-gray-400" /> <FaCalendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-900"> <span className="text-gray-900">
{new Date( {new Date(opportunity.actualCloseDate).toLocaleDateString('tr-TR')}
opportunity.actualCloseDate
).toLocaleDateString("tr-TR")}
</span> </span>
</div> </div>
</div> </div>
@ -291,14 +262,10 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="flex items-start gap-3 border-l-2 border-blue-200 pl-4"> <div className="flex items-start gap-3 border-l-2 border-blue-200 pl-4">
<div className="w-3 h-3 bg-blue-600 rounded-full mt-1.5 -ml-6 border-2 border-white"></div> <div className="w-3 h-3 bg-blue-600 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900">Fırsat Oluşturuldu</p>
Fırsat Oluşturuldu
</p>
<p className="text-xs text-gray-500 flex items-center gap-1"> <p className="text-xs text-gray-500 flex items-center gap-1">
<FaClock className="w-3 h-3" /> <FaClock className="w-3 h-3" />
{new Date(opportunity.creationTime).toLocaleDateString( {new Date(opportunity.creationTime).toLocaleDateString('tr-TR')}
"tr-TR"
)}
</p> </p>
</div> </div>
</div> </div>
@ -306,14 +273,10 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="flex items-start gap-3 border-l-2 border-gray-200 pl-4"> <div className="flex items-start gap-3 border-l-2 border-gray-200 pl-4">
<div className="w-3 h-3 bg-gray-400 rounded-full mt-1.5 -ml-6 border-2 border-white"></div> <div className="w-3 h-3 bg-gray-400 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900">Son Güncelleme</p>
Son Güncelleme
</p>
<p className="text-xs text-gray-500 flex items-center gap-1"> <p className="text-xs text-gray-500 flex items-center gap-1">
<FaClock className="w-3 h-3" /> <FaClock className="w-3 h-3" />
{new Date( {new Date(opportunity.lastModificationTime).toLocaleDateString('tr-TR')}
opportunity.lastModificationTime
).toLocaleDateString("tr-TR")}
</p> </p>
</div> </div>
</div> </div>
@ -322,41 +285,37 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
{/* Progress Indicator */} {/* Progress Indicator */}
<div className="bg-gray-50 rounded-lg p-4"> <div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3"> <h3 className="text-base font-semibold text-gray-900 mb-3">Aşama İlerlemesi</h3>
Aşama İlerlemesi
</h3>
<div className="space-y-2"> <div className="space-y-2">
{Object.values(OpportunityStageEnum).map((stage, index) => { {Object.values(OpportunityStageEnum).map((stage, index) => {
const isCurrentStage = stage === opportunity.stage; const isCurrentStage = stage === opportunity.stage
const isPassedStage = const isPassedStage =
Object.values(OpportunityStageEnum).indexOf( Object.values(OpportunityStageEnum).indexOf(opportunity.stage) > index
opportunity.stage
) > index;
return ( return (
<div key={stage} className="flex items-center gap-3"> <div key={stage} className="flex items-center gap-3">
<div <div
className={`w-4 h-4 rounded-full border-2 ${ className={`w-4 h-4 rounded-full border-2 ${
isCurrentStage isCurrentStage
? "bg-blue-600 border-blue-600" ? 'bg-blue-600 border-blue-600'
: isPassedStage : isPassedStage
? "bg-green-600 border-green-600" ? 'bg-green-600 border-green-600'
: "bg-white border-gray-300" : 'bg-white border-gray-300'
}`} }`}
></div> ></div>
<span <span
className={`text-sm ${ className={`text-sm ${
isCurrentStage isCurrentStage
? "font-semibold text-blue-600" ? 'font-semibold text-blue-600'
: isPassedStage : isPassedStage
? "text-green-600" ? 'text-green-600'
: "text-gray-500" : 'text-gray-500'
}`} }`}
> >
{getOpportunityStageText(stage)} {getOpportunityStageText(stage)}
</span> </span>
</div> </div>
); )
})} })}
</div> </div>
</div> </div>
@ -365,7 +324,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
</div> </div>
</div> </div>
</div> </div>
); )
}; }
export default OpportunityDetails; export default OpportunityDetails

View file

@ -1,21 +1,22 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react'
import { FaTimes, FaDollarSign, FaFileAlt } from "react-icons/fa"; import { FaTimes, FaDollarSign, FaFileAlt } from 'react-icons/fa'
import { import {
CrmOpportunity, CrmOpportunity,
OpportunityStageEnum, OpportunityStageEnum,
LeadSourceEnum, LeadSourceEnum,
OpportunityStatusEnum, OpportunityStatusEnum,
} from "../../../types/crm"; } from '../../../types/crm'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { mockEmployees } from "../../../mocks/mockEmployees"; import { mockEmployees } from '../../../mocks/mockEmployees'
import { BusinessParty } from "../../../types/common"; import { BusinessParty } from '../../../types/common'
import { getOpportunityLeadSourceText, getOpportunityStageText } from '@/utils/erp'
interface OpportunityFormProps { interface OpportunityFormProps {
isOpen: boolean; isOpen: boolean
onClose: () => void; onClose: () => void
onSave: (opportunity: CrmOpportunity) => void; onSave: (opportunity: CrmOpportunity) => void
opportunity?: CrmOpportunity | null; opportunity?: CrmOpportunity | null
mode: "create" | "edit"; mode: 'create' | 'edit'
} }
const OpportunityForm: React.FC<OpportunityFormProps> = ({ const OpportunityForm: React.FC<OpportunityFormProps> = ({
@ -26,122 +27,118 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
mode, mode,
}) => { }) => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
id: "", id: '',
opportunityNumber: "", opportunityNumber: '',
title: "", title: '',
description: "", description: '',
customerId: "", customerId: '',
contactId: "", contactId: '',
stage: OpportunityStageEnum.Qualification, stage: OpportunityStageEnum.Qualification,
probability: 10, probability: 10,
estimatedValue: 0, estimatedValue: 0,
currency: "TRY", currency: 'TRY',
expectedCloseDate: "", expectedCloseDate: '',
assignedTo: "", assignedTo: '',
teamId: "", teamId: '',
leadSource: LeadSourceEnum.Website, leadSource: LeadSourceEnum.Website,
campaignId: "", campaignId: '',
}); })
const [customers] = useState<BusinessParty[]>(mockBusinessParties); const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({})
useEffect(() => { useEffect(() => {
if (opportunity && mode === "edit") { if (opportunity && mode === 'edit') {
setFormData({ setFormData({
id: opportunity.id, id: opportunity.id,
opportunityNumber: opportunity.opportunityNumber, opportunityNumber: opportunity.opportunityNumber,
title: opportunity.title, title: opportunity.title,
description: opportunity.description || "", description: opportunity.description || '',
customerId: opportunity.customerId, customerId: opportunity.customerId,
contactId: opportunity.contactId || "", contactId: opportunity.contactId || '',
stage: opportunity.stage, stage: opportunity.stage,
probability: opportunity.probability, probability: opportunity.probability,
estimatedValue: opportunity.estimatedValue, estimatedValue: opportunity.estimatedValue,
currency: opportunity.currency, currency: opportunity.currency,
expectedCloseDate: opportunity.expectedCloseDate expectedCloseDate: opportunity.expectedCloseDate.toISOString().split('T')[0],
.toISOString()
.split("T")[0],
assignedTo: opportunity.assignedTo, assignedTo: opportunity.assignedTo,
teamId: opportunity.teamId || "", teamId: opportunity.teamId || '',
leadSource: opportunity.leadSource, leadSource: opportunity.leadSource,
campaignId: opportunity.campaignId || "", campaignId: opportunity.campaignId || '',
}); })
} else if (mode === "create") { } else if (mode === 'create') {
// Generate new opportunity number // Generate new opportunity number
const newNumber = `OPP-${Date.now().toString().slice(-6)}`; const newNumber = `OPP-${Date.now().toString().slice(-6)}`
setFormData({ setFormData({
id: `opp_${Date.now()}`, id: `opp_${Date.now()}`,
opportunityNumber: newNumber, opportunityNumber: newNumber,
title: "", title: '',
description: "", description: '',
customerId: "", customerId: '',
contactId: "", contactId: '',
stage: OpportunityStageEnum.Qualification, stage: OpportunityStageEnum.Qualification,
probability: 10, probability: 10,
estimatedValue: 0, estimatedValue: 0,
currency: "TRY", currency: 'TRY',
expectedCloseDate: "", expectedCloseDate: '',
assignedTo: "Mevcut Kullanıcı", assignedTo: 'Mevcut Kullanıcı',
teamId: "", teamId: '',
leadSource: LeadSourceEnum.Website, leadSource: LeadSourceEnum.Website,
campaignId: "", campaignId: '',
}); })
} }
}, [opportunity, mode, isOpen]); }, [opportunity, mode, isOpen])
const handleInputChange = ( const handleInputChange = (
e: React.ChangeEvent< e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>
) => { ) => {
const { name, value } = e.target; const { name, value } = e.target
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[name]: value, [name]: value,
})); }))
// Clear error when user starts typing // Clear error when user starts typing
if (errors[name]) { if (errors[name]) {
setErrors((prev) => ({ setErrors((prev) => ({
...prev, ...prev,
[name]: "", [name]: '',
})); }))
}
} }
};
const validateForm = () => { const validateForm = () => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {}
if (!formData.title.trim()) { if (!formData.title.trim()) {
newErrors.title = "Fırsat başlığı zorunludur"; newErrors.title = 'Fırsat başlığı zorunludur'
} }
if (!formData.customerId) { if (!formData.customerId) {
newErrors.customerId = "Müşteri seçimi zorunludur"; newErrors.customerId = 'Müşteri seçimi zorunludur'
} }
if (formData.estimatedValue <= 0) { if (formData.estimatedValue <= 0) {
newErrors.estimatedValue = "Tahmini değer 0'dan büyük olmalıdır"; newErrors.estimatedValue = "Tahmini değer 0'dan büyük olmalıdır"
} }
if (!formData.expectedCloseDate) { if (!formData.expectedCloseDate) {
newErrors.expectedCloseDate = "Beklenen kapanış tarihi zorunludur"; newErrors.expectedCloseDate = 'Beklenen kapanış tarihi zorunludur'
} }
if (formData.probability < 0 || formData.probability > 100) { if (formData.probability < 0 || formData.probability > 100) {
newErrors.probability = "Olasılık 0-100 arasında olmalıdır"; newErrors.probability = 'Olasılık 0-100 arasında olmalıdır'
} }
setErrors(newErrors); setErrors(newErrors)
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0
}; }
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault()
if (!validateForm()) { if (!validateForm()) {
return; return
} }
const opportunityData: CrmOpportunity = { const opportunityData: CrmOpportunity = {
@ -171,38 +168,13 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
competitors: [], competitors: [],
creationTime: opportunity?.creationTime || new Date(), creationTime: opportunity?.creationTime || new Date(),
lastModificationTime: new Date(), lastModificationTime: new Date(),
}; }
onSave(opportunityData); onSave(opportunityData)
onClose(); onClose()
}; }
const getStageLabel = (stage: OpportunityStageEnum) => { if (!isOpen) return null
const stageLabels = {
[OpportunityStageEnum.Qualification]: "Nitelendirme",
[OpportunityStageEnum.NeedsAnalysis]: "İhtiyaç Analizi",
[OpportunityStageEnum.Proposal]: "Teklif",
[OpportunityStageEnum.Negotiation]: "Müzakere",
[OpportunityStageEnum.ClosedWon]: "Kazanıldı",
[OpportunityStageEnum.ClosedLost]: "Kaybedildi",
};
return stageLabels[stage];
};
const getLeadSourceLabel = (source: LeadSourceEnum) => {
const sourceLabels: Record<LeadSourceEnum, string> = {
[LeadSourceEnum.Website]: "Web Sitesi",
[LeadSourceEnum.Referral]: "Referans",
[LeadSourceEnum.Campaign]: "Kampanya",
[LeadSourceEnum.Trade_Show]: "Fuar",
[LeadSourceEnum.Cold_Call]: "Soğuk Arama",
[LeadSourceEnum.Social_Media]: "Sosyal Medya",
[LeadSourceEnum.Partner]: "Partner",
};
return sourceLabels[source];
};
if (!isOpen) return null;
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
@ -210,12 +182,9 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b"> <div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">
{mode === "create" ? "Yeni Fırsat Oluştur" : "Fırsat Düzenle"} {mode === 'create' ? 'Yeni Fırsat Oluştur' : 'Fırsat Düzenle'}
</h2> </h2>
<button <button onClick={onClose} className="text-gray-400 hover:text-gray-600">
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<FaTimes className="w-5 h-5" /> <FaTimes className="w-5 h-5" />
</button> </button>
</div> </div>
@ -254,19 +223,15 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
value={formData.title} value={formData.title}
onChange={handleInputChange} 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 ${ 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" errors.title ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="Fırsat başlığını girin" placeholder="Fırsat başlığını girin"
/> />
{errors.title && ( {errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
<p className="text-red-500 text-sm mt-1">{errors.title}</p>
)}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">ıklama</label>
ıklama
</label>
<textarea <textarea
name="description" name="description"
value={formData.description} value={formData.description}
@ -278,15 +243,13 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Müşteri *</label>
Müşteri *
</label>
<select <select
name="customerId" name="customerId"
value={formData.customerId} value={formData.customerId}
onChange={handleInputChange} 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 ${ 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" errors.customerId ? 'border-red-500' : 'border-gray-300'
}`} }`}
> >
<option value="">Müşteri seçin</option> <option value="">Müşteri seçin</option>
@ -297,18 +260,14 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
))} ))}
</select> </select>
{errors.customerId && ( {errors.customerId && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">{errors.customerId}</p>
{errors.customerId}
</p>
)} )}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Sorumlu</label>
Sorumlu
</label>
<select <select
value={formData.assignedTo || ""} value={formData.assignedTo || ''}
onChange={handleInputChange} 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" 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"
> >
@ -330,9 +289,7 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
</h3> </h3>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Aşama</label>
Aşama
</label>
<select <select
name="stage" name="stage"
value={formData.stage} value={formData.stage}
@ -341,7 +298,7 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
> >
{Object.values(OpportunityStageEnum).map((stage) => ( {Object.values(OpportunityStageEnum).map((stage) => (
<option key={stage} value={stage}> <option key={stage} value={stage}>
{getStageLabel(stage)} {getOpportunityStageText(stage)}
</option> </option>
))} ))}
</select> </select>
@ -359,13 +316,11 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
min="0" min="0"
max="100" 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 ${ 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 ? 'border-red-500' : 'border-gray-300'
}`} }`}
/> />
{errors.probability && ( {errors.probability && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">{errors.probability}</p>
{errors.probability}
</p>
)} )}
</div> </div>
@ -381,21 +336,17 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
min="0" min="0"
step="0.01" 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 ${ 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" errors.estimatedValue ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="0.00" placeholder="0.00"
/> />
{errors.estimatedValue && ( {errors.estimatedValue && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">{errors.estimatedValue}</p>
{errors.estimatedValue}
</p>
)} )}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Para Birimi</label>
Para Birimi
</label>
<select <select
name="currency" name="currency"
value={formData.currency} value={formData.currency}
@ -418,22 +369,16 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
value={formData.expectedCloseDate} value={formData.expectedCloseDate}
onChange={handleInputChange} 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 ${ 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 errors.expectedCloseDate ? 'border-red-500' : 'border-gray-300'
? "border-red-500"
: "border-gray-300"
}`} }`}
/> />
{errors.expectedCloseDate && ( {errors.expectedCloseDate && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">{errors.expectedCloseDate}</p>
{errors.expectedCloseDate}
</p>
)} )}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Kaynak</label>
Kaynak
</label>
<select <select
name="leadSource" name="leadSource"
value={formData.leadSource} value={formData.leadSource}
@ -442,7 +387,7 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
> >
{Object.values(LeadSourceEnum).map((source) => ( {Object.values(LeadSourceEnum).map((source) => (
<option key={source} value={source}> <option key={source} value={source}>
{getLeadSourceLabel(source)} {getOpportunityLeadSourceText(source)}
</option> </option>
))} ))}
</select> </select>
@ -463,13 +408,13 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
type="submit" type="submit"
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" 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"} {mode === 'create' ? 'Oluştur' : 'Güncelle'}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
); )
}; }
export default OpportunityForm; export default OpportunityForm

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { import {
FaBullseye, FaBullseye,
FaPlus, FaPlus,
@ -9,86 +9,80 @@ import {
FaArrowUp, FaArrowUp,
FaUser, FaUser,
FaBuilding, FaBuilding,
} from "react-icons/fa"; } from 'react-icons/fa'
import { CrmOpportunity, OpportunityStageEnum } from "../../../types/crm"; import { CrmOpportunity, OpportunityStageEnum } from '../../../types/crm'
import DataTable, { Column } from "../../../components/common/DataTable"; import DataTable, { Column } from '../../../components/common/DataTable'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { mockOpportunities } from "../../../mocks/mockOpportunities"; import { mockOpportunities } from '../../../mocks/mockOpportunities'
import OpportunityForm from "./OpportunityForm"; import OpportunityForm from './OpportunityForm'
import OpportunityDetails from "./OpportunityDetails"; import OpportunityDetails from './OpportunityDetails'
import { BusinessParty } from "../../../types/common"; import { BusinessParty } from '../../../types/common'
import Widget from "../../../components/common/Widget"; import Widget from '../../../components/common/Widget'
import { import {
getOpportunityProbabilityColor, getOpportunityProbabilityColor,
getOpportunityStageColor, getOpportunityStageColor,
getOpportunityStageText, getOpportunityStageText,
} from "../../../utils/erp"; } from '../../../utils/erp'
import { Container } from '@/components/shared'
// Mock data - replace with actual data fetching
const OpportunityManagement: React.FC = () => { const OpportunityManagement: React.FC = () => {
const [opportunities, setOpportunities] = const [opportunities, setOpportunities] = useState<CrmOpportunity[]>(mockOpportunities)
useState<CrmOpportunity[]>(mockOpportunities); const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [customers] = useState<BusinessParty[]>(mockBusinessParties); const [searchTerm, setSearchTerm] = useState('')
const [searchTerm, setSearchTerm] = useState(""); const [selectedStage, setSelectedStage] = useState<OpportunityStageEnum | 'all'>('all')
const [selectedStage, setSelectedStage] = useState< const [selectedCustomer, setSelectedCustomer] = useState<string>('all')
OpportunityStageEnum | "all"
>("all");
const [selectedCustomer, setSelectedCustomer] = useState<string>("all");
// Modal states // Modal states
const [isFormOpen, setIsFormOpen] = useState(false); const [isFormOpen, setIsFormOpen] = useState(false)
const [isDetailsOpen, setIsDetailsOpen] = useState(false); const [isDetailsOpen, setIsDetailsOpen] = useState(false)
const [selectedOpportunity, setSelectedOpportunity] = const [selectedOpportunity, setSelectedOpportunity] = useState<CrmOpportunity | null>(null)
useState<CrmOpportunity | null>(null); const [formMode, setFormMode] = useState<'create' | 'edit'>('create')
const [formMode, setFormMode] = useState<"create" | "edit">("create");
const handleAdd = () => { const handleAdd = () => {
setSelectedOpportunity(null); setSelectedOpportunity(null)
setFormMode("create"); setFormMode('create')
setIsFormOpen(true); setIsFormOpen(true)
}; }
const handleEdit = (opportunity: CrmOpportunity) => { const handleEdit = (opportunity: CrmOpportunity) => {
setSelectedOpportunity(opportunity); setSelectedOpportunity(opportunity)
setFormMode("edit"); setFormMode('edit')
setIsFormOpen(true); setIsFormOpen(true)
}; }
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
if (confirm("Bu fırsatı silmek istediğinizden emin misiniz?")) { if (confirm('Bu fırsatı silmek istediğinizden emin misiniz?')) {
setOpportunities((prev) => prev.filter((opp) => opp.id !== id)); setOpportunities((prev) => prev.filter((opp) => opp.id !== id))
}
} }
};
const handleViewDetails = (opportunity: CrmOpportunity) => { const handleViewDetails = (opportunity: CrmOpportunity) => {
setSelectedOpportunity(opportunity); setSelectedOpportunity(opportunity)
setIsDetailsOpen(true); setIsDetailsOpen(true)
}; }
const handleSaveOpportunity = (opportunity: CrmOpportunity) => { const handleSaveOpportunity = (opportunity: CrmOpportunity) => {
if (formMode === "create") { if (formMode === 'create') {
setOpportunities((prev) => [...prev, opportunity]); setOpportunities((prev) => [...prev, opportunity])
} else { } else {
setOpportunities((prev) => setOpportunities((prev) => prev.map((opp) => (opp.id === opportunity.id ? opportunity : opp)))
prev.map((opp) => (opp.id === opportunity.id ? opportunity : opp)) }
);
} }
};
const handleCloseForm = () => { const handleCloseForm = () => {
setIsFormOpen(false); setIsFormOpen(false)
setSelectedOpportunity(null); setSelectedOpportunity(null)
}; }
const handleCloseDetails = () => { const handleCloseDetails = () => {
setIsDetailsOpen(false); setIsDetailsOpen(false)
setSelectedOpportunity(null); setSelectedOpportunity(null)
}; }
const handleEditFromDetails = (opportunity: CrmOpportunity) => { const handleEditFromDetails = (opportunity: CrmOpportunity) => {
setIsDetailsOpen(false); setIsDetailsOpen(false)
handleEdit(opportunity); handleEdit(opportunity)
}; }
const filteredOpportunities = opportunities.filter((opportunity) => { const filteredOpportunities = opportunities.filter((opportunity) => {
if ( if (
@ -96,56 +90,51 @@ const OpportunityManagement: React.FC = () => {
!opportunity.title.toLowerCase().includes(searchTerm.toLowerCase()) && !opportunity.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
!opportunity.description?.toLowerCase().includes(searchTerm.toLowerCase()) !opportunity.description?.toLowerCase().includes(searchTerm.toLowerCase())
) { ) {
return false; return false
} }
if (selectedStage !== "all" && opportunity.stage !== selectedStage) { if (selectedStage !== 'all' && opportunity.stage !== selectedStage) {
return false; return false
} }
if ( if (selectedCustomer !== 'all' && opportunity.customerId !== selectedCustomer) {
selectedCustomer !== "all" && return false
opportunity.customerId !== selectedCustomer
) {
return false;
} }
return true; return true
}); })
const columns: Column<CrmOpportunity>[] = [ const columns: Column<CrmOpportunity>[] = [
{ {
key: "title", key: 'title',
header: "Fırsat Başlığı", header: 'Fırsat Başlığı',
sortable: true, sortable: true,
render: (opportunity: CrmOpportunity) => ( render: (opportunity: CrmOpportunity) => (
<div> <div>
<div className="font-medium text-gray-900">{opportunity.title}</div> <div className="font-medium text-gray-900">{opportunity.title}</div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">{opportunity.opportunityNumber}</div>
{opportunity.opportunityNumber}
</div>
</div> </div>
), ),
}, },
{ {
key: "customer", key: 'customer',
header: "Müşteri", header: 'Müşteri',
render: (opportunity: CrmOpportunity) => { render: (opportunity: CrmOpportunity) => {
const customer = customers.find((c) => c.id === opportunity.customerId); const customer = customers.find((c) => c.id === opportunity.customerId)
return customer ? ( return customer ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-400" /> <FaBuilding className="w-4 h-4 text-gray-400" />
<span>{customer.name}</span> <span>{customer.name}</span>
</div> </div>
) : ( ) : (
"-" '-'
); )
}, },
}, },
{ {
key: "stage", key: 'stage',
header: "Aşama", header: 'Aşama',
render: (opportunity: CrmOpportunity) => ( render: (opportunity: CrmOpportunity) => (
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${getOpportunityStageColor( className={`px-2 py-1 text-xs font-medium rounded-full ${getOpportunityStageColor(
opportunity.stage opportunity.stage,
)}`} )}`}
> >
{getOpportunityStageText(opportunity.stage)} {getOpportunityStageText(opportunity.stage)}
@ -153,27 +142,23 @@ const OpportunityManagement: React.FC = () => {
), ),
}, },
{ {
key: "estimatedValue", key: 'estimatedValue',
header: "Tahmini Değer", header: 'Tahmini Değer',
render: (opportunity: CrmOpportunity) => ( render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-400" /> <FaDollarSign className="w-4 h-4 text-gray-400" />
<span className="font-medium"> <span className="font-medium">{opportunity.estimatedValue.toLocaleString()}</span>
{opportunity.estimatedValue.toLocaleString()}
</span>
</div> </div>
), ),
}, },
{ {
key: "probability", key: 'probability',
header: "Olasılık", header: 'Olasılık',
render: (opportunity: CrmOpportunity) => ( render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FaArrowUp className="w-4 h-4 text-gray-400" /> <FaArrowUp className="w-4 h-4 text-gray-400" />
<span <span
className={`font-medium ${getOpportunityProbabilityColor( className={`font-medium ${getOpportunityProbabilityColor(opportunity.probability)}`}
opportunity.probability
)}`}
> >
%{opportunity.probability} %{opportunity.probability}
</span> </span>
@ -181,22 +166,18 @@ const OpportunityManagement: React.FC = () => {
), ),
}, },
{ {
key: "expectedCloseDate", key: 'expectedCloseDate',
header: "Beklenen Kapanış", header: 'Beklenen Kapanış',
render: (opportunity: CrmOpportunity) => ( render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1 text-sm"> <div className="flex items-center gap-1 text-sm">
<FaCalendar className="w-3 h-3 text-gray-400" /> <FaCalendar className="w-3 h-3 text-gray-400" />
<span> <span>{new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}</span>
{new Date(opportunity.expectedCloseDate).toLocaleDateString(
"tr-TR"
)}
</span>
</div> </div>
), ),
}, },
{ {
key: "assignedTo", key: 'assignedTo',
header: "Sorumlu", header: 'Sorumlu',
render: (opportunity: CrmOpportunity) => ( render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FaUser className="w-4 h-4 text-gray-400" /> <FaUser className="w-4 h-4 text-gray-400" />
@ -205,8 +186,8 @@ const OpportunityManagement: React.FC = () => {
), ),
}, },
{ {
key: "actions", key: 'actions',
header: "İşlemler", header: 'İşlemler',
render: (opportunity: CrmOpportunity) => ( render: (opportunity: CrmOpportunity) => (
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@ -233,45 +214,35 @@ const OpportunityManagement: React.FC = () => {
</div> </div>
), ),
}, },
]; ]
// Calculate statistics // Calculate statistics
const totalOpportunities = opportunities.length; const totalOpportunities = opportunities.length
const activeOpportunities = opportunities.filter( const activeOpportunities = opportunities.filter(
(o) => (o) =>
o.stage !== OpportunityStageEnum.ClosedWon && o.stage !== OpportunityStageEnum.ClosedWon && o.stage !== OpportunityStageEnum.ClosedLost,
o.stage !== OpportunityStageEnum.ClosedLost ).length
).length; const totalValue = opportunities.reduce((sum, opp) => sum + opp.estimatedValue, 0)
const totalValue = opportunities.reduce(
(sum, opp) => sum + opp.estimatedValue,
0
);
const averageProbability = const averageProbability =
opportunities.reduce((sum, opp) => sum + opp.probability, 0) / opportunities.reduce((sum, opp) => sum + opp.probability, 0) / opportunities.length || 0
opportunities.length || 0;
// Stage distribution // Stage distribution
const stageDistribution = Object.values(OpportunityStageEnum).map( const stageDistribution = Object.values(OpportunityStageEnum).map((stage) => ({
(stage) => ({
stage, stage,
count: opportunities.filter((o) => o.stage === stage).length, count: opportunities.filter((o) => o.stage === stage).length,
value: opportunities value: opportunities
.filter((o) => o.stage === stage) .filter((o) => o.stage === stage)
.reduce((sum, o) => sum + o.estimatedValue, 0), .reduce((sum, o) => sum + o.estimatedValue, 0),
}) }))
);
return ( return (
<Container>
<div className="space-y-3 pt-2"> <div className="space-y-3 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-xl font-bold text-gray-900"> <h2 className="text-xl font-bold text-gray-900">Teklif & Fırsat Yönetimi</h2>
Teklif & Fırsat Yönetimi <p className="text-sm text-gray-600">Satış fırsatları ve teklif takibi</p>
</h2>
<p className="text-sm text-gray-600">
Satış fırsatları ve teklif takibi
</p>
</div> </div>
<button <button
onClick={handleAdd} onClick={handleAdd}
@ -284,19 +255,9 @@ const OpportunityManagement: React.FC = () => {
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget <Widget title="Toplam Fırsat" value={totalOpportunities} color="blue" icon="FaBullseye" />
title="Toplam Fırsat"
value={totalOpportunities}
color="blue"
icon="FaBullseye"
/>
<Widget <Widget title="Aktif Fırsat" value={activeOpportunities} color="green" icon="FaArrowUp" />
title="Aktif Fırsat"
value={activeOpportunities}
color="green"
icon="FaArrowUp"
/>
<Widget <Widget
title="Toplam Değer" title="Toplam Değer"
@ -315,25 +276,19 @@ const OpportunityManagement: React.FC = () => {
{/* Stage Pipeline */} {/* Stage Pipeline */}
<div className="bg-white rounded-lg shadow-sm border p-3"> <div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3"> <h3 className="text-sm font-semibold text-gray-900 mb-3">Satış Aşamaları</h3>
Satış Aşamaları
</h3>
<div className="grid grid-cols-2 md:grid-cols-6 gap-2"> <div className="grid grid-cols-2 md:grid-cols-6 gap-2">
{stageDistribution.map(({ stage, count, value }) => ( {stageDistribution.map(({ stage, count, value }) => (
<div key={stage} className="text-center p-2 border rounded-lg"> <div key={stage} className="text-center p-2 border rounded-lg">
<div <div
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getOpportunityStageColor( className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getOpportunityStageColor(
stage stage,
)}`} )}`}
> >
{getOpportunityStageText(stage)} {getOpportunityStageText(stage)}
</div> </div>
<div className="text-lg font-bold text-gray-900 mb-1"> <div className="text-lg font-bold text-gray-900 mb-1">{count}</div>
{count} <div className="text-xs text-gray-500">{value.toLocaleString()}</div>
</div>
<div className="text-xs text-gray-500">
{value.toLocaleString()}
</div>
</div> </div>
))} ))}
</div> </div>
@ -341,28 +296,18 @@ const OpportunityManagement: React.FC = () => {
{/* Win Rate Analysis */} {/* Win Rate Analysis */}
<div className="bg-white rounded-lg shadow-sm border p-3"> <div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3"> <h3 className="text-sm font-semibold text-gray-900 mb-3">Başarı Oranı Analizi</h3>
Başarı Oranı Analizi
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-green-600 mb-1"> <div className="text-2xl font-bold text-green-600 mb-1">
{ {opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedWon).length}
opportunities.filter(
(o) => o.stage === OpportunityStageEnum.ClosedWon
).length
}
</div> </div>
<p className="text-sm text-gray-600">Kazanılan Fırsat</p> <p className="text-sm text-gray-600">Kazanılan Fırsat</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-red-600 mb-1"> <div className="text-2xl font-bold text-red-600 mb-1">
{ {opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedLost).length}
opportunities.filter(
(o) => o.stage === OpportunityStageEnum.ClosedLost
).length
}
</div> </div>
<p className="text-sm text-gray-600">Kaybedilen Fırsat</p> <p className="text-sm text-gray-600">Kaybedilen Fırsat</p>
</div> </div>
@ -370,18 +315,16 @@ const OpportunityManagement: React.FC = () => {
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1"> <div className="text-2xl font-bold text-blue-600 mb-1">
{Math.round( {Math.round(
(opportunities.filter( (opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedWon).length /
(o) => o.stage === OpportunityStageEnum.ClosedWon
).length /
Math.max( Math.max(
opportunities.filter( opportunities.filter(
(o) => (o) =>
o.stage === OpportunityStageEnum.ClosedWon || o.stage === OpportunityStageEnum.ClosedWon ||
o.stage === OpportunityStageEnum.ClosedLost o.stage === OpportunityStageEnum.ClosedLost,
).length, ).length,
1 1,
)) * )) *
100 100,
)} )}
% %
</div> </div>
@ -404,9 +347,7 @@ const OpportunityManagement: React.FC = () => {
<select <select
value={selectedStage} value={selectedStage}
onChange={(e) => onChange={(e) => setSelectedStage(e.target.value as OpportunityStageEnum | 'all')}
setSelectedStage(e.target.value as OpportunityStageEnum | "all")
}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="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="all">Tüm Aşamalar</option> <option value="all">Tüm Aşamalar</option>
@ -439,12 +380,8 @@ const OpportunityManagement: React.FC = () => {
{filteredOpportunities.length === 0 && ( {filteredOpportunities.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<FaBullseye className="w-10 h-10 text-gray-400 mx-auto mb-3" /> <FaBullseye className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-sm font-medium text-gray-900 mb-2"> <h3 className="text-sm font-medium text-gray-900 mb-2">Fırsat bulunamadı</h3>
Fırsat bulunamadı <p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
</div> </div>
)} )}
@ -464,7 +401,8 @@ const OpportunityManagement: React.FC = () => {
opportunity={selectedOpportunity} opportunity={selectedOpportunity}
/> />
</div> </div>
); </Container>
}; )
}
export default OpportunityManagement; export default OpportunityManagement

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react'
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from 'react-router-dom'
import { import {
FaSave, FaSave,
FaTimes, FaTimes,
@ -11,31 +11,32 @@ import {
FaBuilding, FaBuilding,
FaClipboardList, FaClipboardList,
FaNotesMedical, FaNotesMedical,
} from "react-icons/fa"; } from 'react-icons/fa'
import { import {
SaleOrderStatusEnum, SaleOrderStatusEnum,
CrmSalesOrder, CrmSalesOrder,
CrmSalesOrderItem, CrmSalesOrderItem,
SaleOrderItemStatusEnum, SaleOrderItemStatusEnum,
} from "../../../types/crm"; } from '../../../types/crm'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { mockMaterials } from "../../../mocks/mockMaterials"; import { mockMaterials } from '../../../mocks/mockMaterials'
import { mockSalesOrders } from "../../../mocks/mockSalesOrders"; import { mockSalesOrders } from '../../../mocks/mockSalesOrders'
import { BusinessParty, PaymentTerms } from "../../../types/common"; import { BusinessParty, PaymentTerms } from '../../../types/common'
import { Container } from '@/components/shared'
const SalesOrderForm: React.FC = () => { const SalesOrderForm: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const { id } = useParams(); const { id } = useParams()
const isEdit = Boolean(id); const isEdit = Boolean(id)
const [customers] = useState<BusinessParty[]>(mockBusinessParties); const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [materials] = useState(mockMaterials); const [materials] = useState(mockMaterials)
// Form data state with extended fields for form handling // Form data state with extended fields for form handling
const [formData, setFormData] = useState<CrmSalesOrder>({ const [formData, setFormData] = useState<CrmSalesOrder>({
id: "", id: '',
orderNumber: "", orderNumber: '',
customerId: "", customerId: '',
customer: undefined, customer: undefined,
orderDate: new Date(), orderDate: new Date(),
requestedDeliveryDate: new Date(), requestedDeliveryDate: new Date(),
@ -45,76 +46,73 @@ const SalesOrderForm: React.FC = () => {
taxAmount: 0, taxAmount: 0,
discountAmount: 0, discountAmount: 0,
totalAmount: 0, totalAmount: 0,
currency: "TRY", currency: 'TRY',
paymentTerms: PaymentTerms.Net30, paymentTerms: PaymentTerms.Net30,
deliveryAddress: { deliveryAddress: {
street: "", street: '',
city: "", city: '',
state: "", state: '',
postalCode: "", postalCode: '',
country: "Türkiye", country: 'Türkiye',
}, },
billingAddress: { billingAddress: {
street: "", street: '',
city: "", city: '',
state: "", state: '',
postalCode: "", postalCode: '',
country: "Türkiye", country: 'Türkiye',
}, },
items: [], items: [],
deliveries: [], deliveries: [],
creationTime: new Date(), creationTime: new Date(),
lastModificationTime: new Date(), lastModificationTime: new Date(),
// Additional form fields // Additional form fields
deliveryTerms: "", deliveryTerms: '',
notes: "", notes: '',
exchangeRate: 1, exchangeRate: 1,
discountRate: 0, discountRate: 0,
taxRate: 18, taxRate: 18,
specialInstructions: "", specialInstructions: '',
}); })
const [newItem, setNewItem] = useState<Partial<CrmSalesOrderItem>>({ const [newItem, setNewItem] = useState<Partial<CrmSalesOrderItem>>({
materialId: "", materialId: '',
quantity: 1, quantity: 1,
unitPrice: 0, unitPrice: 0,
discountRate: 0, discountRate: 0,
taxRate: 18, taxRate: 18,
notes: "", notes: '',
}); })
const [showItemForm, setShowItemForm] = useState(false); const [showItemForm, setShowItemForm] = useState(false)
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({})
useEffect(() => { useEffect(() => {
if (isEdit && id) { if (isEdit && id) {
const existingOrder = mockSalesOrders.find((order) => order.id === id); const existingOrder = mockSalesOrders.find((order) => order.id === id)
if (existingOrder) { if (existingOrder) {
setFormData(existingOrder); setFormData(existingOrder)
} }
// Load existing sales order data // Load existing sales order data
// This would typically come from an API call // This would typically come from an API call
console.log("Loading sales order:", id); console.log('Loading sales order:', id)
} }
}, [id, isEdit]); }, [id, isEdit])
useEffect(() => { useEffect(() => {
const calculateTotals = () => { const calculateTotals = () => {
if (!formData.items) return; if (!formData.items) return
const subtotal = formData.items.reduce( const subtotal = formData.items.reduce((sum: number, item: CrmSalesOrderItem) => {
(sum: number, item: CrmSalesOrderItem) => { const itemTotal = item.quantity * item.unitPrice
const itemTotal = item.quantity * item.unitPrice; const itemDiscount = itemTotal * ((item.discountRate || 0) / 100)
const itemDiscount = itemTotal * ((item.discountRate || 0) / 100); return sum + (itemTotal - itemDiscount)
return sum + (itemTotal - itemDiscount); }, 0)
},
0
);
const discountAmount = subtotal * ((formData.discountRate || 0) / 100); const discountAmount = subtotal * ((formData.discountRate || 0) / 100)
const taxableAmount = subtotal - discountAmount; const taxableAmount = subtotal - discountAmount
const taxAmount = taxableAmount * ((formData.taxRate || 18) / 100); const taxAmount = taxableAmount * ((formData.taxRate || 18) / 100)
const totalAmount = taxableAmount + taxAmount; const totalAmount = taxableAmount + taxAmount
setFormData((prev: CrmSalesOrder) => ({ setFormData((prev: CrmSalesOrder) => ({
...prev, ...prev,
@ -122,53 +120,47 @@ const SalesOrderForm: React.FC = () => {
discountAmount, discountAmount,
taxAmount, taxAmount,
totalAmount, totalAmount,
})); }))
}; }
calculateTotals(); calculateTotals()
}, [formData.items, formData.discountRate, formData.taxRate]); }, [formData.items, formData.discountRate, formData.taxRate])
const handleInputChange = ( const handleInputChange = (
field: string, field: string,
value: value: string | number | SaleOrderStatusEnum | PaymentTerms | Date | undefined,
| string
| number
| SaleOrderStatusEnum
| PaymentTerms
| Date
| undefined
) => { ) => {
setFormData((prev: CrmSalesOrder) => ({ setFormData((prev: CrmSalesOrder) => ({
...prev, ...prev,
[field]: value, [field]: value,
})); }))
if (errors[field]) { if (errors[field]) {
setErrors((prev: Record<string, string>) => ({ setErrors((prev: Record<string, string>) => ({
...prev, ...prev,
[field]: "", [field]: '',
})); }))
}
} }
};
const handleAddItem = () => { const handleAddItem = () => {
if (!newItem.materialId || !newItem.quantity || !newItem.unitPrice) { if (!newItem.materialId || !newItem.quantity || !newItem.unitPrice) {
alert("Lütfen tüm item bilgilerini doldurun!"); alert('Lütfen tüm item bilgilerini doldurun!')
return; return
} }
const material = materials.find((m) => m.id === newItem.materialId); const material = materials.find((m) => m.id === newItem.materialId)
if (!material) return; if (!material) return
const itemTotal = (newItem.quantity || 0) * (newItem.unitPrice || 0); const itemTotal = (newItem.quantity || 0) * (newItem.unitPrice || 0)
const itemDiscount = itemTotal * ((newItem.discountRate || 0) / 100); const itemDiscount = itemTotal * ((newItem.discountRate || 0) / 100)
const itemTaxableAmount = itemTotal - itemDiscount; const itemTaxableAmount = itemTotal - itemDiscount
const itemTaxAmount = itemTaxableAmount * ((newItem.taxRate || 0) / 100); const itemTaxAmount = itemTaxableAmount * ((newItem.taxRate || 0) / 100)
const itemTotalAmount = itemTaxableAmount + itemTaxAmount; const itemTotalAmount = itemTaxableAmount + itemTaxAmount
const item: CrmSalesOrderItem = { const item: CrmSalesOrderItem = {
id: `item_${Date.now()}`, id: `item_${Date.now()}`,
orderId: formData.id || "", orderId: formData.id || '',
materialId: newItem.materialId!, materialId: newItem.materialId!,
material: material, material: material,
description: material.name, description: material.name,
@ -185,89 +177,88 @@ const SalesOrderForm: React.FC = () => {
requestedDate: formData.requestedDeliveryDate, requestedDate: formData.requestedDeliveryDate,
confirmedDate: undefined, confirmedDate: undefined,
status: SaleOrderItemStatusEnum.Pending, status: SaleOrderItemStatusEnum.Pending,
notes: newItem.notes || "", notes: newItem.notes || '',
}; }
setFormData((prev: CrmSalesOrder) => ({ setFormData((prev: CrmSalesOrder) => ({
...prev, ...prev,
items: [...prev.items, item], items: [...prev.items, item],
})); }))
setNewItem({ setNewItem({
materialId: "", materialId: '',
quantity: 1, quantity: 1,
unitPrice: 0, unitPrice: 0,
discountRate: 0, discountRate: 0,
taxRate: 18, taxRate: 18,
notes: "", notes: '',
}); })
setShowItemForm(false); setShowItemForm(false)
}; }
const handleRemoveItem = (itemId: string) => { const handleRemoveItem = (itemId: string) => {
setFormData((prev: CrmSalesOrder) => ({ setFormData((prev: CrmSalesOrder) => ({
...prev, ...prev,
items: prev.items.filter((item: CrmSalesOrderItem) => item.id !== itemId), items: prev.items.filter((item: CrmSalesOrderItem) => item.id !== itemId),
})); }))
}; }
const validateForm = (): boolean => { const validateForm = (): boolean => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {}
if (!formData.orderNumber) { if (!formData.orderNumber) {
newErrors.orderNumber = "Sipariş numarası gerekli"; newErrors.orderNumber = 'Sipariş numarası gerekli'
} }
if (!formData.customerId) { if (!formData.customerId) {
newErrors.customerId = "Müşteri seçimi gerekli"; newErrors.customerId = 'Müşteri seçimi gerekli'
} }
if (!formData.orderDate) { if (!formData.orderDate) {
newErrors.orderDate = "Sipariş tarihi gerekli"; newErrors.orderDate = 'Sipariş tarihi gerekli'
} }
if (!formData.items || formData.items.length === 0) { if (!formData.items || formData.items.length === 0) {
newErrors.items = "En az bir ürün eklemelisiniz"; newErrors.items = 'En az bir ürün eklemelisiniz'
} }
setErrors(newErrors); setErrors(newErrors)
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0
}; }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault()
if (!validateForm()) { if (!validateForm()) {
return; return
} }
try { try {
// Here you would make an API call to save the sales order // Here you would make an API call to save the sales order
console.log("Saving sales order:", formData); console.log('Saving sales order:', formData)
// Navigate back to sales orders list // Navigate back to sales orders list
navigate("/admin/crm/sales-orders"); navigate('/admin/crm/sales-orders')
} catch (error) { } catch (error) {
console.error("Error saving sales order:", error); console.error('Error saving sales order:', error)
alert("Sipariş kaydedilirken bir hata oluştu!"); alert('Sipariş kaydedilirken bir hata oluştu!')
}
} }
};
const selectedCustomer = customers.find((c) => c.id === formData.customerId); const selectedCustomer = customers.find((c) => c.id === formData.customerId)
return ( return (
<Container>
<div className="space-y-6 pt-2"> <div className="space-y-6 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-xl font-bold text-gray-900"> <h2 className="text-xl font-bold text-gray-900">
{isEdit ? "Satış Siparişi Düzenle" : "Yeni Satış Siparişi"} {isEdit ? 'Satış Siparişi Düzenle' : 'Yeni Satış Siparişi'}
</h2> </h2>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">Müşteri siparişi oluştur ve yönet</p>
Müşteri siparişi oluştur ve yönet
</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
type="button" type="button"
onClick={() => navigate("/admin/crm/sales-orders")} onClick={() => navigate('/admin/crm/sales-orders')}
className="flex items-center gap-2 px-3 py-1.5 text-sm border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors" className="flex items-center gap-2 px-3 py-1.5 text-sm border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors"
> >
<FaTimes className="w-4 h-4" /> <FaTimes className="w-4 h-4" />
@ -288,9 +279,7 @@ const SalesOrderForm: React.FC = () => {
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<FaShoppingCart className="w-5 h-5 text-blue-600" /> <FaShoppingCart className="w-5 h-5 text-blue-600" />
<h3 className="text-base font-semibold text-gray-900"> <h3 className="text-base font-semibold text-gray-900">Sipariş Bilgileri</h3>
Sipariş Bilgileri
</h3>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3"> <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
@ -301,18 +290,14 @@ const SalesOrderForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.orderNumber} value={formData.orderNumber}
onChange={(e) => onChange={(e) => handleInputChange('orderNumber', e.target.value)}
handleInputChange("orderNumber", e.target.value)
}
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.orderNumber ? "border-red-500" : "border-gray-300" errors.orderNumber ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="ORD-2024-001" placeholder="ORD-2024-001"
/> />
{errors.orderNumber && ( {errors.orderNumber && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">{errors.orderNumber}</p>
{errors.orderNumber}
</p>
)} )}
</div> </div>
@ -322,12 +307,10 @@ const SalesOrderForm: React.FC = () => {
</label> </label>
<input <input
type="date" type="date"
value={formData.orderDate.toISOString().split("T")[0]} value={formData.orderDate.toISOString().split('T')[0]}
onChange={(e) => onChange={(e) => handleInputChange('orderDate', new Date(e.target.value))}
handleInputChange("orderDate", new Date(e.target.value))
}
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.orderDate ? "border-red-500" : "border-gray-300" errors.orderDate ? 'border-red-500' : 'border-gray-300'
}`} }`}
/> />
{errors.orderDate && ( {errors.orderDate && (
@ -336,32 +319,21 @@ const SalesOrderForm: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
Durum
</label>
<select <select
value={formData.status} value={formData.status}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('status', e.target.value as SaleOrderStatusEnum)
"status",
e.target.value as SaleOrderStatusEnum
)
} }
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" 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={SaleOrderStatusEnum.Draft}>Taslak</option> <option value={SaleOrderStatusEnum.Draft}>Taslak</option>
<option value={SaleOrderStatusEnum.Confirmed}>Onaylandı</option> <option value={SaleOrderStatusEnum.Confirmed}>Onaylandı</option>
<option value={SaleOrderStatusEnum.InProduction}> <option value={SaleOrderStatusEnum.InProduction}>Üretimde</option>
Üretimde
</option>
<option value={SaleOrderStatusEnum.Ready}>Hazır</option> <option value={SaleOrderStatusEnum.Ready}>Hazır</option>
<option value={SaleOrderStatusEnum.Shipped}>Kargoda</option> <option value={SaleOrderStatusEnum.Shipped}>Kargoda</option>
<option value={SaleOrderStatusEnum.Delivered}> <option value={SaleOrderStatusEnum.Delivered}>Teslim Edildi</option>
Teslim Edildi <option value={SaleOrderStatusEnum.Cancelled}>İptal Edildi</option>
</option>
<option value={SaleOrderStatusEnum.Cancelled}>
İptal Edildi
</option>
</select> </select>
</div> </div>
</div> </div>
@ -371,23 +343,17 @@ const SalesOrderForm: React.FC = () => {
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<FaBuilding className="w-5 h-5 text-blue-600" /> <FaBuilding className="w-5 h-5 text-blue-600" />
<h3 className="text-base font-semibold text-gray-900"> <h3 className="text-base font-semibold text-gray-900">Müşteri Bilgileri</h3>
Müşteri Bilgileri
</h3>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Müşteri *</label>
Müşteri *
</label>
<select <select
value={formData.customerId} value={formData.customerId}
onChange={(e) => onChange={(e) => handleInputChange('customerId', e.target.value)}
handleInputChange("customerId", e.target.value)
}
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ 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" errors.customerId ? 'border-red-500' : 'border-gray-300'
}`} }`}
> >
<option value="">Müşteri seçin...</option> <option value="">Müşteri seçin...</option>
@ -416,20 +382,16 @@ const SalesOrderForm: React.FC = () => {
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Telefon</label>
Telefon
</label>
<input <input
type="text" type="text"
value={selectedCustomer.primaryContact?.phone || ""} value={selectedCustomer.primaryContact?.phone || ''}
disabled disabled
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md bg-gray-50" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md bg-gray-50"
/> />
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Adres</label>
Adres
</label>
<textarea <textarea
value={`${selectedCustomer.address?.street}, ${selectedCustomer.address?.city}, ${selectedCustomer.address?.country}`} value={`${selectedCustomer.address?.street}, ${selectedCustomer.address?.city}, ${selectedCustomer.address?.country}`}
disabled disabled
@ -446,9 +408,7 @@ const SalesOrderForm: React.FC = () => {
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<FaCalendar className="w-5 h-5 text-blue-600" /> <FaCalendar className="w-5 h-5 text-blue-600" />
<h3 className="text-base font-semibold text-gray-900"> <h3 className="text-base font-semibold text-gray-900">Teslimat Bilgileri</h3>
Teslimat Bilgileri
</h3>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
@ -458,14 +418,9 @@ const SalesOrderForm: React.FC = () => {
</label> </label>
<input <input
type="date" type="date"
value={ value={formData.requestedDeliveryDate.toISOString().split('T')[0]}
formData.requestedDeliveryDate.toISOString().split("T")[0]
}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('requestedDeliveryDate', new Date(e.target.value))
"requestedDeliveryDate",
new Date(e.target.value)
)
} }
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" 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"
/> />
@ -477,14 +432,11 @@ const SalesOrderForm: React.FC = () => {
</label> </label>
<input <input
type="date" type="date"
value={ value={formData.confirmedDeliveryDate?.toISOString().split('T')[0] || ''}
formData.confirmedDeliveryDate?.toISOString().split("T")[0] ||
""
}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange(
"confirmedDeliveryDate", 'confirmedDeliveryDate',
e.target.value ? new Date(e.target.value) : undefined e.target.value ? new Date(e.target.value) : undefined,
) )
} }
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" 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"
@ -498,9 +450,7 @@ const SalesOrderForm: React.FC = () => {
<input <input
type="text" type="text"
value={formData.deliveryTerms} value={formData.deliveryTerms}
onChange={(e) => onChange={(e) => handleInputChange('deliveryTerms', e.target.value)}
handleInputChange("deliveryTerms", e.target.value)
}
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" 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="EXW, FOB, CIF, vb." placeholder="EXW, FOB, CIF, vb."
/> />
@ -513,9 +463,7 @@ const SalesOrderForm: React.FC = () => {
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaClipboardList className="w-5 h-5 text-blue-600" /> <FaClipboardList className="w-5 h-5 text-blue-600" />
<h3 className="text-base font-semibold text-gray-900"> <h3 className="text-base font-semibold text-gray-900">Sipariş Kalemleri</h3>
Sipariş Kalemleri
</h3>
</div> </div>
<button <button
type="button" type="button"
@ -527,20 +475,16 @@ const SalesOrderForm: React.FC = () => {
</button> </button>
</div> </div>
{errors.items && ( {errors.items && <p className="text-red-500 text-sm mb-4">{errors.items}</p>}
<p className="text-red-500 text-sm mb-4">{errors.items}</p>
)}
{/* Add Item Form */} {/* Add Item Form */}
{showItemForm && ( {showItemForm && (
<div className="bg-gray-50 p-3 rounded-lg mb-3"> <div className="bg-gray-50 p-3 rounded-lg mb-3">
<div className="grid grid-cols-1 md:grid-cols-6 gap-3"> <div className="grid grid-cols-1 md:grid-cols-6 gap-3">
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Malzeme</label>
Malzeme
</label>
<select <select
value={newItem.materialId || ""} value={newItem.materialId || ''}
onChange={(e) => onChange={(e) =>
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({ setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
...prev, ...prev,
@ -559,14 +503,12 @@ const SalesOrderForm: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Miktar</label>
Miktar
</label>
<input <input
type="number" type="number"
min="0" min="0"
step="0.01" step="0.01"
value={newItem.quantity || ""} value={newItem.quantity || ''}
onChange={(e) => onChange={(e) =>
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({ setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
...prev, ...prev,
@ -585,7 +527,7 @@ const SalesOrderForm: React.FC = () => {
type="number" type="number"
min="0" min="0"
step="0.01" step="0.01"
value={newItem.unitPrice || ""} value={newItem.unitPrice || ''}
onChange={(e) => onChange={(e) =>
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({ setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
...prev, ...prev,
@ -605,7 +547,7 @@ const SalesOrderForm: React.FC = () => {
min="0" min="0"
max="100" max="100"
step="0.01" step="0.01"
value={newItem.discountRate || ""} value={newItem.discountRate || ''}
onChange={(e) => onChange={(e) =>
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({ setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
...prev, ...prev,
@ -681,7 +623,7 @@ const SalesOrderForm: React.FC = () => {
</div> </div>
</td> </td>
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900"> <td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
{item.quantity} {item.unit?.name || ""} {item.quantity} {item.unit?.name || ''}
</td> </td>
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900"> <td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
{item.unitPrice.toLocaleString()} {item.unitPrice.toLocaleString()}
@ -717,8 +659,8 @@ const SalesOrderForm: React.FC = () => {
</div> </div>
) : ( ) : (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
Henüz sipariş kalemi eklenmedi. Yukarıdaki "Kalem Ekle" butonunu Henüz sipariş kalemi eklenmedi. Yukarıdaki "Kalem Ekle" butonunu kullanarak ürün
kullanarak ürün ekleyebilirsiniz. ekleyebilirsiniz.
</div> </div>
)} )}
</div> </div>
@ -727,9 +669,7 @@ const SalesOrderForm: React.FC = () => {
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<FaCalculator className="w-5 h-5 text-blue-600" /> <FaCalculator className="w-5 h-5 text-blue-600" />
<h3 className="text-base font-semibold text-gray-900"> <h3 className="text-base font-semibold text-gray-900">Mali Bilgiler</h3>
Mali Bilgiler
</h3>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@ -740,9 +680,7 @@ const SalesOrderForm: React.FC = () => {
</label> </label>
<select <select
value={formData.currency} value={formData.currency}
onChange={(e) => onChange={(e) => handleInputChange('currency', e.target.value)}
handleInputChange("currency", e.target.value)
}
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" 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="TRY">TRY - Türk Lirası</option> <option value="TRY">TRY - Türk Lirası</option>
@ -752,19 +690,14 @@ const SalesOrderForm: React.FC = () => {
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Döviz Kuru</label>
Döviz Kuru
</label>
<input <input
type="number" type="number"
min="0" min="0"
step="0.01" step="0.01"
value={formData.exchangeRate} value={formData.exchangeRate}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('exchangeRate', parseFloat(e.target.value) || 1)
"exchangeRate",
parseFloat(e.target.value) || 1
)
} }
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" 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"
/> />
@ -781,10 +714,7 @@ const SalesOrderForm: React.FC = () => {
step="0.01" step="0.01"
value={formData.discountRate} value={formData.discountRate}
onChange={(e) => onChange={(e) =>
handleInputChange( handleInputChange('discountRate', parseFloat(e.target.value) || 0)
"discountRate",
parseFloat(e.target.value) || 0
)
} }
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" 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"
/> />
@ -800,12 +730,7 @@ const SalesOrderForm: React.FC = () => {
max="100" max="100"
step="0.01" step="0.01"
value={formData.taxRate} value={formData.taxRate}
onChange={(e) => onChange={(e) => handleInputChange('taxRate', parseFloat(e.target.value) || 18)}
handleInputChange(
"taxRate",
parseFloat(e.target.value) || 18
)
}
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" 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"
/> />
</div> </div>
@ -816,9 +741,7 @@ const SalesOrderForm: React.FC = () => {
</label> </label>
<select <select
value={formData.paymentTerms} value={formData.paymentTerms}
onChange={(e) => onChange={(e) => handleInputChange('paymentTerms', e.target.value)}
handleInputChange("paymentTerms", e.target.value)
}
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" 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="">Seçin...</option> <option value="">Seçin...</option>
@ -832,9 +755,7 @@ const SalesOrderForm: React.FC = () => {
</div> </div>
<div className="bg-gray-50 p-3 rounded-lg"> <div className="bg-gray-50 p-3 rounded-lg">
<h4 className="font-medium text-sm text-gray-900 mb-2"> <h4 className="font-medium text-sm text-gray-900 mb-2">Sipariş Özeti</h4>
Sipariş Özeti
</h4>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-sm text-gray-600">Ara Toplam:</span> <span className="text-sm text-gray-600">Ara Toplam:</span>
@ -856,9 +777,7 @@ const SalesOrderForm: React.FC = () => {
</div> </div>
<div className="border-t pt-2 mt-2"> <div className="border-t pt-2 mt-2">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-lg font-semibold text-gray-900"> <span className="text-lg font-semibold text-gray-900">Genel Toplam:</span>
Genel Toplam:
</span>
<span className="text-lg font-bold text-blue-600"> <span className="text-lg font-bold text-blue-600">
{formData.totalAmount.toLocaleString()} {formData.totalAmount.toLocaleString()}
</span> </span>
@ -873,19 +792,15 @@ const SalesOrderForm: React.FC = () => {
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<FaNotesMedical className="w-5 h-5 text-blue-600" /> <FaNotesMedical className="w-5 h-5 text-blue-600" />
<h3 className="text-base font-semibold text-gray-900"> <h3 className="text-base font-semibold text-gray-900">Notlar ve Talimatlar</h3>
Notlar ve Talimatlar
</h3>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Genel Notlar</label>
Genel Notlar
</label>
<textarea <textarea
value={formData.notes} value={formData.notes}
onChange={(e) => handleInputChange("notes", e.target.value)} onChange={(e) => handleInputChange('notes', e.target.value)}
rows={4} 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" 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="Sipariş ile ilgili genel notlar..." placeholder="Sipariş ile ilgili genel notlar..."
@ -898,9 +813,7 @@ const SalesOrderForm: React.FC = () => {
</label> </label>
<textarea <textarea
value={formData.specialInstructions} value={formData.specialInstructions}
onChange={(e) => onChange={(e) => handleInputChange('specialInstructions', e.target.value)}
handleInputChange("specialInstructions", e.target.value)
}
rows={4} 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" 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="Özel üretim talimatları, ambalaj notları vb..." placeholder="Özel üretim talimatları, ambalaj notları vb..."
@ -910,7 +823,8 @@ const SalesOrderForm: React.FC = () => {
</div> </div>
</form> </form>
</div> </div>
); </Container>
}; )
}
export default SalesOrderForm; export default SalesOrderForm

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react'
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from 'react-router-dom'
import { import {
FaEdit, FaEdit,
FaArrowLeft, FaArrowLeft,
@ -17,73 +17,69 @@ import {
FaTruck, FaTruck,
FaFileInvoiceDollar, FaFileInvoiceDollar,
FaExclamationCircle, FaExclamationCircle,
} from "react-icons/fa"; } from 'react-icons/fa'
import { import { CrmSalesOrder, CrmSalesOrderItem, SaleOrderItemStatusEnum } from '../../../types/crm'
CrmSalesOrder, import { mockSalesOrders } from '../../../mocks/mockSalesOrders'
CrmSalesOrderItem,
SaleOrderItemStatusEnum,
} from "../../../types/crm";
import { mockSalesOrders } from "../../../mocks/mockSalesOrders";
import { import {
getSaleOrderItemStatusnfo, getSaleOrderItemStatusnfo,
getSaleOrderStatusColor, getSaleOrderStatusColor,
getSaleOrderStatusText, getSaleOrderStatusText,
} from "../../../utils/erp"; } from '../../../utils/erp'
import { Container } from '@/components/shared'
const SalesOrderView: React.FC = () => { const SalesOrderView: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const { id } = useParams(); const { id } = useParams()
const [order, setOrder] = useState<CrmSalesOrder | null>(null); const [order, setOrder] = useState<CrmSalesOrder | null>(null)
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true)
useEffect(() => { useEffect(() => {
if (id) { if (id) {
// Find the order from mock data // Find the order from mock data
setTimeout(() => { setTimeout(() => {
const foundOrder = mockSalesOrders.find((order) => order.id === id); const foundOrder = mockSalesOrders.find((order) => order.id === id)
if (foundOrder) { if (foundOrder) {
setOrder(foundOrder); setOrder(foundOrder)
} }
setLoading(false); setLoading(false)
}, 1000); }, 1000)
} }
}, [id]); }, [id])
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div> </div>
); )
} }
if (!order) { if (!order) {
return ( return (
<div className="flex flex-col items-center justify-center h-64"> <div className="flex flex-col items-center justify-center h-64">
<FaExclamationCircle className="text-4xl text-red-500 mb-4" /> <FaExclamationCircle className="text-4xl text-red-500 mb-4" />
<h2 className="text-xl font-semibold text-gray-900 mb-2"> <h2 className="text-xl font-semibold text-gray-900 mb-2">Sipariş Bulunamadı</h2>
Sipariş Bulunamadı
</h2>
<p className="text-gray-600 mb-4">Belirtilen sipariş mevcut değil.</p> <p className="text-gray-600 mb-4">Belirtilen sipariş mevcut değil.</p>
<button <button
onClick={() => navigate("/admin/crm/sales-orders")} onClick={() => navigate('/admin/crm/sales-orders')}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors" className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
> >
Sipariş Listesine Dön Sipariş Listesine Dön
</button> </button>
</div> </div>
); )
} }
return ( return (
<Container>
<div className="space-y-4"> <div className="space-y-4">
{/* Header */} {/* Header */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4"> <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<button <button
onClick={() => navigate("/admin/crm/sales-orders")} onClick={() => navigate('/admin/crm/sales-orders')}
className="flex items-center text-gray-600 hover:text-gray-800 transition-colors" className="flex items-center text-gray-600 hover:text-gray-800 transition-colors"
> >
<FaArrowLeft className="mr-2" /> <FaArrowLeft className="mr-2" />
@ -95,24 +91,20 @@ const SalesOrderView: React.FC = () => {
<FaShoppingCart className="mr-3 text-blue-600" /> <FaShoppingCart className="mr-3 text-blue-600" />
{order.orderNumber} {order.orderNumber}
</h1> </h1>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">Satış Siparişi Detayları</p>
Satış Siparişi Detayları
</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span <span
className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${getSaleOrderStatusColor( className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${getSaleOrderStatusColor(
order.status order.status,
)}`} )}`}
> >
{getSaleOrderStatusText(order.status)} {getSaleOrderStatusText(order.status)}
</span> </span>
<button <button
onClick={() => onClick={() => navigate(`/admin/crm/sales-orders/edit/${order.id}`)}
navigate(`/admin/crm/sales-orders/edit/${order.id}`)
}
className="bg-blue-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-blue-700 transition-colors flex items-center" className="bg-blue-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-blue-700 transition-colors flex items-center"
> >
<FaEdit className="mr-2" /> <FaEdit className="mr-2" />
@ -134,11 +126,9 @@ const SalesOrderView: React.FC = () => {
<div className="bg-gradient-to-r from-blue-50 to-blue-100 p-3 rounded-lg border border-blue-200"> <div className="bg-gradient-to-r from-blue-50 to-blue-100 p-3 rounded-lg border border-blue-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-blue-800"> <p className="text-sm font-medium text-blue-800">Sipariş Tarihi</p>
Sipariş Tarihi
</p>
<p className="text-base font-bold text-blue-900"> <p className="text-base font-bold text-blue-900">
{new Date(order.orderDate).toLocaleDateString("tr-TR")} {new Date(order.orderDate).toLocaleDateString('tr-TR')}
</p> </p>
</div> </div>
<FaCalendar className="text-2xl text-blue-600" /> <FaCalendar className="text-2xl text-blue-600" />
@ -148,13 +138,9 @@ const SalesOrderView: React.FC = () => {
<div className="bg-gradient-to-r from-green-50 to-green-100 p-3 rounded-lg border border-green-200"> <div className="bg-gradient-to-r from-green-50 to-green-100 p-3 rounded-lg border border-green-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-green-800"> <p className="text-sm font-medium text-green-800">Talep Edilen Tarih</p>
Talep Edilen Tarih
</p>
<p className="text-base font-bold text-green-900"> <p className="text-base font-bold text-green-900">
{new Date(order.requestedDeliveryDate).toLocaleDateString( {new Date(order.requestedDeliveryDate).toLocaleDateString('tr-TR')}
"tr-TR"
)}
</p> </p>
</div> </div>
<FaCalendar className="text-2xl text-green-600" /> <FaCalendar className="text-2xl text-green-600" />
@ -164,15 +150,11 @@ const SalesOrderView: React.FC = () => {
<div className="bg-gradient-to-r from-purple-50 to-purple-100 p-3 rounded-lg border border-purple-200"> <div className="bg-gradient-to-r from-purple-50 to-purple-100 p-3 rounded-lg border border-purple-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-purple-800"> <p className="text-sm font-medium text-purple-800">Onaylanan Tarih</p>
Onaylanan Tarih
</p>
<p className="text-base font-bold text-purple-900"> <p className="text-base font-bold text-purple-900">
{order.confirmedDeliveryDate {order.confirmedDeliveryDate
? new Date(order.confirmedDeliveryDate).toLocaleDateString( ? new Date(order.confirmedDeliveryDate).toLocaleDateString('tr-TR')
"tr-TR" : 'Belirtilmemiş'}
)
: "Belirtilmemiş"}
</p> </p>
</div> </div>
<FaCalendar className="text-2xl text-purple-600" /> <FaCalendar className="text-2xl text-purple-600" />
@ -182,11 +164,9 @@ const SalesOrderView: React.FC = () => {
<div className="bg-gradient-to-r from-amber-50 to-amber-100 p-3 rounded-lg border border-amber-200"> <div className="bg-gradient-to-r from-amber-50 to-amber-100 p-3 rounded-lg border border-amber-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-amber-800"> <p className="text-sm font-medium text-amber-800">Toplam Tutar</p>
Toplam Tutar
</p>
<p className="text-base font-bold text-amber-900"> <p className="text-base font-bold text-amber-900">
{order.totalAmount.toLocaleString("tr-TR")} {order.currency} {order.totalAmount.toLocaleString('tr-TR')} {order.currency}
</p> </p>
</div> </div>
<FaCalculator className="text-2xl text-amber-600" /> <FaCalculator className="text-2xl text-amber-600" />
@ -211,8 +191,8 @@ const SalesOrderView: React.FC = () => {
<span className="font-medium">Son Güncelleme:</span> <span className="font-medium">Son Güncelleme:</span>
<div className="mt-1"> <div className="mt-1">
{order.lastModificationTime {order.lastModificationTime
? new Date(order.lastModificationTime).toLocaleString("tr-TR") ? new Date(order.lastModificationTime).toLocaleString('tr-TR')
: "Güncelleme yok"} : 'Güncelleme yok'}
</div> </div>
</div> </div>
</div> </div>
@ -233,32 +213,24 @@ const SalesOrderView: React.FC = () => {
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2">
<div> <div>
<span className="text-sm font-medium text-gray-600"> <span className="text-sm font-medium text-gray-600">Firma Adı:</span>
Firma Adı:
</span>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900">
{order.customer?.name || "Müşteri bilgisi yok"} {order.customer?.name || 'Müşteri bilgisi yok'}
</p> </p>
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-600"> <span className="text-sm font-medium text-gray-600">İletişim Kişisi:</span>
İletişim Kişisi:
</span>
<p className="text-sm text-gray-900"> <p className="text-sm text-gray-900">
{order.customer?.primaryContact?.fullName || "Belirtilmemiş"} {order.customer?.primaryContact?.fullName || 'Belirtilmemiş'}
</p> </p>
</div> </div>
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
<FaPhone className="mr-2" /> <FaPhone className="mr-2" />
<span> <span>{order.customer?.primaryContact?.phone || 'Belirtilmemiş'}</span>
{order.customer?.primaryContact?.phone || "Belirtilmemiş"}
</span>
</div> </div>
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
<FaEnvelope className="mr-2" /> <FaEnvelope className="mr-2" />
<span> <span>{order.customer?.primaryContact?.email || 'Belirtilmemiş'}</span>
{order.customer?.primaryContact?.email || "Belirtilmemiş"}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -313,7 +285,7 @@ const SalesOrderView: React.FC = () => {
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{order.items.map((item: CrmSalesOrderItem) => { {order.items.map((item: CrmSalesOrderItem) => {
const itemStatus = getSaleOrderItemStatusnfo(item.status); const itemStatus = getSaleOrderItemStatusnfo(item.status)
return ( return (
<tr key={item.id} className="hover:bg-gray-50"> <tr key={item.id} className="hover:bg-gray-50">
<td className="px-4 py-2 whitespace-nowrap"> <td className="px-4 py-2 whitespace-nowrap">
@ -327,40 +299,32 @@ const SalesOrderView: React.FC = () => {
</div> </div>
</td> </td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900"> <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{item.quantity} {item.material?.baseUnit?.name || "Adet"} {item.quantity} {item.material?.baseUnit?.name || 'Adet'}
</td> </td>
<td className="px-4 py-2 whitespace-nowrap"> <td className="px-4 py-2 whitespace-nowrap">
<div className="font-medium"> <div className="font-medium">
{item.deliveredQuantity}{" "} {item.deliveredQuantity} {item.material?.baseUnit?.name || 'Adet'}
{item.material?.baseUnit?.name || "Adet"}
</div> </div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
{( {((item.deliveredQuantity / item.quantity) * 100).toFixed(1)}% tamamlandı
(item.deliveredQuantity / item.quantity) *
100
).toFixed(1)}
% tamamlandı
</div> </div>
</td> </td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900"> <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{item.unitPrice.toLocaleString("tr-TR")} {order.currency} {item.unitPrice.toLocaleString('tr-TR')} {order.currency}
</td> </td>
<td className="px-4 py-2 whitespace-nowrap text-sm font-medium text-gray-900"> <td className="px-4 py-2 whitespace-nowrap text-sm font-medium text-gray-900">
{item.totalAmount.toLocaleString("tr-TR")}{" "} {item.totalAmount.toLocaleString('tr-TR')} {order.currency}
{order.currency}
</td> </td>
<td className="px-4 py-2 whitespace-nowrap"> <td className="px-4 py-2 whitespace-nowrap">
<span <span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${itemStatus.color}`} className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${itemStatus.color}`}
> >
<itemStatus.icon <itemStatus.icon className={`mr-1 ${itemStatus.iconColor}`} />
className={`mr-1 ${itemStatus.iconColor}`}
/>
{itemStatus.label} {itemStatus.label}
</span> </span>
</td> </td>
</tr> </tr>
); )
})} })}
</tbody> </tbody>
</table> </table>
@ -377,33 +341,27 @@ const SalesOrderView: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between py-1.5 border-b border-gray-100"> <div className="flex justify-between py-1.5 border-b border-gray-100">
<span className="text-sm font-medium text-gray-600"> <span className="text-sm font-medium text-gray-600">Ara Toplam:</span>
Ara Toplam:
</span>
<span className="text-sm text-gray-900"> <span className="text-sm text-gray-900">
{order.subtotal.toLocaleString("tr-TR")} {order.currency} {order.subtotal.toLocaleString('tr-TR')} {order.currency}
</span> </span>
</div> </div>
<div className="flex justify-between py-1.5 border-b border-gray-100"> <div className="flex justify-between py-1.5 border-b border-gray-100">
<span className="text-sm font-medium text-gray-600"> <span className="text-sm font-medium text-gray-600">İndirim:</span>
İndirim:
</span>
<span className="text-sm text-gray-900"> <span className="text-sm text-gray-900">
-{order.discountAmount.toLocaleString("tr-TR")} {order.currency} -{order.discountAmount.toLocaleString('tr-TR')} {order.currency}
</span> </span>
</div> </div>
<div className="flex justify-between py-1.5 border-b border-gray-100"> <div className="flex justify-between py-1.5 border-b border-gray-100">
<span className="text-sm font-medium text-gray-600">KDV:</span> <span className="text-sm font-medium text-gray-600">KDV:</span>
<span className="text-sm text-gray-900"> <span className="text-sm text-gray-900">
{order.taxAmount.toLocaleString("tr-TR")} {order.currency} {order.taxAmount.toLocaleString('tr-TR')} {order.currency}
</span> </span>
</div> </div>
<div className="flex justify-between py-2 border-t-2 border-gray-200"> <div className="flex justify-between py-2 border-t-2 border-gray-200">
<span className="text-base font-bold text-gray-900">Genel Toplam:</span>
<span className="text-base font-bold text-gray-900"> <span className="text-base font-bold text-gray-900">
Genel Toplam: {order.totalAmount.toLocaleString('tr-TR')} {order.currency}
</span>
<span className="text-base font-bold text-gray-900">
{order.totalAmount.toLocaleString("tr-TR")} {order.currency}
</span> </span>
</div> </div>
</div> </div>
@ -417,20 +375,18 @@ const SalesOrderView: React.FC = () => {
<div className="text-sm text-blue-900"> <div className="text-sm text-blue-900">
<p>Toplam Kalem: {order.items.length}</p> <p>Toplam Kalem: {order.items.length}</p>
<p> <p>
Teslim Edilenler:{" "} Teslim Edilenler:{' '}
{ {
order.items.filter( order.items.filter(
(item) => (item) => item.status === SaleOrderItemStatusEnum.Delivered,
item.status === SaleOrderItemStatusEnum.Delivered
).length ).length
} }
</p> </p>
<p> <p>
Bekleyenler:{" "} Bekleyenler:{' '}
{ {
order.items.filter( order.items.filter(
(item) => (item) => item.status !== SaleOrderItemStatusEnum.Delivered,
item.status !== SaleOrderItemStatusEnum.Delivered
).length ).length
} }
</p> </p>
@ -450,7 +406,8 @@ const SalesOrderView: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
); </Container>
}; )
}
export default SalesOrderView; export default SalesOrderView

View file

@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom'
import { import {
FaShoppingCart, FaShoppingCart,
FaPlus, FaPlus,
@ -9,146 +9,128 @@ import {
FaCalendar, FaCalendar,
FaBox, FaBox,
FaBuilding, FaBuilding,
} from "react-icons/fa"; } from 'react-icons/fa'
import { CrmSalesOrder, SaleOrderStatusEnum } from "../../../types/crm"; import { CrmSalesOrder, SaleOrderStatusEnum } from '../../../types/crm'
import DataTable, { Column } from "../../../components/common/DataTable"; import DataTable, { Column } from '../../../components/common/DataTable'
import { mockBusinessParties } from "../../../mocks/mockBusinessParties"; import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { mockSalesOrders } from "../../../mocks/mockSalesOrders"; import { mockSalesOrders } from '../../../mocks/mockSalesOrders'
import { BusinessParty } from "../../../types/common"; import { BusinessParty } from '../../../types/common'
import Widget from "../../../components/common/Widget"; import Widget from '../../../components/common/Widget'
import { import { getSaleOrderStatusColor, getSaleOrderStatusText } from '../../../utils/erp'
getSaleOrderStatusColor, import { Container } from '@/components/shared'
getSaleOrderStatusText,
} from "../../../utils/erp";
const SalesOrders: React.FC = () => { const SalesOrders: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const [salesOrders] = useState<CrmSalesOrder[]>(mockSalesOrders); const [salesOrders] = useState<CrmSalesOrder[]>(mockSalesOrders)
const [customers] = useState<BusinessParty[]>(mockBusinessParties); const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState('')
const [selectedStatus, setSelectedStatus] = useState< const [selectedStatus, setSelectedStatus] = useState<SaleOrderStatusEnum | 'all'>('all')
SaleOrderStatusEnum | "all" const [selectedCustomer, setSelectedCustomer] = useState<string>('all')
>("all");
const [selectedCustomer, setSelectedCustomer] = useState<string>("all");
const handleAdd = () => { const handleAdd = () => {
navigate("/admin/crm/sales-orders/new"); navigate('/admin/crm/sales-orders/new')
}; }
const handleEdit = (order: CrmSalesOrder) => { const handleEdit = (order: CrmSalesOrder) => {
navigate(`/admin/crm/sales-orders/edit/${order.id}`); navigate(`/admin/crm/sales-orders/edit/${order.id}`)
}; }
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
console.log("Delete sales order:", id); console.log('Delete sales order:', id)
// Implement delete functionality // Implement delete functionality
}; }
const handleViewDetails = (order: CrmSalesOrder) => { const handleViewDetails = (order: CrmSalesOrder) => {
navigate(`/admin/crm/sales-orders/${order.id}`); navigate(`/admin/crm/sales-orders/${order.id}`)
}; }
const filteredOrders = salesOrders.filter((order) => { const filteredOrders = salesOrders.filter((order) => {
if ( if (searchTerm && !order.orderNumber.toLowerCase().includes(searchTerm.toLowerCase())) {
searchTerm && return false
!order.orderNumber.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false;
} }
if (selectedStatus !== "all" && order.status !== selectedStatus) { if (selectedStatus !== 'all' && order.status !== selectedStatus) {
return false; return false
} }
if (selectedCustomer !== "all" && order.customerId !== selectedCustomer) { if (selectedCustomer !== 'all' && order.customerId !== selectedCustomer) {
return false; return false
} }
return true; return true
}); })
const columns: Column<CrmSalesOrder>[] = [ const columns: Column<CrmSalesOrder>[] = [
{ {
key: "orderNumber", key: 'orderNumber',
header: "Sipariş No", header: 'Sipariş No',
sortable: true, sortable: true,
render: (order: CrmSalesOrder) => ( render: (order: CrmSalesOrder) => (
<div> <div>
<div className="font-medium text-gray-900">{order.orderNumber}</div> <div className="font-medium text-gray-900">{order.orderNumber}</div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">Sipariş #{order.id.substring(0, 8)}</div>
Sipariş #{order.id.substring(0, 8)}
</div>
</div> </div>
), ),
}, },
{ {
key: "customer", key: 'customer',
header: "Müşteri", header: 'Müşteri',
render: (order: CrmSalesOrder) => { render: (order: CrmSalesOrder) => {
const customer = customers.find((c) => c.id === order.customerId); const customer = customers.find((c) => c.id === order.customerId)
return customer ? ( return customer ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-400" /> <FaBuilding className="w-4 h-4 text-gray-400" />
<span>{customer.name}</span> <span>{customer.name}</span>
</div> </div>
) : ( ) : (
"-" '-'
); )
}, },
}, },
{ {
key: "orderDate", key: 'orderDate',
header: "Sipariş Tarihi", header: 'Sipariş Tarihi',
render: (order: CrmSalesOrder) => ( render: (order: CrmSalesOrder) => (
<div className="flex items-center gap-1 text-sm"> <div className="flex items-center gap-1 text-sm">
<FaCalendar className="w-3 h-3 text-gray-400" /> <FaCalendar className="w-3 h-3 text-gray-400" />
<span>{new Date(order.orderDate).toLocaleDateString("tr-TR")}</span> <span>{new Date(order.orderDate).toLocaleDateString('tr-TR')}</span>
</div> </div>
), ),
}, },
{ {
key: "deliveryDate", key: 'deliveryDate',
header: "Teslimat Tarihi", header: 'Teslimat Tarihi',
render: (order: CrmSalesOrder) => render: (order: CrmSalesOrder) =>
order.confirmedDeliveryDate ? ( order.confirmedDeliveryDate ? (
<div className="flex items-center gap-1 text-sm"> <div className="flex items-center gap-1 text-sm">
<FaBox className="w-3 h-3 text-gray-400" /> <FaBox className="w-3 h-3 text-gray-400" />
<span> <span>{new Date(order.confirmedDeliveryDate).toLocaleDateString('tr-TR')}</span>
{new Date(order.confirmedDeliveryDate).toLocaleDateString(
"tr-TR"
)}
</span>
</div> </div>
) : order.requestedDeliveryDate ? ( ) : order.requestedDeliveryDate ? (
<div className="flex items-center gap-1 text-sm text-gray-500"> <div className="flex items-center gap-1 text-sm text-gray-500">
<FaBox className="w-3 h-3 text-gray-400" /> <FaBox className="w-3 h-3 text-gray-400" />
<span> <span>
{new Date(order.requestedDeliveryDate).toLocaleDateString( {new Date(order.requestedDeliveryDate).toLocaleDateString('tr-TR')} (İstenen)
"tr-TR"
)}{" "}
(İstenen)
</span> </span>
</div> </div>
) : ( ) : (
"-" '-'
), ),
}, },
{ {
key: "totalAmount", key: 'totalAmount',
header: "Toplam Tutar", header: 'Toplam Tutar',
render: (order: CrmSalesOrder) => ( render: (order: CrmSalesOrder) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-400" /> <FaDollarSign className="w-4 h-4 text-gray-400" />
<span className="font-medium"> <span className="font-medium">{order.totalAmount.toLocaleString()}</span>
{order.totalAmount.toLocaleString()}
</span>
</div> </div>
), ),
}, },
{ {
key: "status", key: 'status',
header: "Durum", header: 'Durum',
render: (order: CrmSalesOrder) => ( render: (order: CrmSalesOrder) => (
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${getSaleOrderStatusColor( className={`px-2 py-1 text-xs font-medium rounded-full ${getSaleOrderStatusColor(
order.status order.status,
)}`} )}`}
> >
{getSaleOrderStatusText(order.status)} {getSaleOrderStatusText(order.status)}
@ -156,8 +138,8 @@ const SalesOrders: React.FC = () => {
), ),
}, },
{ {
key: "actions", key: 'actions',
header: "İşlemler", header: 'İşlemler',
render: (order: CrmSalesOrder) => ( render: (order: CrmSalesOrder) => (
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@ -184,61 +166,49 @@ const SalesOrders: React.FC = () => {
</div> </div>
), ),
}, },
]; ]
// Calculate statistics // Calculate statistics
const totalOrders = salesOrders.length; const totalOrders = salesOrders.length
const activeOrders = salesOrders.filter( const activeOrders = salesOrders.filter(
(o) => (o) =>
o.status === SaleOrderStatusEnum.Confirmed || o.status === SaleOrderStatusEnum.Confirmed || o.status === SaleOrderStatusEnum.InProduction,
o.status === SaleOrderStatusEnum.InProduction ).length
).length;
const totalRevenue = salesOrders const totalRevenue = salesOrders
.filter((o) => o.status === SaleOrderStatusEnum.Delivered) .filter((o) => o.status === SaleOrderStatusEnum.Delivered)
.reduce((sum, order) => sum + order.totalAmount, 0); .reduce((sum, order) => sum + order.totalAmount, 0)
const averageOrderValue = const averageOrderValue =
salesOrders.reduce((sum, order) => sum + order.totalAmount, 0) / salesOrders.reduce((sum, order) => sum + order.totalAmount, 0) / salesOrders.length || 0
salesOrders.length || 0;
// Status distribution // Status distribution
const statusDistribution = Object.values(SaleOrderStatusEnum).map( const statusDistribution = Object.values(SaleOrderStatusEnum).map((status) => ({
(status) => ({
status, status,
count: salesOrders.filter((o) => o.status === status).length, count: salesOrders.filter((o) => o.status === status).length,
value: salesOrders value: salesOrders
.filter((o) => o.status === status) .filter((o) => o.status === status)
.reduce((sum, o) => sum + o.totalAmount, 0), .reduce((sum, o) => sum + o.totalAmount, 0),
}) }))
);
// Monthly trend // Monthly trend
const currentMonth = new Date(); const currentMonth = new Date()
const lastMonth = new Date( const lastMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)
currentMonth.getFullYear(), const currentMonthOrders = salesOrders.filter((o) => new Date(o.orderDate) >= lastMonth).length
currentMonth.getMonth() - 1,
1
);
const currentMonthOrders = salesOrders.filter(
(o) => new Date(o.orderDate) >= lastMonth
).length;
const previousMonthOrders = salesOrders.filter((o) => { const previousMonthOrders = salesOrders.filter((o) => {
const orderDate = new Date(o.orderDate); const orderDate = new Date(o.orderDate)
return ( return (
orderDate >= orderDate >= new Date(lastMonth.getFullYear(), lastMonth.getMonth() - 1, 1) &&
new Date(lastMonth.getFullYear(), lastMonth.getMonth() - 1, 1) &&
orderDate < lastMonth orderDate < lastMonth
); )
}).length; }).length
return ( return (
<Container>
<div className="space-y-3 pt-2"> <div className="space-y-3 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-xl font-bold text-gray-900">Satış Siparişleri</h2> <h2 className="text-xl font-bold text-gray-900">Satış Siparişleri</h2>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">Müşteri siparişleri ve teslimat takibi</p>
Müşteri siparişleri ve teslimat takibi
</p>
</div> </div>
<button <button
onClick={handleAdd} onClick={handleAdd}
@ -251,19 +221,9 @@ const SalesOrders: React.FC = () => {
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget <Widget title="Toplam Sipariş" value={totalOrders} color="blue" icon="FaShoppingCart" />
title="Toplam Sipariş"
value={totalOrders}
color="blue"
icon="FaShoppingCart"
/>
<Widget <Widget title="Aktif Sipariş" value={activeOrders} color="orange" icon="FaBox" />
title="Aktif Sipariş"
value={activeOrders}
color="orange"
icon="FaBox"
/>
<Widget <Widget
title="Toplam Gelir" title="Toplam Gelir"
@ -282,28 +242,19 @@ const SalesOrders: React.FC = () => {
{/* Order Status Pipeline */} {/* Order Status Pipeline */}
<div className="bg-white rounded-lg shadow-sm border p-3"> <div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3"> <h3 className="text-sm font-semibold text-gray-900 mb-3">Sipariş Durumu Dağılımı</h3>
Sipariş Durumu Dağılımı
</h3>
<div className="grid grid-cols-2 md:grid-cols-7 gap-2"> <div className="grid grid-cols-2 md:grid-cols-7 gap-2">
{statusDistribution.map(({ status, count, value }) => ( {statusDistribution.map(({ status, count, value }) => (
<div <div key={status as string} className="text-center p-2 border rounded-lg">
key={status as string}
className="text-center p-2 border rounded-lg"
>
<div <div
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getSaleOrderStatusColor( className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getSaleOrderStatusColor(
status as SaleOrderStatusEnum status as SaleOrderStatusEnum,
)}`} )}`}
> >
{getSaleOrderStatusText(status as SaleOrderStatusEnum)} {getSaleOrderStatusText(status as SaleOrderStatusEnum)}
</div> </div>
<div className="text-xl font-bold text-gray-900 mb-1"> <div className="text-xl font-bold text-gray-900 mb-1">{count}</div>
{count} <div className="text-xs text-gray-500">{value.toLocaleString()}</div>
</div>
<div className="text-xs text-gray-500">
{value.toLocaleString()}
</div>
</div> </div>
))} ))}
</div> </div>
@ -311,39 +262,29 @@ const SalesOrders: React.FC = () => {
{/* Monthly Performance */} {/* Monthly Performance */}
<div className="bg-white rounded-lg shadow-sm border p-3"> <div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3"> <h3 className="text-sm font-semibold text-gray-900 mb-3">Aylık Performans</h3>
Aylık Performans
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1"> <div className="text-2xl font-bold text-blue-600 mb-1">{currentMonthOrders}</div>
{currentMonthOrders}
</div>
<p className="text-sm text-gray-600">Bu Ay</p> <p className="text-sm text-gray-600">Bu Ay</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-gray-600 mb-1"> <div className="text-2xl font-bold text-gray-600 mb-1">{previousMonthOrders}</div>
{previousMonthOrders}
</div>
<p className="text-sm text-gray-600">Geçen Ay</p> <p className="text-sm text-gray-600">Geçen Ay</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<div <div
className={`text-2xl font-bold mb-1 ${ className={`text-2xl font-bold mb-1 ${
currentMonthOrders >= previousMonthOrders currentMonthOrders >= previousMonthOrders ? 'text-green-600' : 'text-red-600'
? "text-green-600"
: "text-red-600"
}`} }`}
> >
{previousMonthOrders > 0 {previousMonthOrders > 0
? `${Math.round( ? `${Math.round(
((currentMonthOrders - previousMonthOrders) / ((currentMonthOrders - previousMonthOrders) / previousMonthOrders) * 100,
previousMonthOrders) *
100
)}%` )}%`
: "-%"} : '-%'}
</div> </div>
<p className="text-sm text-gray-600">Değişim</p> <p className="text-sm text-gray-600">Değişim</p>
</div> </div>
@ -364,9 +305,7 @@ const SalesOrders: React.FC = () => {
<select <select
value={selectedStatus} value={selectedStatus}
onChange={(e) => onChange={(e) => setSelectedStatus(e.target.value as SaleOrderStatusEnum | 'all')}
setSelectedStatus(e.target.value as SaleOrderStatusEnum | "all")
}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="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="all">Tüm Durumlar</option> <option value="all">Tüm Durumlar</option>
@ -399,16 +338,13 @@ const SalesOrders: React.FC = () => {
{filteredOrders.length === 0 && ( {filteredOrders.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<FaShoppingCart className="w-10 h-10 text-gray-400 mx-auto mb-3" /> <FaShoppingCart className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-sm font-medium text-gray-900 mb-2"> <h3 className="text-sm font-medium text-gray-900 mb-2">Sipariş bulunamadı</h3>
Sipariş bulunamadı <p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
</div> </div>
)} )}
</div> </div>
); </Container>
}; )
}
export default SalesOrders; export default SalesOrders

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { import {
FaUsers, FaUsers,
FaArrowLeft, FaArrowLeft,
@ -7,158 +7,144 @@ import {
FaPlus, FaPlus,
FaTrash, FaTrash,
FaMapMarkerAlt, FaMapMarkerAlt,
} from "react-icons/fa"; } from 'react-icons/fa'
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom'
import { CrmTerritory } from "../../../types/crm"; import { CrmTerritory } from '../../../types/crm'
import { mockEmployees } from "../../../mocks/mockEmployees"; import { mockEmployees } from '../../../mocks/mockEmployees'
import { Team, TeamMember, TeamRoleEnum } from "../../../types/common"; import { Team, TeamMember, TeamRoleEnum } from '../../../types/common'
import { Container } from '@/components/shared'
const SalesTeamCreate: React.FC = () => { const SalesTeamCreate: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
teamCode: "", teamCode: '',
name: "", name: '',
description: "", description: '',
managerId: "", managerId: '',
isActive: true, isActive: true,
}); })
const [members, setMembers] = useState<Partial<TeamMember>[]>([]); const [members, setMembers] = useState<Partial<TeamMember>[]>([])
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([]); const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([])
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({})
const handleInputChange = (field: string, value: string | boolean) => { const handleInputChange = (field: string, value: string | boolean) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }))
if (errors[field]) { if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" })); setErrors((prev) => ({ ...prev, [field]: '' }))
}
} }
};
const addMember = () => { const addMember = () => {
setMembers((prev) => [ setMembers((prev) => [
...prev, ...prev,
{ {
employeeId: "", employeeId: '',
role: TeamRoleEnum.Member, role: TeamRoleEnum.Member,
isActive: true, isActive: true,
}, },
]); ])
}; }
const removeMember = (index: number) => { const removeMember = (index: number) => {
setMembers((prev) => prev.filter((_, i) => i !== index)); setMembers((prev) => prev.filter((_, i) => i !== index))
}; }
const updateMember = ( const updateMember = (index: number, field: string, value: string | TeamRoleEnum | boolean) => {
index: number,
field: string,
value: string | TeamRoleEnum | boolean
) => {
setMembers((prev) => setMembers((prev) =>
prev.map((member, i) => prev.map((member, i) => (i === index ? { ...member, [field]: value } : member)),
i === index ? { ...member, [field]: value } : member
) )
); }
};
const addTerritory = () => { const addTerritory = () => {
setTerritories((prev) => [ setTerritories((prev) => [
...prev, ...prev,
{ {
territoryCode: "", territoryCode: '',
name: "", name: '',
description: "", description: '',
region: "", region: '',
countries: [], countries: [],
cities: [], cities: [],
isActive: true, isActive: true,
}, },
]); ])
}; }
const removeTerritory = (index: number) => { const removeTerritory = (index: number) => {
setTerritories((prev) => prev.filter((_, i) => i !== index)); setTerritories((prev) => prev.filter((_, i) => i !== index))
}; }
const updateTerritory = ( const updateTerritory = (index: number, field: string, value: string | string[] | boolean) => {
index: number,
field: string,
value: string | string[] | boolean
) => {
setTerritories((prev) => setTerritories((prev) =>
prev.map((territory, i) => prev.map((territory, i) => (i === index ? { ...territory, [field]: value } : territory)),
i === index ? { ...territory, [field]: value } : territory
) )
); }
};
const validateForm = () => { const validateForm = () => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {}
if (!formData.teamCode.trim()) { if (!formData.teamCode.trim()) {
newErrors.teamCode = "Takım kodu zorunludur"; newErrors.teamCode = 'Takım kodu zorunludur'
} }
if (!formData.name.trim()) { if (!formData.name.trim()) {
newErrors.name = "Takım adı zorunludur"; newErrors.name = 'Takım adı zorunludur'
} }
if (!formData.managerId.trim()) { if (!formData.managerId.trim()) {
newErrors.managerId = "Takım lideri seçimi zorunludur"; newErrors.managerId = 'Takım lideri seçimi zorunludur'
} }
setErrors(newErrors); setErrors(newErrors)
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0
}; }
const handleSave = () => { const handleSave = () => {
if (!validateForm()) return; if (!validateForm()) return
const newTeam: Partial<Team> = { const newTeam: Partial<Team> = {
...formData, ...formData,
members: members.map((member, index) => ({ members: members.map((member, index) => ({
...member, ...member,
id: `member-new-${index}`, id: `member-new-${index}`,
teamId: "new-team", teamId: 'new-team',
joinDate: new Date(), joinDate: new Date(),
})) as TeamMember[], })) as TeamMember[],
territories: territories.map((territory, index) => ({ territories: territories.map((territory, index) => ({
...territory, ...territory,
id: `territory-new-${index}`, id: `territory-new-${index}`,
assignedTeamId: "new-team", assignedTeamId: 'new-team',
})) as CrmTerritory[], })) as CrmTerritory[],
targets: [], targets: [],
creationTime: new Date(), creationTime: new Date(),
lastModificationTime: new Date(), lastModificationTime: new Date(),
}; }
console.log("Creating new sales team:", newTeam); console.log('Creating new sales team:', newTeam)
alert("Satış ekibi başarıyla oluşturuldu!"); alert('Satış ekibi başarıyla oluşturuldu!')
navigate("/admin/crm/sales-teams"); navigate('/admin/crm/sales-teams')
}; }
const handleCancel = () => { const handleCancel = () => {
if ( if (confirm('Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?')) {
confirm("Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?") navigate('/admin/crm/sales-teams')
) { }
navigate("/admin/crm/sales-teams");
} }
};
return ( return (
<Container>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
onClick={() => navigate("/admin/crm/sales-teams")} onClick={() => navigate('/admin/crm/sales-teams')}
className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
> >
<FaArrowLeft className="w-5 h-5" /> <FaArrowLeft className="w-5 h-5" />
</button> </button>
<div> <div>
<h2 className="text-xl font-bold text-gray-900"> <h2 className="text-xl font-bold text-gray-900">Yeni Satış Ekibi</h2>
Yeni Satış Ekibi
</h2>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">
Yeni satış ekibi oluşturun ve ekip üyelerini atayın Yeni satış ekibi oluşturun ve ekip üyelerini atayın
</p> </p>
@ -201,11 +187,9 @@ const SalesTeamCreate: React.FC = () => {
<input <input
type="text" type="text"
value={formData.teamCode} value={formData.teamCode}
onChange={(e) => onChange={(e) => handleInputChange('teamCode', e.target.value)}
handleInputChange("teamCode", e.target.value)
}
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.teamCode ? "border-red-500" : "border-gray-300" errors.teamCode ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="ST-001" placeholder="ST-001"
/> />
@ -221,11 +205,9 @@ const SalesTeamCreate: React.FC = () => {
<select <select
value={formData.managerId} value={formData.managerId}
onChange={(e) => onChange={(e) => handleInputChange('managerId', e.target.value)}
handleInputChange("managerId", e.target.value)
}
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.managerId ? "border-red-500" : "border-gray-300" errors.managerId ? 'border-red-500' : 'border-gray-300'
}`} }`}
> >
<option value="">Çalışan seçin</option> <option value="">Çalışan seçin</option>
@ -237,40 +219,30 @@ const SalesTeamCreate: React.FC = () => {
</select> </select>
{errors.managerId && ( {errors.managerId && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">{errors.managerId}</p>
{errors.managerId}
</p>
)} )}
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Takım Adı *</label>
Takım Adı *
</label>
<input <input
type="text" type="text"
value={formData.name} value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)} onChange={(e) => handleInputChange('name', e.target.value)}
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.name ? "border-red-500" : "border-gray-300" errors.name ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="Kurumsal Satış Ekibi" placeholder="Kurumsal Satış Ekibi"
/> />
{errors.name && ( {errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</div> </div>
<div className="mt-4"> <div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">ıklama</label>
ıklama
</label>
<textarea <textarea
value={formData.description} value={formData.description}
onChange={(e) => onChange={(e) => handleInputChange('description', e.target.value)}
handleInputChange("description", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={3} rows={3}
placeholder="Ekip açıklaması..." placeholder="Ekip açıklaması..."
@ -282,9 +254,7 @@ const SalesTeamCreate: React.FC = () => {
<input <input
type="checkbox" type="checkbox"
checked={formData.isActive} checked={formData.isActive}
onChange={(e) => onChange={(e) => handleInputChange('isActive', e.target.checked)}
handleInputChange("isActive", e.target.checked)
}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/> />
<span className="ml-2 text-sm text-gray-700">Aktif</span> <span className="ml-2 text-sm text-gray-700">Aktif</span>
@ -320,10 +290,8 @@ const SalesTeamCreate: React.FC = () => {
Çalışan Çalışan
</label> </label>
<select <select
value={member.employeeId || ""} value={member.employeeId || ''}
onChange={(e) => onChange={(e) => updateMember(index, 'employeeId', e.target.value)}
updateMember(index, "employeeId", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="">Çalışan seçin</option> <option value="">Çalışan seçin</option>
@ -335,14 +303,10 @@ const SalesTeamCreate: React.FC = () => {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Rol</label>
Rol
</label>
<select <select
value={member.role || TeamRoleEnum.Member} value={member.role || TeamRoleEnum.Member}
onChange={(e) => onChange={(e) => updateMember(index, 'role', e.target.value)}
updateMember(index, "role", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value={TeamRoleEnum.Member}>Üye</option> <option value={TeamRoleEnum.Member}>Üye</option>
@ -364,9 +328,7 @@ const SalesTeamCreate: React.FC = () => {
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" /> <FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz ekip üyesi eklenmedi</p> <p>Henüz ekip üyesi eklenmedi</p>
<p className="text-sm"> <p className="text-sm">Üye eklemek için yukarıdaki butona tıklayın</p>
Üye eklemek için yukarıdaki butona tıklayın
</p>
</div> </div>
)} )}
</div> </div>
@ -392,14 +354,9 @@ const SalesTeamCreate: React.FC = () => {
<div className="space-y-3"> <div className="space-y-3">
{territories.map((territory, index) => ( {territories.map((territory, index) => (
<div <div key={index} className="p-3 border border-gray-200 rounded-lg">
key={index}
className="p-3 border border-gray-200 rounded-lg"
>
<div className="flex justify-between items-start mb-3"> <div className="flex justify-between items-start mb-3">
<h4 className="font-medium text-gray-900"> <h4 className="font-medium text-gray-900">Bölge {index + 1}</h4>
Bölge {index + 1}
</h4>
<button <button
onClick={() => removeTerritory(index)} onClick={() => removeTerritory(index)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded" className="p-1.5 text-red-600 hover:bg-red-50 rounded"
@ -415,14 +372,8 @@ const SalesTeamCreate: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
value={territory.territoryCode || ""} value={territory.territoryCode || ''}
onChange={(e) => onChange={(e) => updateTerritory(index, 'territoryCode', e.target.value)}
updateTerritory(
index,
"territoryCode",
e.target.value
)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="TR-IST" placeholder="TR-IST"
/> />
@ -434,10 +385,8 @@ const SalesTeamCreate: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
value={territory.name || ""} value={territory.name || ''}
onChange={(e) => onChange={(e) => updateTerritory(index, 'name', e.target.value)}
updateTerritory(index, "name", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="İstanbul Anadolu" placeholder="İstanbul Anadolu"
/> />
@ -449,10 +398,8 @@ const SalesTeamCreate: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
value={territory.region || ""} value={territory.region || ''}
onChange={(e) => onChange={(e) => updateTerritory(index, 'region', e.target.value)}
updateTerritory(index, "region", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Marmara" placeholder="Marmara"
/> />
@ -465,9 +412,7 @@ const SalesTeamCreate: React.FC = () => {
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" /> <FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz bölge eklenmedi</p> <p>Henüz bölge eklenmedi</p>
<p className="text-sm"> <p className="text-sm">Bölge eklemek için yukarıdaki butona tıklayın</p>
Bölge eklemek için yukarıdaki butona tıklayın
</p>
</div> </div>
)} )}
</div> </div>
@ -475,7 +420,8 @@ const SalesTeamCreate: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
); </Container>
}; )
}
export default SalesTeamCreate; export default SalesTeamCreate

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react'
import { import {
FaUsers, FaUsers,
FaArrowLeft, FaArrowLeft,
@ -7,131 +7,120 @@ import {
FaPlus, FaPlus,
FaTrash, FaTrash,
FaMapMarkerAlt, FaMapMarkerAlt,
} from "react-icons/fa"; } from 'react-icons/fa'
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from 'react-router-dom'
import { CrmTerritory } from "../../../types/crm"; import { CrmTerritory } from '../../../types/crm'
import mockSalesTeams from "../../../mocks/mockSalesTeams"; import mockSalesTeams from '../../../mocks/mockSalesTeams'
import { mockEmployees } from "../../../mocks/mockEmployees"; import { mockEmployees } from '../../../mocks/mockEmployees'
import { Team, TeamMember, TeamRoleEnum } from "../../../types/common"; import { Team, TeamMember, TeamRoleEnum } from '../../../types/common'
import { Container } from '@/components/shared'
const SalesTeamEdit: React.FC = () => { const SalesTeamEdit: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>()
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
teamCode: "", teamCode: '',
name: "", name: '',
description: "", description: '',
managerId: "", managerId: '',
isActive: true, isActive: true,
}); })
const [members, setMembers] = useState<Partial<TeamMember>[]>([]); const [members, setMembers] = useState<Partial<TeamMember>[]>([])
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([]); const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([])
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({})
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true)
// Load team data on component mount // Load team data on component mount
useEffect(() => { useEffect(() => {
const team = mockSalesTeams.find((t) => t.id === id); const team = mockSalesTeams.find((t) => t.id === id)
if (team) { if (team) {
setFormData({ setFormData({
teamCode: team.code, teamCode: team.code,
name: team.name, name: team.name,
description: team.description || "", description: team.description || '',
managerId: team.managerId, managerId: team.managerId,
isActive: team.isActive, isActive: team.isActive,
}); })
setMembers(team.members || []); setMembers(team.members || [])
setTerritories(team.territories || []); setTerritories(team.territories || [])
} }
setLoading(false); setLoading(false)
}, [id]); }, [id])
const handleInputChange = (field: string, value: string | boolean) => { const handleInputChange = (field: string, value: string | boolean) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }))
if (errors[field]) { if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" })); setErrors((prev) => ({ ...prev, [field]: '' }))
}
} }
};
const addMember = () => { const addMember = () => {
setMembers((prev) => [ setMembers((prev) => [
...prev, ...prev,
{ {
employeeId: "", employeeId: '',
role: TeamRoleEnum.Member, role: TeamRoleEnum.Member,
isActive: true, isActive: true,
}, },
]); ])
}; }
const removeMember = (index: number) => { const removeMember = (index: number) => {
setMembers((prev) => prev.filter((_, i) => i !== index)); setMembers((prev) => prev.filter((_, i) => i !== index))
}; }
const updateMember = ( const updateMember = (index: number, field: string, value: string | TeamRoleEnum | boolean) => {
index: number,
field: string,
value: string | TeamRoleEnum | boolean
) => {
setMembers((prev) => setMembers((prev) =>
prev.map((member, i) => prev.map((member, i) => (i === index ? { ...member, [field]: value } : member)),
i === index ? { ...member, [field]: value } : member
) )
); }
};
const addTerritory = () => { const addTerritory = () => {
setTerritories((prev) => [ setTerritories((prev) => [
...prev, ...prev,
{ {
territoryCode: "", territoryCode: '',
name: "", name: '',
description: "", description: '',
region: "", region: '',
countries: [], countries: [],
cities: [], cities: [],
isActive: true, isActive: true,
}, },
]); ])
}; }
const removeTerritory = (index: number) => { const removeTerritory = (index: number) => {
setTerritories((prev) => prev.filter((_, i) => i !== index)); setTerritories((prev) => prev.filter((_, i) => i !== index))
}; }
const updateTerritory = ( const updateTerritory = (index: number, field: string, value: string | string[] | boolean) => {
index: number,
field: string,
value: string | string[] | boolean
) => {
setTerritories((prev) => setTerritories((prev) =>
prev.map((territory, i) => prev.map((territory, i) => (i === index ? { ...territory, [field]: value } : territory)),
i === index ? { ...territory, [field]: value } : territory
) )
); }
};
const validateForm = () => { const validateForm = () => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {}
if (!formData.teamCode.trim()) { if (!formData.teamCode.trim()) {
newErrors.teamCode = "Takım kodu zorunludur"; newErrors.teamCode = 'Takım kodu zorunludur'
} }
if (!formData.name.trim()) { if (!formData.name.trim()) {
newErrors.name = "Takım adı zorunludur"; newErrors.name = 'Takım adı zorunludur'
} }
if (!formData.managerId.trim()) { if (!formData.managerId.trim()) {
newErrors.managerId = "Takım lideri seçimi zorunludur"; newErrors.managerId = 'Takım lideri seçimi zorunludur'
} }
setErrors(newErrors); setErrors(newErrors)
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0
}; }
const handleSave = () => { const handleSave = () => {
if (!validateForm()) return; if (!validateForm()) return
const updatedTeam: Partial<Team> = { const updatedTeam: Partial<Team> = {
id, id,
@ -139,29 +128,27 @@ const SalesTeamEdit: React.FC = () => {
members: members.map((member, index) => ({ members: members.map((member, index) => ({
...member, ...member,
id: member.id || `member-updated-${index}`, id: member.id || `member-updated-${index}`,
teamId: id || "", teamId: id || '',
joinDate: member.joinDate || new Date(), joinDate: member.joinDate || new Date(),
})) as TeamMember[], })) as TeamMember[],
territories: territories.map((territory, index) => ({ territories: territories.map((territory, index) => ({
...territory, ...territory,
id: territory.id || `territory-updated-${index}`, id: territory.id || `territory-updated-${index}`,
assignedTeamId: id || "", assignedTeamId: id || '',
})) as CrmTerritory[], })) as CrmTerritory[],
lastModificationTime: new Date(), lastModificationTime: new Date(),
}; }
console.log("Updating sales team:", updatedTeam); console.log('Updating sales team:', updatedTeam)
alert("Satış ekibi başarıyla güncellendi!"); alert('Satış ekibi başarıyla güncellendi!')
navigate("/admin/crm/sales-teams"); navigate('/admin/crm/sales-teams')
}; }
const handleCancel = () => { const handleCancel = () => {
if ( if (confirm('Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?')) {
confirm("Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?") navigate('/admin/crm/sales-teams')
) { }
navigate("/admin/crm/sales-teams");
} }
};
if (loading) { if (loading) {
return ( return (
@ -171,47 +158,42 @@ const SalesTeamEdit: React.FC = () => {
<p className="mt-4 text-gray-600">Yükleniyor...</p> <p className="mt-4 text-gray-600">Yükleniyor...</p>
</div> </div>
</div> </div>
); )
} }
const team = mockSalesTeams.find((t) => t.id === id); const team = mockSalesTeams.find((t) => t.id === id)
if (!team) { if (!team) {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">Satış ekibi bulunamadı</h3>
Satış ekibi bulunamadı
</h3>
<p className="text-gray-500 mb-4"> <p className="text-gray-500 mb-4">
Aradığınız satış ekibi mevcut değil veya silinmiş olabilir. Aradığınız satış ekibi mevcut değil veya silinmiş olabilir.
</p> </p>
<button <button
onClick={() => navigate("/admin/crm/sales-teams")} onClick={() => navigate('/admin/crm/sales-teams')}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
> >
Geri Dön Geri Dön
</button> </button>
</div> </div>
); )
} }
return ( return (
<Container>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
onClick={() => navigate("/admin/crm/sales-teams")} onClick={() => navigate('/admin/crm/sales-teams')}
className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
> >
<FaArrowLeft className="w-5 h-5" /> <FaArrowLeft className="w-5 h-5" />
</button> </button>
<div> <div>
<h2 className="text-xl font-bold text-gray-900"> <h2 className="text-xl font-bold text-gray-900">Satış Ekibi Düzenle</h2>
Satış Ekibi Düzenle <p className="text-sm text-gray-600 mt-1">{team.name} ekibini düzenleyin</p>
</h2>
<p className="text-sm text-gray-600 mt-1">
{team.name} ekibini düzenleyin
</p>
</div> </div>
</div> </div>
@ -251,11 +233,9 @@ const SalesTeamEdit: React.FC = () => {
<input <input
type="text" type="text"
value={formData.teamCode} value={formData.teamCode}
onChange={(e) => onChange={(e) => handleInputChange('teamCode', e.target.value)}
handleInputChange("teamCode", e.target.value)
}
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.teamCode ? "border-red-500" : "border-gray-300" errors.teamCode ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="ST-001" placeholder="ST-001"
/> />
@ -271,11 +251,9 @@ const SalesTeamEdit: React.FC = () => {
<select <select
value={formData.managerId} value={formData.managerId}
onChange={(e) => onChange={(e) => handleInputChange('managerId', e.target.value)}
handleInputChange("managerId", e.target.value)
}
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.managerId ? "border-red-500" : "border-gray-300" errors.managerId ? 'border-red-500' : 'border-gray-300'
}`} }`}
> >
<option value="">Lider seçin</option> <option value="">Lider seçin</option>
@ -286,40 +264,30 @@ const SalesTeamEdit: React.FC = () => {
))} ))}
</select> </select>
{errors.managerId && ( {errors.managerId && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">{errors.managerId}</p>
{errors.managerId}
</p>
)} )}
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Takım Adı *</label>
Takım Adı *
</label>
<input <input
type="text" type="text"
value={formData.name} value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)} onChange={(e) => handleInputChange('name', e.target.value)}
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.name ? "border-red-500" : "border-gray-300" errors.name ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="Kurumsal Satış Ekibi" placeholder="Kurumsal Satış Ekibi"
/> />
{errors.name && ( {errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</div> </div>
<div className="mt-4"> <div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">ıklama</label>
ıklama
</label>
<textarea <textarea
value={formData.description} value={formData.description}
onChange={(e) => onChange={(e) => handleInputChange('description', e.target.value)}
handleInputChange("description", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={3} rows={3}
placeholder="Ekip açıklaması..." placeholder="Ekip açıklaması..."
@ -331,9 +299,7 @@ const SalesTeamEdit: React.FC = () => {
<input <input
type="checkbox" type="checkbox"
checked={formData.isActive} checked={formData.isActive}
onChange={(e) => onChange={(e) => handleInputChange('isActive', e.target.checked)}
handleInputChange("isActive", e.target.checked)
}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/> />
<span className="ml-2 text-sm text-gray-700">Aktif</span> <span className="ml-2 text-sm text-gray-700">Aktif</span>
@ -370,10 +336,8 @@ const SalesTeamEdit: React.FC = () => {
</label> </label>
<select <select
value={member.employeeId || ""} value={member.employeeId || ''}
onChange={(e) => onChange={(e) => updateMember(index, 'employeeId', e.target.value)}
updateMember(index, "employeeId", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="">Çalışan seçin</option> <option value="">Çalışan seçin</option>
@ -385,17 +349,11 @@ const SalesTeamEdit: React.FC = () => {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">Rol</label>
Rol
</label>
<select <select
value={member.role || TeamRoleEnum.Member} value={member.role || TeamRoleEnum.Member}
onChange={(e) => onChange={(e) =>
updateMember( updateMember(index, 'role', e.target.value as TeamRoleEnum)
index,
"role",
e.target.value as TeamRoleEnum
)
} }
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
@ -418,9 +376,7 @@ const SalesTeamEdit: React.FC = () => {
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" /> <FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz ekip üyesi yok</p> <p>Henüz ekip üyesi yok</p>
<p className="text-sm"> <p className="text-sm">Üye eklemek için yukarıdaki butona tıklayın</p>
Üye eklemek için yukarıdaki butona tıklayın
</p>
</div> </div>
)} )}
</div> </div>
@ -446,10 +402,7 @@ const SalesTeamEdit: React.FC = () => {
<div className="space-y-3"> <div className="space-y-3">
{territories.map((territory, index) => ( {territories.map((territory, index) => (
<div <div key={index} className="p-3 border border-gray-200 rounded-lg">
key={index}
className="p-3 border border-gray-200 rounded-lg"
>
<div className="flex justify-between items-start mb-3"> <div className="flex justify-between items-start mb-3">
<h4 className="font-medium text-gray-900"> <h4 className="font-medium text-gray-900">
{territory.name || `Bölge ${index + 1}`} {territory.name || `Bölge ${index + 1}`}
@ -469,14 +422,8 @@ const SalesTeamEdit: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
value={territory.territoryCode || ""} value={territory.territoryCode || ''}
onChange={(e) => onChange={(e) => updateTerritory(index, 'territoryCode', e.target.value)}
updateTerritory(
index,
"territoryCode",
e.target.value
)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="TR-IST" placeholder="TR-IST"
/> />
@ -488,10 +435,8 @@ const SalesTeamEdit: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
value={territory.name || ""} value={territory.name || ''}
onChange={(e) => onChange={(e) => updateTerritory(index, 'name', e.target.value)}
updateTerritory(index, "name", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="İstanbul Anadolu" placeholder="İstanbul Anadolu"
/> />
@ -503,10 +448,8 @@ const SalesTeamEdit: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
value={territory.region || ""} value={territory.region || ''}
onChange={(e) => onChange={(e) => updateTerritory(index, 'region', e.target.value)}
updateTerritory(index, "region", e.target.value)
}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Marmara" placeholder="Marmara"
/> />
@ -519,9 +462,7 @@ const SalesTeamEdit: React.FC = () => {
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" /> <FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz bölge yok</p> <p>Henüz bölge yok</p>
<p className="text-sm"> <p className="text-sm">Bölge eklemek için yukarıdaki butona tıklayın</p>
Bölge eklemek için yukarıdaki butona tıklayın
</p>
</div> </div>
)} )}
</div> </div>
@ -529,7 +470,8 @@ const SalesTeamEdit: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
); </Container>
}; )
}
export default SalesTeamEdit; export default SalesTeamEdit

View file

@ -1,4 +1,4 @@
import React from "react"; import React from 'react'
import { import {
FaUsers, FaUsers,
FaArrowLeft, FaArrowLeft,
@ -11,82 +11,80 @@ import {
FaAward, FaAward,
FaTrophy, FaTrophy,
FaChartLine, FaChartLine,
} from "react-icons/fa"; } from 'react-icons/fa'
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from 'react-router-dom'
import mockSalesTeams from "../../../mocks/mockSalesTeams"; import mockSalesTeams from '../../../mocks/mockSalesTeams'
import dayjs from "dayjs"; import dayjs from 'dayjs'
import { Team, TeamRoleEnum } from "../../../types/common"; import { Team, TeamRoleEnum } from '../../../types/common'
import { getTeamRoleText } from "../../../utils/erp"; import { getTeamRoleText } from '../../../utils/erp'
import { Container } from '@/components/shared'
const SalesTeamView: React.FC = () => { const SalesTeamView: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>()
const team = mockSalesTeams.find((t) => t.id === id); const team = mockSalesTeams.find((t) => t.id === id)
const calculateTeamPerformance = (team: Team): number => { const calculateTeamPerformance = (team: Team): number => {
if (!team.targets || team.targets.length === 0) return 0; if (!team.targets || team.targets.length === 0) return 0
const activeTarget = team.targets.find((t) => t.status === "ACTIVE"); const activeTarget = team.targets.find((t) => t.status === 'ACTIVE')
if (!activeTarget || activeTarget.targetValue === 0) return 0; if (!activeTarget || activeTarget.targetValue === 0) return 0
return (activeTarget.actualValue / activeTarget.targetValue) * 100; return (activeTarget.actualValue / activeTarget.targetValue) * 100
}; }
const calculateTeamRevenue = (team: Team): number => { const calculateTeamRevenue = (team: Team): number => {
if (!team.targets || team.targets.length === 0) return 0; if (!team.targets || team.targets.length === 0) return 0
return team.targets.reduce((sum, target) => sum + target.actualValue, 0); return team.targets.reduce((sum, target) => sum + target.actualValue, 0)
}; }
const handleEdit = () => { const handleEdit = () => {
navigate(`/admin/crm/sales-teams/edit/${id}`); navigate(`/admin/crm/sales-teams/edit/${id}`)
}; }
const handleDelete = () => { const handleDelete = () => {
if (confirm(`${team?.name} ekibini silmek istediğinizden emin misiniz?`)) { if (confirm(`${team?.name} ekibini silmek istediğinizden emin misiniz?`)) {
console.log("Delete sales team:", id); console.log('Delete sales team:', id)
alert("Ekip silindi (mock)"); alert('Ekip silindi (mock)')
navigate("/admin/crm/sales-teams"); navigate('/admin/crm/sales-teams')
}
} }
};
if (!team) { if (!team) {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">Satış ekibi bulunamadı</h3>
Satış ekibi bulunamadı
</h3>
<p className="text-gray-500 mb-4"> <p className="text-gray-500 mb-4">
Aradığınız satış ekibi mevcut değil veya silinmiş olabilir. Aradığınız satış ekibi mevcut değil veya silinmiş olabilir.
</p> </p>
<button <button
onClick={() => navigate("/admin/crm/sales-teams")} onClick={() => navigate('/admin/crm/sales-teams')}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
> >
Geri Dön Geri Dön
</button> </button>
</div> </div>
); )
} }
const performance = calculateTeamPerformance(team); const performance = calculateTeamPerformance(team)
const revenue = calculateTeamRevenue(team); const revenue = calculateTeamRevenue(team)
const activeTarget = team.targets?.find((t) => t.status === "ACTIVE"); const activeTarget = team.targets?.find((t) => t.status === 'ACTIVE')
return ( return (
<Container>
<div className="space-y-6 pt-2"> <div className="space-y-6 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
onClick={() => navigate("/admin/crm/sales-teams")} onClick={() => navigate('/admin/crm/sales-teams')}
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
> >
<FaArrowLeft className="w-5 h-5" /> <FaArrowLeft className="w-5 h-5" />
</button> </button>
<div> <div>
<h2 className="text-xl font-bold text-gray-900">{team.name}</h2> <h2 className="text-xl font-bold text-gray-900">{team.name}</h2>
<p className="text-gray-600 mt-1"> <p className="text-gray-600 mt-1">{team.code} Satış ekibi detayları</p>
{team.code} Satış ekibi detayları
</p>
</div> </div>
</div> </div>
@ -115,9 +113,7 @@ const SalesTeamView: React.FC = () => {
<FaUsers className="w-6 h-6 text-blue-500" /> <FaUsers className="w-6 h-6 text-blue-500" />
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600">Üye Sayısı</p> <p className="text-sm font-medium text-gray-600">Üye Sayısı</p>
<p className="text-xl font-bold text-gray-900"> <p className="text-xl font-bold text-gray-900">{team.members?.length || 0}</p>
{team.members?.length || 0}
</p>
</div> </div>
</div> </div>
</div> </div>
@ -127,9 +123,7 @@ const SalesTeamView: React.FC = () => {
<FaMapMarkerAlt className="w-6 h-6 text-green-500" /> <FaMapMarkerAlt className="w-6 h-6 text-green-500" />
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600">Bölge Sayısı</p> <p className="text-sm font-medium text-gray-600">Bölge Sayısı</p>
<p className="text-xl font-bold text-gray-900"> <p className="text-xl font-bold text-gray-900">{team.territories?.length || 0}</p>
{team.territories?.length || 0}
</p>
</div> </div>
</div> </div>
</div> </div>
@ -139,10 +133,10 @@ const SalesTeamView: React.FC = () => {
<FaArrowUp <FaArrowUp
className={`w-6 h-6 ${ className={`w-6 h-6 ${
performance >= 100 performance >= 100
? "text-green-600" ? 'text-green-600'
: performance >= 80 : performance >= 80
? "text-yellow-600" ? 'text-yellow-600'
: "text-red-600" : 'text-red-600'
}`} }`}
/> />
<div className="ml-4"> <div className="ml-4">
@ -150,10 +144,10 @@ const SalesTeamView: React.FC = () => {
<p <p
className={`text-xl font-bold ${ className={`text-xl font-bold ${
performance >= 100 performance >= 100
? "text-green-600" ? 'text-green-600'
: performance >= 80 : performance >= 80
? "text-yellow-600" ? 'text-yellow-600'
: "text-red-600" : 'text-red-600'
}`} }`}
> >
%{performance.toFixed(1)} %{performance.toFixed(1)}
@ -167,9 +161,7 @@ const SalesTeamView: React.FC = () => {
<FaDollarSign className="w-6 h-6 text-purple-500" /> <FaDollarSign className="w-6 h-6 text-purple-500" />
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600">Toplam Gelir</p> <p className="text-sm font-medium text-gray-600">Toplam Gelir</p>
<p className="text-lg font-bold text-gray-900"> <p className="text-lg font-bold text-gray-900">{revenue.toLocaleString()}</p>
{revenue.toLocaleString()}
</p>
</div> </div>
</div> </div>
</div> </div>
@ -187,42 +179,30 @@ const SalesTeamView: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-medium text-gray-600 mb-1"> <label className="block text-xs font-medium text-gray-600 mb-1">Ekip Kodu</label>
Ekip Kodu
</label>
<p className="text-gray-900">{team.code}</p> <p className="text-gray-900">{team.code}</p>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-600 mb-1"> <label className="block text-xs font-medium text-gray-600 mb-1">Durum</label>
Durum
</label>
<span <span
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${ className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
team.isActive team.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`} }`}
> >
{team.isActive ? "Aktif" : "Pasif"} {team.isActive ? 'Aktif' : 'Pasif'}
</span> </span>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-600 mb-1"> <label className="block text-xs font-medium text-gray-600 mb-1">ıklama</label>
ıklama <p className="text-gray-900">{team.description || 'Açıklama girilmemiş'}</p>
</label>
<p className="text-gray-900">
{team.description || "Açıklama girilmemiş"}
</p>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-600 mb-1"> <label className="block text-xs font-medium text-gray-600 mb-1">Yönetici</label>
Yönetici
</label>
<p className="text-gray-900"> <p className="text-gray-900">
{team.manager?.fullName || "Yönetici bilgisi girilmemiş"} {team.manager?.fullName || 'Yönetici bilgisi girilmemiş'}
</p> </p>
</div> </div>
@ -230,9 +210,7 @@ const SalesTeamView: React.FC = () => {
<label className="block text-xs font-medium text-gray-600 mb-1"> <label className="block text-xs font-medium text-gray-600 mb-1">
Oluşturulma Tarihi Oluşturulma Tarihi
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">{dayjs(team.creationTime).format('DD.MM.YYYY')}</p>
{dayjs(team.creationTime).format("DD.MM.YYYY")}
</p>
</div> </div>
<div> <div>
@ -240,7 +218,7 @@ const SalesTeamView: React.FC = () => {
Son Güncelleme Son Güncelleme
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">
{dayjs(team.lastModificationTime).format("DD.MM.YYYY")} {dayjs(team.lastModificationTime).format('DD.MM.YYYY')}
</p> </p>
</div> </div>
</div> </div>
@ -265,11 +243,9 @@ const SalesTeamView: React.FC = () => {
<FaUser className="w-5 h-5 text-blue-600" /> <FaUser className="w-5 h-5 text-blue-600" />
</div> </div>
<div> <div>
<p className="font-medium text-gray-900"> <p className="font-medium text-gray-900">{member.employee?.fullName}</p>
{member.employee?.fullName}
</p>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Katılım: {dayjs(member.joinDate).format("DD.MM.YYYY")} Katılım: {dayjs(member.joinDate).format('DD.MM.YYYY')}
</p> </p>
</div> </div>
</div> </div>
@ -277,16 +253,16 @@ const SalesTeamView: React.FC = () => {
<span <span
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${ className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
member.role === TeamRoleEnum.Manager member.role === TeamRoleEnum.Manager
? "bg-purple-100 text-purple-800" ? 'bg-purple-100 text-purple-800'
: member.role === TeamRoleEnum.Lead : member.role === TeamRoleEnum.Lead
? "bg-blue-100 text-blue-800" ? 'bg-blue-100 text-blue-800'
: "bg-gray-100 text-gray-800" : 'bg-gray-100 text-gray-800'
}`} }`}
> >
{getTeamRoleText(member.role)} {getTeamRoleText(member.role)}
</span> </span>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
{member.isActive ? "Aktif" : "Pasif"} {member.isActive ? 'Aktif' : 'Pasif'}
</p> </p>
</div> </div>
</div> </div>
@ -315,21 +291,15 @@ const SalesTeamView: React.FC = () => {
className="p-3 border border-gray-200 rounded-lg" className="p-3 border border-gray-200 rounded-lg"
> >
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<h4 className="font-medium text-gray-900"> <h4 className="font-medium text-gray-900">{territory.name}</h4>
{territory.name} <span className="text-sm text-gray-500">{territory.territoryCode}</span>
</h4>
<span className="text-sm text-gray-500">
{territory.territoryCode}
</span>
</div> </div>
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">{territory.description}</p>
{territory.description}
</p>
<div className="flex gap-4 text-sm text-gray-500"> <div className="flex gap-4 text-sm text-gray-500">
<span>Bölge: {territory.region}</span> <span>Bölge: {territory.region}</span>
<span>Ülke: {territory.countries?.join(", ")}</span> <span>Ülke: {territory.countries?.join(', ')}</span>
{territory.cities && territory.cities.length > 0 && ( {territory.cities && territory.cities.length > 0 && (
<span>Şehir: {territory.cities.join(", ")}</span> <span>Şehir: {territory.cities.join(', ')}</span>
)} )}
</div> </div>
</div> </div>
@ -358,10 +328,10 @@ const SalesTeamView: React.FC = () => {
<div <div
className={`text-3xl font-bold mb-2 ${ className={`text-3xl font-bold mb-2 ${
performance >= 100 performance >= 100
? "text-green-600" ? 'text-green-600'
: performance >= 80 : performance >= 80
? "text-yellow-600" ? 'text-yellow-600'
: "text-red-600" : 'text-red-600'
}`} }`}
> >
%{performance.toFixed(1)} %{performance.toFixed(1)}
@ -373,10 +343,10 @@ const SalesTeamView: React.FC = () => {
<div <div
className={`h-2.5 rounded-full ${ className={`h-2.5 rounded-full ${
performance >= 100 performance >= 100
? "bg-green-500" ? 'bg-green-500'
: performance >= 80 : performance >= 80
? "bg-yellow-500" ? 'bg-yellow-500'
: "bg-red-500" : 'bg-red-500'
}`} }`}
style={{ width: `${Math.min(performance, 100)}%` }} style={{ width: `${Math.min(performance, 100)}%` }}
/> />
@ -428,8 +398,8 @@ const SalesTeamView: React.FC = () => {
Başlangıç - Bitiş Başlangıç - Bitiş
</label> </label>
<p className="text-sm text-gray-900"> <p className="text-sm text-gray-900">
{dayjs(activeTarget.startDate).format("DD.MM.YYYY")} -{" "} {dayjs(activeTarget.startDate).format('DD.MM.YYYY')} -{' '}
{dayjs(activeTarget.endDate).format("DD.MM.YYYY")} {dayjs(activeTarget.endDate).format('DD.MM.YYYY')}
</p> </p>
</div> </div>
</div> </div>
@ -441,17 +411,13 @@ const SalesTeamView: React.FC = () => {
<div className="bg-gradient-to-r from-yellow-400 to-orange-500 rounded-lg p-4 text-center text-white"> <div className="bg-gradient-to-r from-yellow-400 to-orange-500 rounded-lg p-4 text-center text-white">
<FaAward className="w-10 h-10 mx-auto mb-2" /> <FaAward className="w-10 h-10 mx-auto mb-2" />
<h3 className="text-base font-bold mb-1">Tebrikler!</h3> <h3 className="text-base font-bold mb-1">Tebrikler!</h3>
<p className="text-sm"> <p className="text-sm">Bu ekip hedefini aştı ve başarı ödülünü hak etti!</p>
Bu ekip hedefini aştı ve başarı ödülünü hak etti!
</p>
</div> </div>
)} )}
{/* Quick Stats */} {/* Quick Stats */}
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3"> <h3 className="text-base font-semibold text-gray-900 mb-3">Hızlı İstatistikler</h3>
Hızlı İstatistikler
</h3>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between"> <div className="flex justify-between">
@ -467,10 +433,7 @@ const SalesTeamView: React.FC = () => {
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Kapsam Alanı:</span> <span className="text-gray-600">Kapsam Alanı:</span>
<span className="font-medium"> <span className="font-medium">
{team.territories?.reduce( {team.territories?.reduce((acc, t) => acc + (t.cities?.length || 0), 0) || 0}{' '}
(acc, t) => acc + (t.cities?.length || 0),
0
) || 0}{" "}
şehir şehir
</span> </span>
</div> </div>
@ -479,7 +442,8 @@ const SalesTeamView: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
); </Container>
}; )
}
export default SalesTeamView; export default SalesTeamView

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { import {
FaUsers, FaUsers,
FaPlus, FaPlus,
@ -12,96 +12,93 @@ import {
FaList, FaList,
FaSearch, FaSearch,
FaFilter, FaFilter,
} from "react-icons/fa"; } from 'react-icons/fa'
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom'
import DataTable, { Column } from "../../../components/common/DataTable"; import DataTable, { Column } from '../../../components/common/DataTable'
import mockSalesTeams from "../../../mocks/mockSalesTeams"; import mockSalesTeams from '../../../mocks/mockSalesTeams'
import Widget from "../../../components/common/Widget"; import Widget from '../../../components/common/Widget'
import { Team } from "../../../types/common"; import { Team } from '../../../types/common'
import { Container } from '@/components/shared'
const SalesTeams: React.FC = () => { const SalesTeams: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const [teams] = useState<Team[]>(mockSalesTeams); const [teams] = useState<Team[]>(mockSalesTeams)
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState('')
const [viewMode, setViewMode] = useState<"cards" | "list">("cards"); const [viewMode, setViewMode] = useState<'cards' | 'list'>('cards')
const [filterStatus, setFilterStatus] = useState< const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all')
"all" | "active" | "inactive"
>("all");
const handleAdd = () => { const handleAdd = () => {
navigate("/admin/crm/sales-teams/new"); navigate('/admin/crm/sales-teams/new')
}; }
const handleEdit = (team: Team) => { const handleEdit = (team: Team) => {
navigate(`/admin/crm/sales-teams/edit/${team.id}`); navigate(`/admin/crm/sales-teams/edit/${team.id}`)
}; }
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
const team = teams.find((t) => t.id === id); const team = teams.find((t) => t.id === id)
if (confirm(`${team?.name} ekibini silmek istediğinizden emin misiniz?`)) { if (confirm(`${team?.name} ekibini silmek istediğinizden emin misiniz?`)) {
console.log("Delete sales team:", id); console.log('Delete sales team:', id)
// Implement delete logic here // Implement delete logic here
alert("Ekip silindi (mock)"); alert('Ekip silindi (mock)')
}
} }
};
const handleViewDetails = (team: Team) => { const handleViewDetails = (team: Team) => {
navigate(`/admin/crm/sales-teams/${team.id}`); navigate(`/admin/crm/sales-teams/${team.id}`)
}; }
// Calculate team performance based on targets // Calculate team performance based on targets
const calculateTeamPerformance = (team: Team): number => { const calculateTeamPerformance = (team: Team): number => {
if (!team.targets || team.targets.length === 0) return 0; if (!team.targets || team.targets.length === 0) return 0
const activeTarget = team.targets.find((t) => t.status === "ACTIVE"); const activeTarget = team.targets.find((t) => t.status === 'ACTIVE')
if (!activeTarget || activeTarget.targetValue === 0) return 0; if (!activeTarget || activeTarget.targetValue === 0) return 0
return (activeTarget.actualValue / activeTarget.targetValue) * 100; return (activeTarget.actualValue / activeTarget.targetValue) * 100
}; }
// Calculate team revenue // Calculate team revenue
const calculateTeamRevenue = (team: Team): number => { const calculateTeamRevenue = (team: Team): number => {
if (!team.targets || team.targets.length === 0) return 0; if (!team.targets || team.targets.length === 0) return 0
return team.targets.reduce((sum, target) => sum + target.actualValue, 0); return team.targets.reduce((sum, target) => sum + target.actualValue, 0)
}; }
// Filter teams based on search and status // Filter teams based on search and status
const filteredTeams = teams.filter((team) => { const filteredTeams = teams.filter((team) => {
const matchesSearch = const matchesSearch =
searchTerm === "" || searchTerm === '' ||
team.name.toLowerCase().includes(searchTerm.toLowerCase()) || team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
team.code.toLowerCase().includes(searchTerm.toLowerCase()) || team.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
team.description?.toLowerCase().includes(searchTerm.toLowerCase()); team.description?.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = const matchesStatus =
filterStatus === "all" || filterStatus === 'all' ||
(filterStatus === "active" && team.isActive) || (filterStatus === 'active' && team.isActive) ||
(filterStatus === "inactive" && !team.isActive); (filterStatus === 'inactive' && !team.isActive)
return matchesSearch && matchesStatus; return matchesSearch && matchesStatus
}); })
// Table columns for list view // Table columns for list view
const columns: Column<Team>[] = [ const columns: Column<Team>[] = [
{ {
key: "teamCode", key: 'teamCode',
header: "Takım Kodu", header: 'Takım Kodu',
sortable: true, sortable: true,
}, },
{ {
key: "name", key: 'name',
header: "Takım Adı", header: 'Takım Adı',
sortable: true, sortable: true,
render: (team: Team) => ( render: (team: Team) => (
<div> <div>
<div className="font-medium text-gray-900">{team.name}</div> <div className="font-medium text-gray-900">{team.name}</div>
<div className="text-sm text-gray-500 truncate max-w-xs"> <div className="text-sm text-gray-500 truncate max-w-xs">{team.description}</div>
{team.description}
</div>
</div> </div>
), ),
}, },
{ {
key: "memberCount", key: 'memberCount',
header: "Üye Sayısı", header: 'Üye Sayısı',
render: (team: Team) => ( render: (team: Team) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FaUsers className="w-4 h-4 text-gray-500" /> <FaUsers className="w-4 h-4 text-gray-500" />
@ -110,8 +107,8 @@ const SalesTeams: React.FC = () => {
), ),
}, },
{ {
key: "territories", key: 'territories',
header: "Bölge Sayısı", header: 'Bölge Sayısı',
render: (team: Team) => ( render: (team: Team) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FaMapMarkerAlt className="w-4 h-4 text-gray-500" /> <FaMapMarkerAlt className="w-4 h-4 text-gray-500" />
@ -120,55 +117,53 @@ const SalesTeams: React.FC = () => {
), ),
}, },
{ {
key: "performance", key: 'performance',
header: "Performans", header: 'Performans',
render: (team: Team) => { render: (team: Team) => {
const performance = calculateTeamPerformance(team); const performance = calculateTeamPerformance(team)
const color = const color =
performance >= 100 performance >= 100
? "text-green-600" ? 'text-green-600'
: performance >= 80 : performance >= 80
? "text-yellow-600" ? 'text-yellow-600'
: "text-red-600"; : 'text-red-600'
return ( return (
<div className={`flex items-center gap-1 ${color}`}> <div className={`flex items-center gap-1 ${color}`}>
<FaArrowUp className="w-4 h-4" /> <FaArrowUp className="w-4 h-4" />
<span className="font-medium">%{performance.toFixed(1)}</span> <span className="font-medium">%{performance.toFixed(1)}</span>
</div> </div>
); )
}, },
}, },
{ {
key: "totalRevenue", key: 'totalRevenue',
header: "Toplam Gelir", header: 'Toplam Gelir',
render: (team: Team) => { render: (team: Team) => {
const revenue = calculateTeamRevenue(team); const revenue = calculateTeamRevenue(team)
return ( return (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-500" /> <FaDollarSign className="w-4 h-4 text-gray-500" />
<span>{revenue.toLocaleString()}</span> <span>{revenue.toLocaleString()}</span>
</div> </div>
); )
}, },
}, },
{ {
key: "status", key: 'status',
header: "Durum", header: 'Durum',
render: (team: Team) => ( render: (team: Team) => (
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${ className={`px-2 py-1 text-xs font-medium rounded-full ${
team.isActive team.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`} }`}
> >
{team.isActive ? "Aktif" : "Pasif"} {team.isActive ? 'Aktif' : 'Pasif'}
</span> </span>
), ),
}, },
{ {
key: "actions", key: 'actions',
header: "İşlemler", header: 'İşlemler',
render: (team: Team) => ( render: (team: Team) => (
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@ -195,33 +190,23 @@ const SalesTeams: React.FC = () => {
</div> </div>
), ),
}, },
]; ]
// Calculate overall statistics // Calculate overall statistics
const totalMembers = teams.reduce( const totalMembers = teams.reduce((sum, team) => sum + (team.members?.length || 0), 0)
(sum, team) => sum + (team.members?.length || 0), const totalRevenue = teams.reduce((sum, team) => sum + calculateTeamRevenue(team), 0)
0
);
const totalRevenue = teams.reduce(
(sum, team) => sum + calculateTeamRevenue(team),
0
);
const averagePerformance = const averagePerformance =
teams.reduce((sum, team) => sum + calculateTeamPerformance(team), 0) / teams.reduce((sum, team) => sum + calculateTeamPerformance(team), 0) / teams.length || 0
teams.length || 0; const topPerformingTeams = teams.filter((team) => calculateTeamPerformance(team) >= 100).length
const topPerformingTeams = teams.filter(
(team) => calculateTeamPerformance(team) >= 100
).length;
return ( return (
<Container>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-xl font-bold text-gray-900">Satış Ekipleri</h2> <h2 className="text-xl font-bold text-gray-900">Satış Ekipleri</h2>
<p className="text-gray-600 mt-1"> <p className="text-gray-600 mt-1">Satış ekipleri ve performans yönetimi</p>
Satış ekipleri ve performans yönetimi
</p>
</div> </div>
<button <button
onClick={handleAdd} onClick={handleAdd}
@ -234,19 +219,9 @@ const SalesTeams: React.FC = () => {
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Widget <Widget title="Toplam Ekip" value={teams.length} color="blue" icon="FaUsers" />
title="Toplam Ekip"
value={teams.length}
color="blue"
icon="FaUsers"
/>
<Widget <Widget title="Toplam Üye" value={totalMembers} color="green" icon="FaUser" />
title="Toplam Üye"
value={totalMembers}
color="green"
icon="FaUser"
/>
<Widget <Widget
title="Toplam Gelir" title="Toplam Gelir"
@ -265,9 +240,7 @@ const SalesTeams: React.FC = () => {
{/* Performance Overview */} {/* Performance Overview */}
<div className="bg-white rounded-lg shadow-sm border p-4"> <div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3"> <h3 className="text-base font-semibold text-gray-900 mb-3">Performans Özeti</h3>
Performans Özeti
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1"> <div className="text-2xl font-bold text-blue-600 mb-1">
@ -277,9 +250,7 @@ const SalesTeams: React.FC = () => {
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-green-600 mb-1"> <div className="text-2xl font-bold text-green-600 mb-1">{topPerformingTeams}</div>
{topPerformingTeams}
</div>
<p className="text-sm text-gray-600">Hedef Aşan Ekip</p> <p className="text-sm text-gray-600">Hedef Aşan Ekip</p>
</div> </div>
@ -312,9 +283,7 @@ const SalesTeams: React.FC = () => {
<FaFilter className="w-4 h-4 text-gray-400" /> <FaFilter className="w-4 h-4 text-gray-400" />
<select <select
value={filterStatus} value={filterStatus}
onChange={(e) => onChange={(e) => setFilterStatus(e.target.value as 'all' | 'active' | 'inactive')}
setFilterStatus(e.target.value as "all" | "active" | "inactive")
}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="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="all">Tüm Durumlar</option> <option value="all">Tüm Durumlar</option>
@ -326,21 +295,21 @@ const SalesTeams: React.FC = () => {
{/* View Mode Toggle */} {/* View Mode Toggle */}
<div className="flex bg-gray-100 rounded-lg p-1"> <div className="flex bg-gray-100 rounded-lg p-1">
<button <button
onClick={() => setViewMode("list")} onClick={() => setViewMode('list')}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${ className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
viewMode === "list" viewMode === 'list'
? "bg-white text-blue-600 shadow-sm" ? 'bg-white text-blue-600 shadow-sm'
: "text-gray-600 hover:text-gray-900" : 'text-gray-600 hover:text-gray-900'
}`} }`}
> >
<FaList className="w-4 h-4" /> <FaList className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => setViewMode("cards")} onClick={() => setViewMode('cards')}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${ className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
viewMode === "cards" viewMode === 'cards'
? "bg-white text-blue-600 shadow-sm" ? 'bg-white text-blue-600 shadow-sm'
: "text-gray-600 hover:text-gray-900" : 'text-gray-600 hover:text-gray-900'
}`} }`}
> >
<FaTh className="w-4 h-4" /> <FaTh className="w-4 h-4" />
@ -350,23 +319,18 @@ const SalesTeams: React.FC = () => {
</div> </div>
{/* Content */} {/* Content */}
{viewMode === "cards" ? ( {viewMode === 'cards' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredTeams.map((team) => { {filteredTeams.map((team) => {
const performance = calculateTeamPerformance(team); const performance = calculateTeamPerformance(team)
const revenue = calculateTeamRevenue(team); const revenue = calculateTeamRevenue(team)
return ( return (
<div <div key={team.id} className="bg-white rounded-lg shadow-sm border p-4">
key={team.id}
className="bg-white rounded-lg shadow-sm border p-4"
>
{/* Card Header */} {/* Card Header */}
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex-1"> <div className="flex-1">
<h3 className="text-base font-semibold text-gray-900"> <h3 className="text-base font-semibold text-gray-900">{team.name}</h3>
{team.name}
</h3>
<p className="text-sm text-gray-500">{team.code}</p> <p className="text-sm text-gray-500">{team.code}</p>
{team.description && ( {team.description && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2"> <p className="text-sm text-gray-600 mt-1 line-clamp-2">
@ -376,12 +340,10 @@ const SalesTeams: React.FC = () => {
</div> </div>
<span <span
className={`px-2 py-1 text-xs font-medium rounded-full ${ className={`px-2 py-1 text-xs font-medium rounded-full ${
team.isActive team.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`} }`}
> >
{team.isActive ? "Aktif" : "Pasif"} {team.isActive ? 'Aktif' : 'Pasif'}
</span> </span>
</div> </div>
@ -392,9 +354,7 @@ const SalesTeams: React.FC = () => {
<FaUsers className="w-4 h-4 text-gray-400" /> <FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Yönetici</span> <span className="text-sm text-gray-600">Yönetici</span>
</div> </div>
<span className="font-medium"> <span className="font-medium">{team.manager?.fullName}</span>
{team.manager?.fullName}
</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -402,9 +362,7 @@ const SalesTeams: React.FC = () => {
<FaUsers className="w-4 h-4 text-gray-400" /> <FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Üye Sayısı</span> <span className="text-sm text-gray-600">Üye Sayısı</span>
</div> </div>
<span className="font-medium"> <span className="font-medium">{team.members?.length || 0}</span>
{team.members?.length || 0}
</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -412,9 +370,7 @@ const SalesTeams: React.FC = () => {
<FaMapMarkerAlt className="w-4 h-4 text-gray-400" /> <FaMapMarkerAlt className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Bölge</span> <span className="text-sm text-gray-600">Bölge</span>
</div> </div>
<span className="font-medium"> <span className="font-medium">{team.territories?.length || 0}</span>
{team.territories?.length || 0}
</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -425,10 +381,10 @@ const SalesTeams: React.FC = () => {
<span <span
className={`font-medium ${ className={`font-medium ${
performance >= 100 performance >= 100
? "text-green-600" ? 'text-green-600'
: performance >= 80 : performance >= 80
? "text-yellow-600" ? 'text-yellow-600'
: "text-red-600" : 'text-red-600'
}`} }`}
> >
%{performance.toFixed(1)} %{performance.toFixed(1)}
@ -440,9 +396,7 @@ const SalesTeams: React.FC = () => {
<FaDollarSign className="w-4 h-4 text-gray-400" /> <FaDollarSign className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Gelir</span> <span className="text-sm text-gray-600">Gelir</span>
</div> </div>
<span className="font-medium"> <span className="font-medium">{revenue.toLocaleString()}</span>
{revenue.toLocaleString()}
</span>
</div> </div>
</div> </div>
@ -469,7 +423,7 @@ const SalesTeams: React.FC = () => {
</button> </button>
</div> </div>
</div> </div>
); )
})} })}
</div> </div>
) : ( ) : (
@ -481,16 +435,13 @@ const SalesTeams: React.FC = () => {
{filteredTeams.length === 0 && ( {filteredTeams.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<FaUsers className="w-10 h-10 text-gray-400 mx-auto mb-3" /> <FaUsers className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-base font-medium text-gray-900 mb-2"> <h3 className="text-base font-medium text-gray-900 mb-2">Ekip bulunamadı</h3>
Ekip bulunamadı <p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
</div> </div>
)} )}
</div> </div>
); </Container>
}; )
}
export default SalesTeams; export default SalesTeams