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

View file

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

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";
import React, { useState, useEffect, useCallback } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import {
FaSave,
FaTimes,
@ -8,553 +8,499 @@ import {
FaMapMarkerAlt,
FaCreditCard,
FaEnvelope,
} from "react-icons/fa";
import LoadingSpinner from "../../../components/common/LoadingSpinner";
import {
mockBusinessParties,
mockBusinessPartyNew,
} from "../../../mocks/mockBusinessParties";
import { BusinessParty } from "../../../types/common";
} from 'react-icons/fa'
import LoadingSpinner from '../../../components/common/LoadingSpinner'
import { mockBusinessParties, mockBusinessPartyNew } from '../../../mocks/mockBusinessParties'
import { BusinessParty } from '../../../types/common'
import { Container } from '@/components/shared'
interface ValidationErrors {
[key: string]: string;
[key: string]: string
}
const CustomerForm: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const isEdit = Boolean(id);
const navigate = useNavigate()
const { id } = useParams<{ id: string }>()
const isEdit = Boolean(id)
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [errors, setErrors] = useState<ValidationErrors>({});
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [errors, setErrors] = useState<ValidationErrors>({})
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew);
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew)
const loadFormData = useCallback(async () => {
setLoading(true);
setLoading(true)
try {
if (isEdit && id) {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000))
// Mock customer data
const mockCustomer = mockBusinessParties.find(
(cust) => cust.id === id
)!;
setFormData(mockCustomer);
const mockCustomer = mockBusinessParties.find((cust) => cust.id === id)!
setFormData(mockCustomer)
}
} catch (error) {
console.error("Error loading form data:", error);
console.error('Error loading form data:', error)
} finally {
setLoading(false);
setLoading(false)
}
}, [isEdit, id]);
}, [isEdit, id])
useEffect(() => {
loadFormData();
}, [loadFormData]);
loadFormData()
}, [loadFormData])
const validateForm = (): boolean => {
const newErrors: ValidationErrors = {};
const newErrors: ValidationErrors = {}
if (!formData.code.trim()) {
newErrors.code = "Müşteri kodu zorunludur";
newErrors.code = 'Müşteri kodu zorunludur'
}
if (!formData.name.trim()) {
newErrors.name = "Şirket adı zorunludur";
newErrors.name = 'Şirket adı zorunludur'
}
if (formData.creditLimit < 0) {
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz";
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz"
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleInputChange = (
field: keyof BusinessParty,
value: string | number | boolean
) => {
const handleInputChange = (field: keyof BusinessParty, value: string | number | boolean) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
}))
// Clear error when user starts typing
if (errors[field]) {
setErrors((prev) => ({
...prev,
[field]: "",
}));
[field]: '',
}))
}
};
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!validateForm()) {
return;
return
}
setSaving(true);
setSaving(true)
try {
// 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,
id: isEdit ? id : undefined,
});
})
// Show success message
alert(
isEdit
? "Müşteri başarıyla güncellendi!"
: "Müşteri başarıyla oluşturuldu!"
);
alert(isEdit ? 'Müşteri başarıyla güncellendi!' : 'Müşteri başarıyla oluşturuldu!')
// Navigate back to list
navigate("/admin/crm");
navigate('/admin/crm')
} catch (error) {
console.error("Error saving customer:", error);
alert("Bir hata oluştu. Lütfen tekrar deneyin.");
console.error('Error saving customer:', error)
alert('Bir hata oluştu. Lütfen tekrar deneyin.')
} finally {
setSaving(false);
setSaving(false)
}
};
}
const handleCancel = () => {
navigate("/admin/crm");
};
navigate('/admin/crm')
}
if (loading) {
return <LoadingSpinner />;
return <LoadingSpinner />
}
return (
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900">
{isEdit ? "Müşteri Düzenle" : "Yeni Müşteri"}
</h2>
<p className="text-sm text-gray-600">
{isEdit
? "Mevcut müşteri bilgilerini güncelleyin"
: "Yeni müşteri bilgilerini girin"}
</p>
<Container>
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900">
{isEdit ? 'Müşteri Düzenle' : 'Yeni Müşteri'}
</h2>
<p className="text-sm text-gray-600">
{isEdit ? 'Mevcut müşteri bilgilerini güncelleyin' : 'Yeni müşteri bilgilerini girin'}
</p>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-3">
{/* General Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaBuilding className="w-5 h-5 mr-2" />
Genel Bilgiler
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Kodu *
</label>
<input
type="text"
value={formData.code}
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 ${
errors.code
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="Örn: MST001"
/>
{errors.customerCode && (
<p className="mt-1 text-sm text-red-600">{errors.customerCode}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Tipi
</label>
<select
value={formData.customerType}
onChange={(e) =>
handleInputChange('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"
>
<option value="COMPANY">Şirket</option>
<option value="INDIVIDUAL">Bireysel</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Şirket Adı *
</label>
<input
type="text"
value={formData.name}
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 ${
errors.name
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="Şirket adını girin"
/>
{errors.companyName && (
<p className="mt-1 text-sm text-red-600">{errors.companyName}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Sektör</label>
<select
value={formData.industry}
onChange={(e) => 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"
>
<option value="">Sektör seçin</option>
<option value="TECHNOLOGY">Teknoloji</option>
<option value="MANUFACTURING">İmalat</option>
<option value="CONSTRUCTION">İnşaat</option>
<option value="AUTOMOTIVE">Otomotiv</option>
<option value="RETAIL">Perakende</option>
<option value="FINANCE">Finans</option>
<option value="HEALTHCARE">Sağlık</option>
<option value="OTHER">Diğer</option>
</select>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Website</label>
<input
type="url"
value={formData.website}
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"
placeholder="https://www.ornek.com"
/>
</div>
</div>
</div>
{/* Contact Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaUser className="w-5 h-5 mr-2" />
İletişim Bilgileri
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Email *</label>
<input
type="email"
value={formData.email}
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 ${
errors.email
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="email@ornek.com"
/>
{errors.email && <p className="mt-1 text-sm text-red-600">{errors.email}</p>}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Telefon *</label>
<input
type="tel"
value={formData.phone}
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 ${
errors.phone
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="+90 212 555 0123"
/>
{errors.phone && <p className="mt-1 text-sm text-red-600">{errors.phone}</p>}
</div>
</div>
</div>
</div>
{/* Address Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Adres Bilgileri
</h3>
</div>
</div>
{/* Business Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaCreditCard className="w-5 h-5 mr-2" />
İş Bilgileri
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Vergi Numarası
</label>
<input
type="text"
value={formData.taxNumber}
onChange={(e) => 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"
placeholder="1234567890"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Ticaret Sicil No
</label>
<input
type="text"
value={formData.registrationNumber}
onChange={(e) => 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"
placeholder="98765"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Kredi Limiti
</label>
<input
type="number"
min="0"
step="0.01"
value={formData.creditLimit}
onChange={(e) =>
handleInputChange('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 ${
errors.creditLimit
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="0.00"
/>
{errors.creditLimit && (
<p className="mt-1 text-sm text-red-600">{errors.creditLimit}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Ödeme Koşulları
</label>
<select
value={formData.paymentTerms}
onChange={(e) => 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"
>
<option value="CASH">Peşin</option>
<option value="NET_15">15 Gün Vadeli</option>
<option value="NET_30">30 Gün Vadeli</option>
<option value="NET_60">60 Gün Vadeli</option>
<option value="NET_90">90 Gün Vadeli</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Para Birimi
</label>
<select
value={formData.currency}
onChange={(e) => 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"
>
<option value="TRY">Türk Lirası (TRY)</option>
<option value="USD">Amerikan Doları (USD)</option>
<option value="EUR">Euro (EUR)</option>
</select>
</div>
</div>
</div>
</div>
{/* Relationship Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaEnvelope className="w-5 h-5 mr-2" />
İlişki Yönetimi
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Durumu
</label>
<select
value={formData.status}
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"
>
<option value="ACTIVE">Aktif</option>
<option value="INACTIVE">Pasif</option>
<option value="PROSPECT">Potansiyel</option>
<option value="BLOCKED">Blokeli</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Segmenti
</label>
<select
value={formData.customerSegment}
onChange={(e) => 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"
>
<option value="STANDARD">Standart</option>
<option value="PREMIUM">Premium</option>
<option value="ENTERPRISE">Kurumsal</option>
<option value="VIP">VIP</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Sorumlu Satış Temsilcisi
</label>
<select
value={formData.assignedSalesRep}
onChange={(e) => 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"
>
<option value="">Temsilci seçin</option>
<option value="sales_rep_1">Mehmet Özkan</option>
<option value="sales_rep_2">Ayşe Demir</option>
<option value="sales_rep_3">Ali Yılmaz</option>
</select>
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="isActive"
checked={formData.isActive}
onChange={(e) => handleInputChange('isActive', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
Aktif
</label>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex items-center justify-end space-x-2 bg-gray-50 px-4 py-3 rounded-b-lg">
<button
type="button"
onClick={handleCancel}
className="inline-flex items-center px-4 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<FaTimes className="w-4 h-4 mr-2" />
İptal
</button>
<button
type="submit"
disabled={saving}
className="inline-flex items-center px-4 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Kaydediliyor...
</>
) : (
<>
<FaSave className="w-4 h-4 mr-2" />
{isEdit ? 'Güncelle' : 'Kaydet'}
</>
)}
</button>
</div>
</form>
</div>
</Container>
)
}
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-3">
{/* General Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaBuilding className="w-5 h-5 mr-2" />
Genel Bilgiler
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Kodu *
</label>
<input
type="text"
value={formData.code}
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 ${
errors.code
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="Örn: MST001"
/>
{errors.customerCode && (
<p className="mt-1 text-sm text-red-600">
{errors.customerCode}
</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Tipi
</label>
<select
value={formData.customerType}
onChange={(e) =>
handleInputChange(
"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"
>
<option value="COMPANY">Şirket</option>
<option value="INDIVIDUAL">Bireysel</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Şirket Adı *
</label>
<input
type="text"
value={formData.name}
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 ${
errors.name
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="Şirket adını girin"
/>
{errors.companyName && (
<p className="mt-1 text-sm text-red-600">
{errors.companyName}
</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Sektör
</label>
<select
value={formData.industry}
onChange={(e) =>
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"
>
<option value="">Sektör seçin</option>
<option value="TECHNOLOGY">Teknoloji</option>
<option value="MANUFACTURING">İmalat</option>
<option value="CONSTRUCTION">İnşaat</option>
<option value="AUTOMOTIVE">Otomotiv</option>
<option value="RETAIL">Perakende</option>
<option value="FINANCE">Finans</option>
<option value="HEALTHCARE">Sağlık</option>
<option value="OTHER">Diğer</option>
</select>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Website
</label>
<input
type="url"
value={formData.website}
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"
placeholder="https://www.ornek.com"
/>
</div>
</div>
</div>
{/* Contact Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaUser className="w-5 h-5 mr-2" />
İletişim Bilgileri
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Email *
</label>
<input
type="email"
value={formData.email}
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 ${
errors.email
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="email@ornek.com"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Telefon *
</label>
<input
type="tel"
value={formData.phone}
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 ${
errors.phone
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="+90 212 555 0123"
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-600">{errors.phone}</p>
)}
</div>
</div>
</div>
</div>
{/* Address Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Adres Bilgileri
</h3>
</div>
</div>
{/* Business Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaCreditCard className="w-5 h-5 mr-2" />
İş Bilgileri
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Vergi Numarası
</label>
<input
type="text"
value={formData.taxNumber}
onChange={(e) =>
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"
placeholder="1234567890"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Ticaret Sicil No
</label>
<input
type="text"
value={formData.registrationNumber}
onChange={(e) =>
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"
placeholder="98765"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Kredi Limiti
</label>
<input
type="number"
min="0"
step="0.01"
value={formData.creditLimit}
onChange={(e) =>
handleInputChange(
"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 ${
errors.creditLimit
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="0.00"
/>
{errors.creditLimit && (
<p className="mt-1 text-sm text-red-600">
{errors.creditLimit}
</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Ödeme Koşulları
</label>
<select
value={formData.paymentTerms}
onChange={(e) =>
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"
>
<option value="CASH">Peşin</option>
<option value="NET_15">15 Gün Vadeli</option>
<option value="NET_30">30 Gün Vadeli</option>
<option value="NET_60">60 Gün Vadeli</option>
<option value="NET_90">90 Gün Vadeli</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Para Birimi
</label>
<select
value={formData.currency}
onChange={(e) =>
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"
>
<option value="TRY">Türk Lirası (TRY)</option>
<option value="USD">Amerikan Doları (USD)</option>
<option value="EUR">Euro (EUR)</option>
</select>
</div>
</div>
</div>
</div>
{/* Relationship Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaEnvelope className="w-5 h-5 mr-2" />
İlişki Yönetimi
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Durumu
</label>
<select
value={formData.status}
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"
>
<option value="ACTIVE">Aktif</option>
<option value="INACTIVE">Pasif</option>
<option value="PROSPECT">Potansiyel</option>
<option value="BLOCKED">Blokeli</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Segmenti
</label>
<select
value={formData.customerSegment}
onChange={(e) =>
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"
>
<option value="STANDARD">Standart</option>
<option value="PREMIUM">Premium</option>
<option value="ENTERPRISE">Kurumsal</option>
<option value="VIP">VIP</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Sorumlu Satış Temsilcisi
</label>
<select
value={formData.assignedSalesRep}
onChange={(e) =>
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"
>
<option value="">Temsilci seçin</option>
<option value="sales_rep_1">Mehmet Özkan</option>
<option value="sales_rep_2">Ayşe Demir</option>
<option value="sales_rep_3">Ali Yılmaz</option>
</select>
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="isActive"
checked={formData.isActive}
onChange={(e) =>
handleInputChange("isActive", e.target.checked)
}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label
htmlFor="isActive"
className="ml-2 block text-sm text-gray-900"
>
Aktif
</label>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex items-center justify-end space-x-2 bg-gray-50 px-4 py-3 rounded-b-lg">
<button
type="button"
onClick={handleCancel}
className="inline-flex items-center px-4 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<FaTimes className="w-4 h-4 mr-2" />
İptal
</button>
<button
type="submit"
disabled={saving}
className="inline-flex items-center px-4 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Kaydediliyor...
</>
) : (
<>
<FaSave className="w-4 h-4 mr-2" />
{isEdit ? "Güncelle" : "Kaydet"}
</>
)}
</button>
</div>
</form>
</div>
);
};
export default CustomerForm;
export default CustomerForm

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";
import React, { useState, useEffect, useCallback } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import {
FaSave,
FaTimes,
@ -8,546 +8,497 @@ import {
FaMapMarkerAlt,
FaCreditCard,
FaEnvelope,
} from "react-icons/fa";
import LoadingSpinner from "../../../components/common/LoadingSpinner";
import { BusinessParty } from "../../../types/common";
import { mockBusinessPartyNew } from "../../../mocks/mockBusinessParties";
} from 'react-icons/fa'
import LoadingSpinner from '../../../components/common/LoadingSpinner'
import { BusinessParty } from '../../../types/common'
import { mockBusinessPartyNew } from '../../../mocks/mockBusinessParties'
import { Container } from '@/components/shared'
interface ValidationErrors {
[key: string]: string;
[key: string]: string
}
const CustomerForm: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const isEdit = Boolean(id);
const navigate = useNavigate()
const { id } = useParams<{ id: string }>()
const isEdit = Boolean(id)
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [errors, setErrors] = useState<ValidationErrors>({});
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [errors, setErrors] = useState<ValidationErrors>({})
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew);
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew)
const loadFormData = useCallback(async () => {
setLoading(true);
setLoading(true)
try {
if (isEdit && id) {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000))
setFormData(mockBusinessPartyNew);
setFormData(mockBusinessPartyNew)
}
} catch (error) {
console.error("Error loading form data:", error);
console.error('Error loading form data:', error)
} finally {
setLoading(false);
setLoading(false)
}
}, [isEdit, id]);
}, [isEdit, id])
useEffect(() => {
loadFormData();
}, [loadFormData]);
loadFormData()
}, [loadFormData])
const validateForm = (): boolean => {
const newErrors: ValidationErrors = {};
const newErrors: ValidationErrors = {}
if (!formData.code.trim()) {
newErrors.code = "Müşteri kodu zorunludur";
newErrors.code = 'Müşteri kodu zorunludur'
}
if (!formData.name.trim()) {
newErrors.name = "Şirket adı zorunludur";
newErrors.name = 'Şirket adı zorunludur'
}
if (formData.creditLimit < 0) {
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz";
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz"
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleInputChange = (
field: keyof BusinessParty,
value: string | number | boolean
) => {
const handleInputChange = (field: keyof BusinessParty, value: string | number | boolean) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
}))
// Clear error when user starts typing
if (errors[field]) {
setErrors((prev) => ({
...prev,
[field]: "",
}));
[field]: '',
}))
}
};
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!validateForm()) {
return;
return
}
setSaving(true);
setSaving(true)
try {
// 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,
id: isEdit ? id : undefined,
});
})
// Show success message
alert(
isEdit
? "Müşteri başarıyla güncellendi!"
: "Müşteri başarıyla oluşturuldu!"
);
alert(isEdit ? 'Müşteri başarıyla güncellendi!' : 'Müşteri başarıyla oluşturuldu!')
// Navigate back to list
navigate("/admin/crm");
navigate('/admin/crm')
} catch (error) {
console.error("Error saving customer:", error);
alert("Bir hata oluştu. Lütfen tekrar deneyin.");
console.error('Error saving customer:', error)
alert('Bir hata oluştu. Lütfen tekrar deneyin.')
} finally {
setSaving(false);
setSaving(false)
}
};
}
const handleCancel = () => {
navigate("/admin/crm");
};
navigate('/admin/crm')
}
if (loading) {
return <LoadingSpinner />;
return <LoadingSpinner />
}
return (
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900">
{isEdit ? "Müşteri Düzenle" : "Yeni Müşteri"}
</h2>
<p className="text-sm text-gray-600">
{isEdit
? "Mevcut müşteri bilgilerini güncelleyin"
: "Yeni müşteri bilgilerini girin"}
</p>
<Container>
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900">
{isEdit ? 'Müşteri Düzenle' : 'Yeni Müşteri'}
</h2>
<p className="text-sm text-gray-600">
{isEdit ? 'Mevcut müşteri bilgilerini güncelleyin' : 'Yeni müşteri bilgilerini girin'}
</p>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-3">
{/* General Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaBuilding className="w-5 h-5 mr-2" />
Genel Bilgiler
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Kodu *
</label>
<input
type="text"
value={formData.code}
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 ${
errors.code
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="Örn: MST001"
/>
{errors.customerCode && (
<p className="mt-1 text-sm text-red-600">{errors.customerCode}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Tipi
</label>
<select
value={formData.customerType}
onChange={(e) =>
handleInputChange('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"
>
<option value="COMPANY">Şirket</option>
<option value="INDIVIDUAL">Bireysel</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Şirket Adı *
</label>
<input
type="text"
value={formData.name}
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 ${
errors.name
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="Şirket adını girin"
/>
{errors.companyName && (
<p className="mt-1 text-sm text-red-600">{errors.companyName}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Sektör</label>
<select
value={formData.industry}
onChange={(e) => 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"
>
<option value="">Sektör seçin</option>
<option value="TECHNOLOGY">Teknoloji</option>
<option value="MANUFACTURING">İmalat</option>
<option value="CONSTRUCTION">İnşaat</option>
<option value="AUTOMOTIVE">Otomotiv</option>
<option value="RETAIL">Perakende</option>
<option value="FINANCE">Finans</option>
<option value="HEALTHCARE">Sağlık</option>
<option value="OTHER">Diğer</option>
</select>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Website</label>
<input
type="url"
value={formData.website}
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"
placeholder="https://www.ornek.com"
/>
</div>
</div>
</div>
{/* Contact Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaUser className="w-5 h-5 mr-2" />
İletişim Bilgileri
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Email *</label>
<input
type="email"
value={formData.email}
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 ${
errors.email
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="email@ornek.com"
/>
{errors.email && <p className="mt-1 text-sm text-red-600">{errors.email}</p>}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Telefon *</label>
<input
type="tel"
value={formData.phone}
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 ${
errors.phone
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="+90 212 555 0123"
/>
{errors.phone && <p className="mt-1 text-sm text-red-600">{errors.phone}</p>}
</div>
</div>
</div>
</div>
{/* Address Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Adres Bilgileri
</h3>
</div>
</div>
{/* Business Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaCreditCard className="w-5 h-5 mr-2" />
İş Bilgileri
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Vergi Numarası
</label>
<input
type="text"
value={formData.taxNumber}
onChange={(e) => 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"
placeholder="1234567890"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Ticaret Sicil No
</label>
<input
type="text"
value={formData.registrationNumber}
onChange={(e) => 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"
placeholder="98765"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Kredi Limiti
</label>
<input
type="number"
min="0"
step="0.01"
value={formData.creditLimit}
onChange={(e) =>
handleInputChange('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 ${
errors.creditLimit
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}`}
placeholder="0.00"
/>
{errors.creditLimit && (
<p className="mt-1 text-sm text-red-600">{errors.creditLimit}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Ödeme Koşulları
</label>
<select
value={formData.paymentTerms}
onChange={(e) => 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"
>
<option value="CASH">Peşin</option>
<option value="NET_15">15 Gün Vadeli</option>
<option value="NET_30">30 Gün Vadeli</option>
<option value="NET_60">60 Gün Vadeli</option>
<option value="NET_90">90 Gün Vadeli</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Para Birimi
</label>
<select
value={formData.currency}
onChange={(e) => 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"
>
<option value="TRY">Türk Lirası (TRY)</option>
<option value="USD">Amerikan Doları (USD)</option>
<option value="EUR">Euro (EUR)</option>
</select>
</div>
</div>
</div>
</div>
{/* Relationship Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaEnvelope className="w-5 h-5 mr-2" />
İlişki Yönetimi
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Durumu
</label>
<select
value={formData.status}
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"
>
<option value="ACTIVE">Aktif</option>
<option value="INACTIVE">Pasif</option>
<option value="PROSPECT">Potansiyel</option>
<option value="BLOCKED">Blokeli</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Segmenti
</label>
<select
value={formData.customerSegment}
onChange={(e) => 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"
>
<option value="STANDARD">Standart</option>
<option value="PREMIUM">Premium</option>
<option value="ENTERPRISE">Kurumsal</option>
<option value="VIP">VIP</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Sorumlu Satış Temsilcisi
</label>
<select
value={formData.assignedSalesRep}
onChange={(e) => 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"
>
<option value="">Temsilci seçin</option>
<option value="sales_rep_1">Mehmet Özkan</option>
<option value="sales_rep_2">Ayşe Demir</option>
<option value="sales_rep_3">Ali Yılmaz</option>
</select>
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="isActive"
checked={formData.isActive}
onChange={(e) => handleInputChange('isActive', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
Aktif
</label>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex items-center justify-end space-x-2 bg-gray-50 px-4 py-3 rounded-b-lg">
<button
type="button"
onClick={handleCancel}
className="inline-flex items-center px-4 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<FaTimes className="w-4 h-4 mr-2" />
İptal
</button>
<button
type="submit"
disabled={saving}
className="inline-flex items-center px-4 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Kaydediliyor...
</>
) : (
<>
<FaSave className="w-4 h-4 mr-2" />
{isEdit ? 'Güncelle' : 'Kaydet'}
</>
)}
</button>
</div>
</form>
</div>
</Container>
)
}
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-3">
{/* General Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaBuilding className="w-5 h-5 mr-2" />
Genel Bilgiler
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Kodu *
</label>
<input
type="text"
value={formData.code}
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 ${
errors.code
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="Örn: MST001"
/>
{errors.customerCode && (
<p className="mt-1 text-sm text-red-600">
{errors.customerCode}
</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Tipi
</label>
<select
value={formData.customerType}
onChange={(e) =>
handleInputChange(
"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"
>
<option value="COMPANY">Şirket</option>
<option value="INDIVIDUAL">Bireysel</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Şirket Adı *
</label>
<input
type="text"
value={formData.name}
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 ${
errors.name
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="Şirket adını girin"
/>
{errors.companyName && (
<p className="mt-1 text-sm text-red-600">
{errors.companyName}
</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Sektör
</label>
<select
value={formData.industry}
onChange={(e) =>
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"
>
<option value="">Sektör seçin</option>
<option value="TECHNOLOGY">Teknoloji</option>
<option value="MANUFACTURING">İmalat</option>
<option value="CONSTRUCTION">İnşaat</option>
<option value="AUTOMOTIVE">Otomotiv</option>
<option value="RETAIL">Perakende</option>
<option value="FINANCE">Finans</option>
<option value="HEALTHCARE">Sağlık</option>
<option value="OTHER">Diğer</option>
</select>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Website
</label>
<input
type="url"
value={formData.website}
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"
placeholder="https://www.ornek.com"
/>
</div>
</div>
</div>
{/* Contact Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaUser className="w-5 h-5 mr-2" />
İletişim Bilgileri
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Email *
</label>
<input
type="email"
value={formData.email}
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 ${
errors.email
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="email@ornek.com"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Telefon *
</label>
<input
type="tel"
value={formData.phone}
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 ${
errors.phone
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="+90 212 555 0123"
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-600">{errors.phone}</p>
)}
</div>
</div>
</div>
</div>
{/* Address Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Adres Bilgileri
</h3>
</div>
</div>
{/* Business Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaCreditCard className="w-5 h-5 mr-2" />
İş Bilgileri
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Vergi Numarası
</label>
<input
type="text"
value={formData.taxNumber}
onChange={(e) =>
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"
placeholder="1234567890"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Ticaret Sicil No
</label>
<input
type="text"
value={formData.registrationNumber}
onChange={(e) =>
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"
placeholder="98765"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Kredi Limiti
</label>
<input
type="number"
min="0"
step="0.01"
value={formData.creditLimit}
onChange={(e) =>
handleInputChange(
"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 ${
errors.creditLimit
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
placeholder="0.00"
/>
{errors.creditLimit && (
<p className="mt-1 text-sm text-red-600">
{errors.creditLimit}
</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Ödeme Koşulları
</label>
<select
value={formData.paymentTerms}
onChange={(e) =>
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"
>
<option value="CASH">Peşin</option>
<option value="NET_15">15 Gün Vadeli</option>
<option value="NET_30">30 Gün Vadeli</option>
<option value="NET_60">60 Gün Vadeli</option>
<option value="NET_90">90 Gün Vadeli</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Para Birimi
</label>
<select
value={formData.currency}
onChange={(e) =>
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"
>
<option value="TRY">Türk Lirası (TRY)</option>
<option value="USD">Amerikan Doları (USD)</option>
<option value="EUR">Euro (EUR)</option>
</select>
</div>
</div>
</div>
</div>
{/* Relationship Information */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900 flex items-center">
<FaEnvelope className="w-5 h-5 mr-2" />
İlişki Yönetimi
</h3>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Durumu
</label>
<select
value={formData.status}
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"
>
<option value="ACTIVE">Aktif</option>
<option value="INACTIVE">Pasif</option>
<option value="PROSPECT">Potansiyel</option>
<option value="BLOCKED">Blokeli</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Müşteri Segmenti
</label>
<select
value={formData.customerSegment}
onChange={(e) =>
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"
>
<option value="STANDARD">Standart</option>
<option value="PREMIUM">Premium</option>
<option value="ENTERPRISE">Kurumsal</option>
<option value="VIP">VIP</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Sorumlu Satış Temsilcisi
</label>
<select
value={formData.assignedSalesRep}
onChange={(e) =>
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"
>
<option value="">Temsilci seçin</option>
<option value="sales_rep_1">Mehmet Özkan</option>
<option value="sales_rep_2">Ayşe Demir</option>
<option value="sales_rep_3">Ali Yılmaz</option>
</select>
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="isActive"
checked={formData.isActive}
onChange={(e) =>
handleInputChange("isActive", e.target.checked)
}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label
htmlFor="isActive"
className="ml-2 block text-sm text-gray-900"
>
Aktif
</label>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex items-center justify-end space-x-2 bg-gray-50 px-4 py-3 rounded-b-lg">
<button
type="button"
onClick={handleCancel}
className="inline-flex items-center px-4 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<FaTimes className="w-4 h-4 mr-2" />
İptal
</button>
<button
type="submit"
disabled={saving}
className="inline-flex items-center px-4 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Kaydediliyor...
</>
) : (
<>
<FaSave className="w-4 h-4 mr-2" />
{isEdit ? "Güncelle" : "Kaydet"}
</>
)}
</button>
</div>
</form>
</div>
);
};
export default CustomerForm;
export default CustomerForm

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,14 +1,14 @@
import React, { useState, useEffect } from "react";
import { FaTimes, FaSave } from "react-icons/fa";
import { CrmLostReason, LostReasonCategoryEnum } from "../../../types/crm";
import { getLostReasonCategoryText } from "../../../utils/erp";
import React, { useState, useEffect } from 'react'
import { FaTimes, FaSave } from 'react-icons/fa'
import { CrmLostReason, LostReasonCategoryEnum } from '../../../types/crm'
import { getLostReasonCategoryText } from '../../../utils/erp'
interface LossReasonModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (reason: Partial<CrmLostReason>) => void;
editingReason?: CrmLostReason | null;
mode: "add" | "edit" | "view";
isOpen: boolean
onClose: () => void
onSave: (reason: Partial<CrmLostReason>) => void
editingReason?: CrmLostReason | null
mode: 'add' | 'edit' | 'view'
}
const LossReasonModal: React.FC<LossReasonModalProps> = ({
@ -19,102 +19,97 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
mode,
}) => {
const [formData, setFormData] = useState<Partial<CrmLostReason>>({
code: "",
name: "",
description: "",
code: '',
name: '',
description: '',
category: LostReasonCategoryEnum.Price,
isActive: true,
});
})
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [errors, setErrors] = useState<{ [key: string]: string }>({})
useEffect(() => {
if (editingReason) {
setFormData(editingReason);
setFormData(editingReason)
} else {
setFormData({
code: "",
name: "",
description: "",
code: '',
name: '',
description: '',
category: LostReasonCategoryEnum.Price,
isActive: true,
});
})
}
setErrors({});
}, [editingReason, isOpen]);
setErrors({})
}, [editingReason, isOpen])
const validateForm = () => {
const newErrors: { [key: string]: string } = {};
const newErrors: { [key: string]: string } = {}
if (!formData.code?.trim()) {
newErrors.code = "Kod zorunludur";
newErrors.code = 'Kod zorunludur'
}
if (!formData.name?.trim()) {
newErrors.name = "Kayıp nedeni adı zorunludur";
newErrors.name = 'Kayıp nedeni adı zorunludur'
}
if (!formData.category) {
newErrors.category = "Kategori seçimi zorunludur";
newErrors.category = 'Kategori seçimi zorunludur'
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (mode === "view") {
onClose();
return;
if (mode === 'view') {
onClose()
return
}
if (validateForm()) {
onSave(formData);
onClose();
onSave(formData)
onClose()
}
};
}
const handleInputChange = (
field: keyof CrmLostReason,
value: string | boolean | LostReasonCategoryEnum
value: string | boolean | LostReasonCategoryEnum,
) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setFormData((prev) => ({ ...prev, [field]: value }))
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" }));
setErrors((prev) => ({ ...prev, [field]: '' }))
}
};
}
if (!isOpen) return null;
if (!isOpen) return null
const getModalTitle = () => {
switch (mode) {
case "add":
return "Yeni Kayıp Nedeni";
case "edit":
return "Kayıp Nedeni Düzenle";
case "view":
return "Kayıp Nedeni Detayları";
case 'add':
return 'Yeni Kayıp Nedeni'
case 'edit':
return 'Kayıp Nedeni Düzenle'
case 'view':
return 'Kayıp Nedeni Detayları'
default:
return "";
return ''
}
};
}
const isReadOnly = mode === "view";
const isReadOnly = mode === 'view'
return (
<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">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b">
<h3 className="text-sm font-semibold text-gray-900">
{getModalTitle()}
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<h3 className="text-sm font-semibold text-gray-900">{getModalTitle()}</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<FaTimes className="w-5 h-5" />
</button>
</div>
@ -124,60 +119,47 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
<div className="space-y-2">
{/* Code */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Kod *
</label>
<label className="block text-xs font-medium text-gray-700 mb-1">Kod *</label>
<input
type="text"
value={formData.code || ""}
onChange={(e) => handleInputChange("code", e.target.value)}
value={formData.code || ''}
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 ${
errors.code ? "border-red-500" : "border-gray-300"
} ${isReadOnly ? "bg-gray-100" : ""}`}
errors.code ? 'border-red-500' : 'border-gray-300'
} ${isReadOnly ? 'bg-gray-100' : ''}`}
placeholder="Örn: LR001"
readOnly={isReadOnly}
/>
{errors.code && (
<p className="text-red-500 text-sm mt-1">{errors.code}</p>
)}
{errors.code && <p className="text-red-500 text-sm mt-1">{errors.code}</p>}
</div>
{/* Name */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Kayıp Nedeni *
</label>
<label className="block text-xs font-medium text-gray-700 mb-1">Kayıp Nedeni *</label>
<input
type="text"
value={formData.name || ""}
onChange={(e) => handleInputChange("name", e.target.value)}
value={formData.name || ''}
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 ${
errors.name ? "border-red-500" : "border-gray-300"
} ${isReadOnly ? "bg-gray-100" : ""}`}
errors.name ? 'border-red-500' : 'border-gray-300'
} ${isReadOnly ? 'bg-gray-100' : ''}`}
placeholder="Kayıp nedeni adını girin"
readOnly={isReadOnly}
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
</div>
{/* Category */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Kategori *
</label>
<label className="block text-xs font-medium text-gray-700 mb-1">Kategori *</label>
<select
value={formData.category || ""}
value={formData.category || ''}
onChange={(e) =>
handleInputChange(
"category",
e.target.value as LostReasonCategoryEnum
)
handleInputChange('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 ${
errors.category ? "border-red-500" : "border-gray-300"
} ${isReadOnly ? "bg-gray-100" : ""}`}
errors.category ? 'border-red-500' : 'border-gray-300'
} ${isReadOnly ? 'bg-gray-100' : ''}`}
disabled={isReadOnly}
>
{Object.values(LostReasonCategoryEnum).map((category) => (
@ -186,24 +168,18 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
</option>
))}
</select>
{errors.category && (
<p className="text-red-500 text-sm mt-1">{errors.category}</p>
)}
{errors.category && <p className="text-red-500 text-sm mt-1">{errors.category}</p>}
</div>
{/* Description */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
ıklama
</label>
<label className="block text-xs font-medium text-gray-700 mb-1">ıklama</label>
<textarea
value={formData.description || ""}
onChange={(e) =>
handleInputChange("description", e.target.value)
}
value={formData.description || ''}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={2}
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"
readOnly={isReadOnly}
@ -216,16 +192,11 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
type="checkbox"
id="isActive"
checked={formData.isActive || false}
onChange={(e) =>
handleInputChange("isActive", e.target.checked)
}
onChange={(e) => handleInputChange('isActive', e.target.checked)}
className="mr-2"
disabled={isReadOnly}
/>
<label
htmlFor="isActive"
className="text-sm font-medium text-gray-700"
>
<label htmlFor="isActive" className="text-sm font-medium text-gray-700">
Aktif
</label>
</div>
@ -238,9 +209,9 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
onClick={onClose}
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>
{mode !== "view" && (
{mode !== 'view' && (
<button
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"
@ -253,7 +224,7 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
</form>
</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 {
FaPlus,
FaEdit,
@ -9,104 +9,86 @@ import {
FaTh,
FaList,
FaTimes,
} from "react-icons/fa";
import {
CrmLostReason,
LostReasonCategoryEnum,
CrmOpportunity,
} from "../../../types/crm";
import DataTable, { Column } from "../../../components/common/DataTable";
import LossReasonModal from "./LossReasonModal";
import LossReasonCardView from "./LossReasonCardView";
import { mockLossReasons } from "../../../mocks/mockLossReasons";
import { mockOpportunities } from "../../../mocks/mockOpportunities";
import Widget from "../../../components/common/Widget";
import {
getLostReasonCategoryColor,
getLostReasonCategoryText,
} from "../../../utils/erp";
} from 'react-icons/fa'
import { CrmLostReason, LostReasonCategoryEnum, CrmOpportunity } from '../../../types/crm'
import DataTable, { Column } from '../../../components/common/DataTable'
import LossReasonModal from './LossReasonModal'
import LossReasonCardView from './LossReasonCardView'
import { mockLossReasons } from '../../../mocks/mockLossReasons'
import { mockOpportunities } from '../../../mocks/mockOpportunities'
import Widget from '../../../components/common/Widget'
import { getLostReasonCategoryColor, getLostReasonCategoryText } from '../../../utils/erp'
import { Container } from '@/components/shared'
const LossReasons: React.FC = () => {
const [lossReasons, setLossReasons] =
useState<CrmLostReason[]>(mockLossReasons);
const [opportunities] = useState<CrmOpportunity[]>(mockOpportunities);
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState<
LostReasonCategoryEnum | "all"
>("all");
const [viewMode, setViewMode] = useState<"table" | "card">("table");
const [lossReasons, setLossReasons] = useState<CrmLostReason[]>(mockLossReasons)
const [opportunities] = useState<CrmOpportunity[]>(mockOpportunities)
const [searchTerm, setSearchTerm] = useState('')
const [selectedCategory, setSelectedCategory] = useState<LostReasonCategoryEnum | 'all'>('all')
const [viewMode, setViewMode] = useState<'table' | 'card'>('table')
// Modal states
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalMode, setModalMode] = useState<"add" | "edit" | "view">("add");
const [editingReason, setEditingReason] = useState<CrmLostReason | null>(
null
);
const [isModalOpen, setIsModalOpen] = useState(false)
const [modalMode, setModalMode] = useState<'add' | 'edit' | 'view'>('add')
const [editingReason, setEditingReason] = useState<CrmLostReason | null>(null)
// Delete modal states
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [reasonToDelete, setReasonToDelete] = useState<CrmLostReason | null>(
null
);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [reasonToDelete, setReasonToDelete] = useState<CrmLostReason | null>(null)
const handleAdd = () => {
setModalMode("add");
setEditingReason(null);
setIsModalOpen(true);
};
setModalMode('add')
setEditingReason(null)
setIsModalOpen(true)
}
const handleEdit = (reason: CrmLostReason) => {
setModalMode("edit");
setEditingReason(reason);
setIsModalOpen(true);
};
setModalMode('edit')
setEditingReason(reason)
setIsModalOpen(true)
}
const handleView = (reason: CrmLostReason) => {
setModalMode("view");
setEditingReason(reason);
setIsModalOpen(true);
};
setModalMode('view')
setEditingReason(reason)
setIsModalOpen(true)
}
const handleDelete = (id: string) => {
const reason = lossReasons.find((r) => r.id === id);
const reason = lossReasons.find((r) => r.id === id)
if (reason) {
setReasonToDelete(reason);
setIsDeleteModalOpen(true);
setReasonToDelete(reason)
setIsDeleteModalOpen(true)
}
};
}
const handleSave = (reasonData: Partial<CrmLostReason>) => {
if (modalMode === "add") {
if (modalMode === 'add') {
const newReason: CrmLostReason = {
id: Date.now().toString(),
code: reasonData.code || "",
name: reasonData.name || "",
description: reasonData.description || "",
code: reasonData.code || '',
name: reasonData.name || '',
description: reasonData.description || '',
category: reasonData.category || LostReasonCategoryEnum.Price,
isActive:
reasonData.isActive !== undefined ? reasonData.isActive : true,
};
setLossReasons((prev) => [...prev, newReason]);
} else if (modalMode === "edit" && editingReason) {
isActive: reasonData.isActive !== undefined ? reasonData.isActive : true,
}
setLossReasons((prev) => [...prev, newReason])
} else if (modalMode === 'edit' && editingReason) {
setLossReasons((prev) =>
prev.map((reason) =>
reason.id === editingReason.id
? ({ ...reason, ...reasonData } as CrmLostReason)
: reason
)
);
reason.id === editingReason.id ? ({ ...reason, ...reasonData } as CrmLostReason) : reason,
),
)
}
};
}
const confirmDelete = () => {
if (reasonToDelete) {
setLossReasons((prev) =>
prev.filter((reason) => reason.id !== reasonToDelete.id)
);
setIsDeleteModalOpen(false);
setReasonToDelete(null);
setLossReasons((prev) => prev.filter((reason) => reason.id !== reasonToDelete.id))
setIsDeleteModalOpen(false)
setReasonToDelete(null)
}
};
}
const filteredReasons = lossReasons.filter((reason) => {
if (
@ -114,26 +96,26 @@ const LossReasons: React.FC = () => {
!reason.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
!reason.code.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false;
return false
}
if (selectedCategory !== "all" && reason.category !== selectedCategory) {
return false;
if (selectedCategory !== 'all' && reason.category !== selectedCategory) {
return false
}
return true;
});
return true
})
const columns: Column<CrmLostReason>[] = [
{
key: "code",
header: "Kod",
key: 'code',
header: 'Kod',
sortable: true,
render: (reason: CrmLostReason) => (
<div className="font-medium text-gray-900">{reason.code}</div>
),
},
{
key: "name",
header: "Kayıp Nedeni",
key: 'name',
header: 'Kayıp Nedeni',
sortable: true,
render: (reason: CrmLostReason) => (
<div>
@ -143,12 +125,12 @@ const LossReasons: React.FC = () => {
),
},
{
key: "category",
header: "Kategori",
key: 'category',
header: 'Kategori',
render: (reason: CrmLostReason) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getLostReasonCategoryColor(
reason.category
reason.category,
)}`}
>
{getLostReasonCategoryText(reason.category)}
@ -156,38 +138,34 @@ const LossReasons: React.FC = () => {
),
},
{
key: "usage",
header: "Kullanım",
key: 'usage',
header: 'Kullanım',
render: (reason: CrmLostReason) => {
const usageCount = opportunities.filter(
(opp) => opp.lostReason?.id === reason.id
).length;
const usageCount = opportunities.filter((opp) => opp.lostReason?.id === reason.id).length
return (
<div className="flex items-center gap-1">
<FaBullseye className="w-4 h-4 text-gray-400" />
<span>{usageCount} Fırsat</span>
</div>
);
)
},
},
{
key: "status",
header: "Durum",
key: 'status',
header: 'Durum',
render: (reason: CrmLostReason) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
reason.isActive
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
reason.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}
>
{reason.isActive ? "Aktif" : "Pasif"}
{reason.isActive ? 'Aktif' : 'Pasif'}
</span>
),
},
{
key: "actions",
header: "İşlemler",
key: 'actions',
header: 'İşlemler',
render: (reason: CrmLostReason) => (
<div className="flex gap-2">
<button
@ -214,306 +192,280 @@ const LossReasons: React.FC = () => {
</div>
),
},
];
]
// Calculate statistics
const totalReasons = lossReasons.length;
const activeReasons = lossReasons.filter((r) => r.isActive).length;
const categoryDistribution = Object.values(LostReasonCategoryEnum).map(
(category) => ({
category,
count: lossReasons.filter((r) => r.category === category).length,
usage: opportunities.filter(
(opp) =>
opp.lostReason &&
lossReasons.find(
(r) => r.id === opp.lostReason?.id && r.category === category
)
).length,
})
);
const totalReasons = lossReasons.length
const activeReasons = lossReasons.filter((r) => r.isActive).length
const categoryDistribution = Object.values(LostReasonCategoryEnum).map((category) => ({
category,
count: lossReasons.filter((r) => r.category === category).length,
usage: opportunities.filter(
(opp) =>
opp.lostReason &&
lossReasons.find((r) => r.id === opp.lostReason?.id && r.category === category),
).length,
}))
// Top loss reasons by usage
const topLossReasons = lossReasons
.map((reason) => ({
...reason,
usageCount: opportunities.filter(
(opp) => opp.lostReason?.id === reason.id
).length,
usageCount: opportunities.filter((opp) => opp.lostReason?.id === reason.id).length,
lostValue: opportunities
.filter((opp) => opp.lostReason?.id === reason.id)
.reduce((sum, opp) => sum + opp.estimatedValue, 0),
}))
.sort((a, b) => b.usageCount - a.usageCount)
.slice(0, 5);
.slice(0, 5)
return (
<div className="space-y-3 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-bold text-gray-900">Kayıp Nedenleri</h2>
<p className="text-sm text-gray-600">
Fırsat kaybı nedenlerini analiz edin
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleAdd}
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"
>
<FaPlus className="w-4 h-4" />
Yeni Kayıp Nedeni
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget
title="Toplam Neden"
value={totalReasons}
color="red"
icon="FaExclamationTriangle"
/>
<Widget
title="Aktif Neden"
value={activeReasons}
color="green"
icon="FaArrowDown"
/>
<Widget
title="Kayıp Fırsat"
value={opportunities.filter((opp) => opp.lostReason).length}
color="orange"
icon="FaBullseye"
/>
<Widget
title="En Çok Kullanılan"
value={topLossReasons[0]?.name || "-"}
color="purple"
icon="FaChartBar"
valueClassName="text-lg"
/>
</div>
{/* Category Distribution */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">
Kategori Dağılımı
</h3>
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
{categoryDistribution.map(({ category, count, usage }) => (
<div key={category} className="text-center p-4 border rounded-lg">
<div
className={`inline-block px-2 py-1 text-xs font-medium rounded-full mb-2 ${getLostReasonCategoryColor(
category
)}`}
>
{getLostReasonCategoryText(category)}
</div>
<div className="text-lg font-bold text-gray-900 mb-1">
{count}
</div>
<div className="text-xs text-gray-500">{usage} Kullanım</div>
</div>
))}
</div>
</div>
{/* Top Loss Reasons */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-2">
En Çok Kayıp Yaşanan Nedenler
</h3>
<div className="space-y-3 pt-2">
{topLossReasons.map((reason, index) => (
<div
key={reason.id}
className="flex items-center justify-between p-2 bg-gray-50 rounded-lg"
<Container>
<div className="space-y-3 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-bold text-gray-900">Kayıp Nedenleri</h2>
<p className="text-sm text-gray-600">Fırsat kaybı nedenlerini analiz edin</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleAdd}
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"
>
<div className="flex items-center gap-4">
<div className="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
<span className="text-red-600 font-bold">{index + 1}</span>
</div>
<div>
<h4 className="font-medium text-gray-900">{reason.name}</h4>
<p className="text-sm text-gray-500">
{getLostReasonCategoryText(reason.category)} {" "}
{reason.usageCount} fırsat
</p>
</div>
</div>
<div className="text-right">
<div className="text-sm font-bold text-red-600">
{reason.lostValue.toLocaleString()}
</div>
<div className="text-sm text-gray-500">Kayıp Değer</div>
</div>
</div>
))}
<FaPlus className="w-4 h-4" />
Yeni Kayıp Nedeni
</button>
</div>
</div>
</div>
{/* Filters */}
<div className="flex gap-3 items-center">
<div className="flex-1">
<input
type="text"
placeholder="Neden adı veya kodu ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget
title="Toplam Neden"
value={totalReasons}
color="red"
icon="FaExclamationTriangle"
/>
<Widget title="Aktif Neden" value={activeReasons} color="green" icon="FaArrowDown" />
<Widget
title="Kayıp Fırsat"
value={opportunities.filter((opp) => opp.lostReason).length}
color="orange"
icon="FaBullseye"
/>
<Widget
title="En Çok Kullanılan"
value={topLossReasons[0]?.name || '-'}
color="purple"
icon="FaChartBar"
valueClassName="text-lg"
/>
</div>
<select
value={selectedCategory}
onChange={(e) =>
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"
>
<option value="all">Tüm Kategoriler</option>
{Object.values(LostReasonCategoryEnum).map((category) => (
<option key={category} value={category}>
{getLostReasonCategoryText(category)}
</option>
))}
</select>
{/* View Mode Toggle */}
<div className="flex bg-gray-100 rounded-lg p-1">
<button
onClick={() => setViewMode("table")}
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
viewMode === "table"
? "bg-white text-gray-900 shadow-sm"
: "text-gray-600 hover:text-gray-900"
}`}
>
<FaList className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode("card")}
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
viewMode === "card"
? "bg-white text-gray-900 shadow-sm"
: "text-gray-600 hover:text-gray-900"
}`}
>
<FaTh className="w-4 h-4" />
</button>
</div>
</div>
{/* Data Display */}
{viewMode === "table" ? (
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredReasons} columns={columns} />
</div>
) : (
<LossReasonCardView
lossReasons={filteredReasons}
opportunities={opportunities}
onEdit={handleEdit}
onDelete={handleDelete}
onView={handleView}
/>
)}
{filteredReasons.length === 0 && viewMode === "table" && (
<div className="text-center py-12">
<FaExclamationTriangle className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<h3 className="text-sm font-medium text-gray-900 mb-2">
Kayıp nedeni bulunamadı
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
</div>
)}
{/* Modals */}
<LossReasonModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setEditingReason(null);
}}
onSave={handleSave}
editingReason={editingReason}
mode={modalMode}
/>
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<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">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b">
<div className="flex items-center gap-3">
<FaExclamationTriangle className="w-6 h-6 text-red-600" />
<h3 className="text-sm font-semibold text-gray-900">
Silme Onayı
</h3>
</div>
<button
onClick={() => {
setIsDeleteModalOpen(false);
setReasonToDelete(null);
}}
className="text-gray-400 hover:text-gray-600"
>
<FaTimes className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-3">
<div className="text-center">
<div className="mx-auto flex items-center justify-center h-10 w-10 rounded-full bg-red-100 mb-3">
<FaTrash className="h-6 w-6 text-red-600" />
{/* Category Distribution */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">Kategori Dağılımı</h3>
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
{categoryDistribution.map(({ category, count, usage }) => (
<div key={category} className="text-center p-4 border rounded-lg">
<div
className={`inline-block px-2 py-1 text-xs font-medium rounded-full mb-2 ${getLostReasonCategoryColor(
category,
)}`}
>
{getLostReasonCategoryText(category)}
</div>
<h3 className="text-sm font-medium text-gray-900 mb-2">
Kayıp nedeni silinsin mi?
</h3>
<p className="text-sm text-gray-500 mb-6">
<span className="font-medium">"{reasonToDelete?.name}"</span>{" "}
kayıp nedenini silmek üzeresiniz. Bu işlem geri alınamaz.
</p>
<div className="text-lg font-bold text-gray-900 mb-1">{count}</div>
<div className="text-xs text-gray-500">{usage} Kullanım</div>
</div>
))}
</div>
</div>
{/* Buttons */}
<div className="flex justify-end gap-1.5">
{/* Top Loss Reasons */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-2">
En Çok Kayıp Yaşanan Nedenler
</h3>
<div className="space-y-3 pt-2">
{topLossReasons.map((reason, index) => (
<div
key={reason.id}
className="flex items-center justify-between p-2 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-4">
<div className="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
<span className="text-red-600 font-bold">{index + 1}</span>
</div>
<div>
<h4 className="font-medium text-gray-900">{reason.name}</h4>
<p className="text-sm text-gray-500">
{getLostReasonCategoryText(reason.category)} {reason.usageCount} fırsat
</p>
</div>
</div>
<div className="text-right">
<div className="text-sm font-bold text-red-600">
{reason.lostValue.toLocaleString()}
</div>
<div className="text-sm text-gray-500">Kayıp Değer</div>
</div>
</div>
))}
</div>
</div>
{/* Filters */}
<div className="flex gap-3 items-center">
<div className="flex-1">
<input
type="text"
placeholder="Neden adı veya kodu ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<select
value={selectedCategory}
onChange={(e) => 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"
>
<option value="all">Tüm Kategoriler</option>
{Object.values(LostReasonCategoryEnum).map((category) => (
<option key={category} value={category}>
{getLostReasonCategoryText(category)}
</option>
))}
</select>
{/* View Mode Toggle */}
<div className="flex bg-gray-100 rounded-lg p-1">
<button
onClick={() => setViewMode('table')}
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
viewMode === 'table'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<FaList className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('card')}
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
viewMode === 'card'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<FaTh className="w-4 h-4" />
</button>
</div>
</div>
{/* Data Display */}
{viewMode === 'table' ? (
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredReasons} columns={columns} />
</div>
) : (
<LossReasonCardView
lossReasons={filteredReasons}
opportunities={opportunities}
onEdit={handleEdit}
onDelete={handleDelete}
onView={handleView}
/>
)}
{filteredReasons.length === 0 && viewMode === 'table' && (
<div className="text-center py-12">
<FaExclamationTriangle className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<h3 className="text-sm font-medium text-gray-900 mb-2">Kayıp nedeni bulunamadı</h3>
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</div>
)}
{/* Modals */}
<LossReasonModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false)
setEditingReason(null)
}}
onSave={handleSave}
editingReason={editingReason}
mode={modalMode}
/>
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<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">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b">
<div className="flex items-center gap-3">
<FaExclamationTriangle className="w-6 h-6 text-red-600" />
<h3 className="text-sm font-semibold text-gray-900">Silme Onayı</h3>
</div>
<button
onClick={() => {
setIsDeleteModalOpen(false);
setReasonToDelete(null);
setIsDeleteModalOpen(false)
setReasonToDelete(null)
}}
className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
className="text-gray-400 hover:text-gray-600"
>
İptal
</button>
<button
onClick={confirmDelete}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
>
<FaTrash className="w-4 h-4" />
Sil
<FaTimes className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-3">
<div className="text-center">
<div className="mx-auto flex items-center justify-center h-10 w-10 rounded-full bg-red-100 mb-3">
<FaTrash className="h-6 w-6 text-red-600" />
</div>
<h3 className="text-sm font-medium text-gray-900 mb-2">
Kayıp nedeni silinsin mi?
</h3>
<p className="text-sm text-gray-500 mb-6">
<span className="font-medium">"{reasonToDelete?.name}"</span> kayıp nedenini
silmek üzeresiniz. Bu işlem geri alınamaz.
</p>
</div>
{/* Buttons */}
<div className="flex justify-end gap-1.5">
<button
onClick={() => {
setIsDeleteModalOpen(false)
setReasonToDelete(null)
}}
className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
>
İptal
</button>
<button
onClick={confirmDelete}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
>
<FaTrash className="w-4 h-4" />
Sil
</button>
</div>
</div>
</div>
</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 {
FaTimes,
FaBullseye,
@ -13,21 +13,21 @@ import {
FaClock,
FaPhone,
FaEnvelope,
} from "react-icons/fa";
import { CrmOpportunity, OpportunityStageEnum } from "../../../types/crm";
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
} from 'react-icons/fa'
import { CrmOpportunity, OpportunityStageEnum } from '../../../types/crm'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import {
getOpportunityLeadSourceText,
getOpportunityProbabilityColor,
getOpportunityStageColor,
getOpportunityStageText,
} from "../../../utils/erp";
} from '../../../utils/erp'
interface OpportunityDetailsProps {
isOpen: boolean;
onClose: () => void;
onEdit: (opportunity: CrmOpportunity) => void;
opportunity: CrmOpportunity | null;
isOpen: boolean
onClose: () => void
onEdit: (opportunity: CrmOpportunity) => void
opportunity: CrmOpportunity | null
}
const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
@ -36,11 +36,9 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
onEdit,
opportunity,
}) => {
if (!isOpen || !opportunity) return null;
if (!isOpen || !opportunity) return null
const customer = mockBusinessParties.find(
(c) => c.id === opportunity.customerId
);
const customer = mockBusinessParties.find((c) => c.id === opportunity.customerId)
return (
<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" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900">
{opportunity.title}
</h2>
<p className="text-sm text-gray-600">
{opportunity.opportunityNumber}
</p>
<h2 className="text-lg font-semibold text-gray-900">{opportunity.title}</h2>
<p className="text-sm text-gray-600">{opportunity.opportunityNumber}</p>
</div>
</div>
<div className="flex items-center gap-3">
@ -68,10 +62,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<FaEdit className="w-4 h-4" />
Düzenle
</button>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 p-2"
>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-2">
<FaTimes className="w-5 h-5" />
</button>
</div>
@ -83,7 +74,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="text-center">
<div
className={`inline-flex px-3 py-1.5 rounded-full text-xs font-medium border ${getOpportunityStageColor(
opportunity.stage
opportunity.stage,
)}`}
>
{getOpportunityStageText(opportunity.stage)}
@ -102,7 +93,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="text-center">
<div
className={`flex items-center justify-center gap-2 text-xl font-bold ${getOpportunityProbabilityColor(
opportunity.probability
opportunity.probability,
)}`}
>
<FaPercentage className="w-5 h-5" />
@ -114,9 +105,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="text-center">
<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" />
{new Date(opportunity.expectedCloseDate).toLocaleDateString(
"tr-TR"
)}
{new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}
</div>
<p className="text-xs text-gray-500 mt-1">Beklenen Kapanış</p>
</div>
@ -133,9 +122,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Başlık
</label>
<label className="block text-sm font-medium text-gray-600 mb-1">Başlık</label>
<p className="text-gray-900">{opportunity.title}</p>
</div>
@ -144,16 +131,12 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<label className="block text-sm font-medium text-gray-600 mb-1">
ıklama
</label>
<p className="text-gray-900 whitespace-pre-wrap">
{opportunity.description}
</p>
<p className="text-gray-900 whitespace-pre-wrap">{opportunity.description}</p>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Kaynak
</label>
<label className="block text-sm font-medium text-gray-600 mb-1">Kaynak</label>
<div className="flex items-center gap-2">
<FaTags className="w-4 h-4 text-gray-400" />
<span className="text-gray-900">
@ -209,16 +192,12 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<FaEnvelope className="w-4 h-4 text-gray-400" />
<span className="text-gray-900">
{customer.primaryContact?.email}
</span>
<span className="text-gray-900">{customer.primaryContact?.email}</span>
</div>
{customer.primaryContact?.phone && (
<div className="flex items-center gap-2">
<FaPhone className="w-4 h-4 text-gray-400" />
<span className="text-gray-900">
{customer.primaryContact?.phone}
</span>
<span className="text-gray-900">{customer.primaryContact?.phone}</span>
</div>
)}
</div>
@ -238,14 +217,10 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Sorumlu
</label>
<label className="block text-sm font-medium text-gray-600 mb-1">Sorumlu</label>
<div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-400" />
<span className="text-gray-900">
{opportunity.assigned?.fullName}
</span>
<span className="text-gray-900">{opportunity.assigned?.fullName}</span>
</div>
</div>
@ -256,9 +231,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="flex items-center gap-2">
<FaCalendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-900">
{new Date(
opportunity.expectedCloseDate
).toLocaleDateString("tr-TR")}
{new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}
</span>
</div>
</div>
@ -271,9 +244,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
<div className="flex items-center gap-2">
<FaCalendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-900">
{new Date(
opportunity.actualCloseDate
).toLocaleDateString("tr-TR")}
{new Date(opportunity.actualCloseDate).toLocaleDateString('tr-TR')}
</span>
</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="w-3 h-3 bg-blue-600 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
<div>
<p className="text-sm font-medium text-gray-900">
Fırsat Oluşturuldu
</p>
<p className="text-sm font-medium text-gray-900">Fırsat Oluşturuldu</p>
<p className="text-xs text-gray-500 flex items-center gap-1">
<FaClock className="w-3 h-3" />
{new Date(opportunity.creationTime).toLocaleDateString(
"tr-TR"
)}
{new Date(opportunity.creationTime).toLocaleDateString('tr-TR')}
</p>
</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="w-3 h-3 bg-gray-400 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
<div>
<p className="text-sm font-medium text-gray-900">
Son Güncelleme
</p>
<p className="text-sm font-medium text-gray-900">Son Güncelleme</p>
<p className="text-xs text-gray-500 flex items-center gap-1">
<FaClock className="w-3 h-3" />
{new Date(
opportunity.lastModificationTime
).toLocaleDateString("tr-TR")}
{new Date(opportunity.lastModificationTime).toLocaleDateString('tr-TR')}
</p>
</div>
</div>
@ -322,41 +285,37 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
{/* Progress Indicator */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">
Aşama İlerlemesi
</h3>
<h3 className="text-base font-semibold text-gray-900 mb-3">Aşama İlerlemesi</h3>
<div className="space-y-2">
{Object.values(OpportunityStageEnum).map((stage, index) => {
const isCurrentStage = stage === opportunity.stage;
const isCurrentStage = stage === opportunity.stage
const isPassedStage =
Object.values(OpportunityStageEnum).indexOf(
opportunity.stage
) > index;
Object.values(OpportunityStageEnum).indexOf(opportunity.stage) > index
return (
<div key={stage} className="flex items-center gap-3">
<div
className={`w-4 h-4 rounded-full border-2 ${
isCurrentStage
? "bg-blue-600 border-blue-600"
? 'bg-blue-600 border-blue-600'
: isPassedStage
? "bg-green-600 border-green-600"
: "bg-white border-gray-300"
? 'bg-green-600 border-green-600'
: 'bg-white border-gray-300'
}`}
></div>
<span
className={`text-sm ${
isCurrentStage
? "font-semibold text-blue-600"
? 'font-semibold text-blue-600'
: isPassedStage
? "text-green-600"
: "text-gray-500"
? 'text-green-600'
: 'text-gray-500'
}`}
>
{getOpportunityStageText(stage)}
</span>
</div>
);
)
})}
</div>
</div>
@ -365,7 +324,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
</div>
</div>
</div>
);
};
)
}
export default OpportunityDetails;
export default OpportunityDetails

View file

@ -1,21 +1,22 @@
import React, { useState, useEffect } from "react";
import { FaTimes, FaDollarSign, FaFileAlt } from "react-icons/fa";
import React, { useState, useEffect } from 'react'
import { FaTimes, FaDollarSign, FaFileAlt } from 'react-icons/fa'
import {
CrmOpportunity,
OpportunityStageEnum,
LeadSourceEnum,
OpportunityStatusEnum,
} from "../../../types/crm";
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
import { mockEmployees } from "../../../mocks/mockEmployees";
import { BusinessParty } from "../../../types/common";
} from '../../../types/crm'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { mockEmployees } from '../../../mocks/mockEmployees'
import { BusinessParty } from '../../../types/common'
import { getOpportunityLeadSourceText, getOpportunityStageText } from '@/utils/erp'
interface OpportunityFormProps {
isOpen: boolean;
onClose: () => void;
onSave: (opportunity: CrmOpportunity) => void;
opportunity?: CrmOpportunity | null;
mode: "create" | "edit";
isOpen: boolean
onClose: () => void
onSave: (opportunity: CrmOpportunity) => void
opportunity?: CrmOpportunity | null
mode: 'create' | 'edit'
}
const OpportunityForm: React.FC<OpportunityFormProps> = ({
@ -26,122 +27,118 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
mode,
}) => {
const [formData, setFormData] = useState({
id: "",
opportunityNumber: "",
title: "",
description: "",
customerId: "",
contactId: "",
id: '',
opportunityNumber: '',
title: '',
description: '',
customerId: '',
contactId: '',
stage: OpportunityStageEnum.Qualification,
probability: 10,
estimatedValue: 0,
currency: "TRY",
expectedCloseDate: "",
assignedTo: "",
teamId: "",
currency: 'TRY',
expectedCloseDate: '',
assignedTo: '',
teamId: '',
leadSource: LeadSourceEnum.Website,
campaignId: "",
});
campaignId: '',
})
const [customers] = useState<BusinessParty[]>(mockBusinessParties);
const [errors, setErrors] = useState<Record<string, string>>({});
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [errors, setErrors] = useState<Record<string, string>>({})
useEffect(() => {
if (opportunity && mode === "edit") {
if (opportunity && mode === 'edit') {
setFormData({
id: opportunity.id,
opportunityNumber: opportunity.opportunityNumber,
title: opportunity.title,
description: opportunity.description || "",
description: opportunity.description || '',
customerId: opportunity.customerId,
contactId: opportunity.contactId || "",
contactId: opportunity.contactId || '',
stage: opportunity.stage,
probability: opportunity.probability,
estimatedValue: opportunity.estimatedValue,
currency: opportunity.currency,
expectedCloseDate: opportunity.expectedCloseDate
.toISOString()
.split("T")[0],
expectedCloseDate: opportunity.expectedCloseDate.toISOString().split('T')[0],
assignedTo: opportunity.assignedTo,
teamId: opportunity.teamId || "",
teamId: opportunity.teamId || '',
leadSource: opportunity.leadSource,
campaignId: opportunity.campaignId || "",
});
} else if (mode === "create") {
campaignId: opportunity.campaignId || '',
})
} else if (mode === 'create') {
// Generate new opportunity number
const newNumber = `OPP-${Date.now().toString().slice(-6)}`;
const newNumber = `OPP-${Date.now().toString().slice(-6)}`
setFormData({
id: `opp_${Date.now()}`,
opportunityNumber: newNumber,
title: "",
description: "",
customerId: "",
contactId: "",
title: '',
description: '',
customerId: '',
contactId: '',
stage: OpportunityStageEnum.Qualification,
probability: 10,
estimatedValue: 0,
currency: "TRY",
expectedCloseDate: "",
assignedTo: "Mevcut Kullanıcı",
teamId: "",
currency: 'TRY',
expectedCloseDate: '',
assignedTo: 'Mevcut Kullanıcı',
teamId: '',
leadSource: LeadSourceEnum.Website,
campaignId: "",
});
campaignId: '',
})
}
}, [opportunity, mode, isOpen]);
}, [opportunity, mode, isOpen])
const handleInputChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
const { name, value } = e.target
setFormData((prev) => ({
...prev,
[name]: value,
}));
}))
// Clear error when user starts typing
if (errors[name]) {
setErrors((prev) => ({
...prev,
[name]: "",
}));
[name]: '',
}))
}
};
}
const validateForm = () => {
const newErrors: Record<string, string> = {};
const newErrors: Record<string, string> = {}
if (!formData.title.trim()) {
newErrors.title = "Fırsat başlığı zorunludur";
newErrors.title = 'Fırsat başlığı zorunludur'
}
if (!formData.customerId) {
newErrors.customerId = "Müşteri seçimi zorunludur";
newErrors.customerId = 'Müşteri seçimi zorunludur'
}
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) {
newErrors.expectedCloseDate = "Beklenen kapanış tarihi zorunludur";
newErrors.expectedCloseDate = 'Beklenen kapanış tarihi zorunludur'
}
if (formData.probability < 0 || formData.probability > 100) {
newErrors.probability = "Olasılık 0-100 arasında olmalıdır";
newErrors.probability = 'Olasılık 0-100 arasında olmalıdır'
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!validateForm()) {
return;
return
}
const opportunityData: CrmOpportunity = {
@ -164,45 +161,20 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
formData.stage === OpportunityStageEnum.ClosedWon
? OpportunityStatusEnum.Won
: formData.stage === OpportunityStageEnum.ClosedLost
? OpportunityStatusEnum.Lost
: OpportunityStatusEnum.Open,
? OpportunityStatusEnum.Lost
: OpportunityStatusEnum.Open,
activities: [],
competitors: [],
creationTime: opportunity?.creationTime || new Date(),
lastModificationTime: new Date(),
};
}
onSave(opportunityData);
onClose();
};
onSave(opportunityData)
onClose()
}
const getStageLabel = (stage: OpportunityStageEnum) => {
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;
if (!isOpen) return null
return (
<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 */}
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold text-gray-900">
{mode === "create" ? "Yeni Fırsat Oluştur" : "Fırsat Düzenle"}
{mode === 'create' ? 'Yeni Fırsat Oluştur' : 'Fırsat Düzenle'}
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<FaTimes className="w-5 h-5" />
</button>
</div>
@ -254,19 +223,15 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
value={formData.title}
onChange={handleInputChange}
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.title ? "border-red-500" : "border-gray-300"
errors.title ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Fırsat başlığını girin"
/>
{errors.title && (
<p className="text-red-500 text-sm mt-1">{errors.title}</p>
)}
{errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
ıklama
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">ıklama</label>
<textarea
name="description"
value={formData.description}
@ -278,15 +243,13 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Müşteri *
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Müşteri *</label>
<select
name="customerId"
value={formData.customerId}
onChange={handleInputChange}
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.customerId ? "border-red-500" : "border-gray-300"
errors.customerId ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">Müşteri seçin</option>
@ -297,18 +260,14 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
))}
</select>
{errors.customerId && (
<p className="text-red-500 text-sm mt-1">
{errors.customerId}
</p>
<p className="text-red-500 text-sm mt-1">{errors.customerId}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Sorumlu
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Sorumlu</label>
<select
value={formData.assignedTo || ""}
value={formData.assignedTo || ''}
onChange={handleInputChange}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
@ -330,9 +289,7 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Aşama
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Aşama</label>
<select
name="stage"
value={formData.stage}
@ -341,7 +298,7 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
>
{Object.values(OpportunityStageEnum).map((stage) => (
<option key={stage} value={stage}>
{getStageLabel(stage)}
{getOpportunityStageText(stage)}
</option>
))}
</select>
@ -359,13 +316,11 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
min="0"
max="100"
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.probability ? "border-red-500" : "border-gray-300"
errors.probability ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.probability && (
<p className="text-red-500 text-sm mt-1">
{errors.probability}
</p>
<p className="text-red-500 text-sm mt-1">{errors.probability}</p>
)}
</div>
@ -381,21 +336,17 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
min="0"
step="0.01"
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.estimatedValue ? "border-red-500" : "border-gray-300"
errors.estimatedValue ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="0.00"
/>
{errors.estimatedValue && (
<p className="text-red-500 text-sm mt-1">
{errors.estimatedValue}
</p>
<p className="text-red-500 text-sm mt-1">{errors.estimatedValue}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Para Birimi
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Para Birimi</label>
<select
name="currency"
value={formData.currency}
@ -418,22 +369,16 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
value={formData.expectedCloseDate}
onChange={handleInputChange}
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.expectedCloseDate
? "border-red-500"
: "border-gray-300"
errors.expectedCloseDate ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.expectedCloseDate && (
<p className="text-red-500 text-sm mt-1">
{errors.expectedCloseDate}
</p>
<p className="text-red-500 text-sm mt-1">{errors.expectedCloseDate}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kaynak
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Kaynak</label>
<select
name="leadSource"
value={formData.leadSource}
@ -442,7 +387,7 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
>
{Object.values(LeadSourceEnum).map((source) => (
<option key={source} value={source}>
{getLeadSourceLabel(source)}
{getOpportunityLeadSourceText(source)}
</option>
))}
</select>
@ -463,13 +408,13 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
type="submit"
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
{mode === "create" ? "Oluştur" : "Güncelle"}
{mode === 'create' ? 'Oluştur' : 'Güncelle'}
</button>
</div>
</form>
</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 {
FaBullseye,
FaPlus,
@ -9,86 +9,80 @@ import {
FaArrowUp,
FaUser,
FaBuilding,
} from "react-icons/fa";
import { CrmOpportunity, OpportunityStageEnum } from "../../../types/crm";
import DataTable, { Column } from "../../../components/common/DataTable";
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
import { mockOpportunities } from "../../../mocks/mockOpportunities";
import OpportunityForm from "./OpportunityForm";
import OpportunityDetails from "./OpportunityDetails";
import { BusinessParty } from "../../../types/common";
import Widget from "../../../components/common/Widget";
} from 'react-icons/fa'
import { CrmOpportunity, OpportunityStageEnum } from '../../../types/crm'
import DataTable, { Column } from '../../../components/common/DataTable'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { mockOpportunities } from '../../../mocks/mockOpportunities'
import OpportunityForm from './OpportunityForm'
import OpportunityDetails from './OpportunityDetails'
import { BusinessParty } from '../../../types/common'
import Widget from '../../../components/common/Widget'
import {
getOpportunityProbabilityColor,
getOpportunityStageColor,
getOpportunityStageText,
} from "../../../utils/erp";
} from '../../../utils/erp'
import { Container } from '@/components/shared'
// Mock data - replace with actual data fetching
const OpportunityManagement: React.FC = () => {
const [opportunities, setOpportunities] =
useState<CrmOpportunity[]>(mockOpportunities);
const [customers] = useState<BusinessParty[]>(mockBusinessParties);
const [searchTerm, setSearchTerm] = useState("");
const [selectedStage, setSelectedStage] = useState<
OpportunityStageEnum | "all"
>("all");
const [selectedCustomer, setSelectedCustomer] = useState<string>("all");
const [opportunities, setOpportunities] = useState<CrmOpportunity[]>(mockOpportunities)
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [searchTerm, setSearchTerm] = useState('')
const [selectedStage, setSelectedStage] = useState<OpportunityStageEnum | 'all'>('all')
const [selectedCustomer, setSelectedCustomer] = useState<string>('all')
// Modal states
const [isFormOpen, setIsFormOpen] = useState(false);
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
const [selectedOpportunity, setSelectedOpportunity] =
useState<CrmOpportunity | null>(null);
const [formMode, setFormMode] = useState<"create" | "edit">("create");
const [isFormOpen, setIsFormOpen] = useState(false)
const [isDetailsOpen, setIsDetailsOpen] = useState(false)
const [selectedOpportunity, setSelectedOpportunity] = useState<CrmOpportunity | null>(null)
const [formMode, setFormMode] = useState<'create' | 'edit'>('create')
const handleAdd = () => {
setSelectedOpportunity(null);
setFormMode("create");
setIsFormOpen(true);
};
setSelectedOpportunity(null)
setFormMode('create')
setIsFormOpen(true)
}
const handleEdit = (opportunity: CrmOpportunity) => {
setSelectedOpportunity(opportunity);
setFormMode("edit");
setIsFormOpen(true);
};
setSelectedOpportunity(opportunity)
setFormMode('edit')
setIsFormOpen(true)
}
const handleDelete = (id: string) => {
if (confirm("Bu fırsatı silmek istediğinizden emin misiniz?")) {
setOpportunities((prev) => prev.filter((opp) => opp.id !== id));
if (confirm('Bu fırsatı silmek istediğinizden emin misiniz?')) {
setOpportunities((prev) => prev.filter((opp) => opp.id !== id))
}
};
}
const handleViewDetails = (opportunity: CrmOpportunity) => {
setSelectedOpportunity(opportunity);
setIsDetailsOpen(true);
};
setSelectedOpportunity(opportunity)
setIsDetailsOpen(true)
}
const handleSaveOpportunity = (opportunity: CrmOpportunity) => {
if (formMode === "create") {
setOpportunities((prev) => [...prev, opportunity]);
if (formMode === 'create') {
setOpportunities((prev) => [...prev, opportunity])
} else {
setOpportunities((prev) =>
prev.map((opp) => (opp.id === opportunity.id ? opportunity : opp))
);
setOpportunities((prev) => prev.map((opp) => (opp.id === opportunity.id ? opportunity : opp)))
}
};
}
const handleCloseForm = () => {
setIsFormOpen(false);
setSelectedOpportunity(null);
};
setIsFormOpen(false)
setSelectedOpportunity(null)
}
const handleCloseDetails = () => {
setIsDetailsOpen(false);
setSelectedOpportunity(null);
};
setIsDetailsOpen(false)
setSelectedOpportunity(null)
}
const handleEditFromDetails = (opportunity: CrmOpportunity) => {
setIsDetailsOpen(false);
handleEdit(opportunity);
};
setIsDetailsOpen(false)
handleEdit(opportunity)
}
const filteredOpportunities = opportunities.filter((opportunity) => {
if (
@ -96,56 +90,51 @@ const OpportunityManagement: React.FC = () => {
!opportunity.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
!opportunity.description?.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false;
return false
}
if (selectedStage !== "all" && opportunity.stage !== selectedStage) {
return false;
if (selectedStage !== 'all' && opportunity.stage !== selectedStage) {
return false
}
if (
selectedCustomer !== "all" &&
opportunity.customerId !== selectedCustomer
) {
return false;
if (selectedCustomer !== 'all' && opportunity.customerId !== selectedCustomer) {
return false
}
return true;
});
return true
})
const columns: Column<CrmOpportunity>[] = [
{
key: "title",
header: "Fırsat Başlığı",
key: 'title',
header: 'Fırsat Başlığı',
sortable: true,
render: (opportunity: CrmOpportunity) => (
<div>
<div className="font-medium text-gray-900">{opportunity.title}</div>
<div className="text-sm text-gray-500">
{opportunity.opportunityNumber}
</div>
<div className="text-sm text-gray-500">{opportunity.opportunityNumber}</div>
</div>
),
},
{
key: "customer",
header: "Müşteri",
key: 'customer',
header: 'Müşteri',
render: (opportunity: CrmOpportunity) => {
const customer = customers.find((c) => c.id === opportunity.customerId);
const customer = customers.find((c) => c.id === opportunity.customerId)
return customer ? (
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-400" />
<span>{customer.name}</span>
</div>
) : (
"-"
);
'-'
)
},
},
{
key: "stage",
header: "Aşama",
key: 'stage',
header: 'Aşama',
render: (opportunity: CrmOpportunity) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getOpportunityStageColor(
opportunity.stage
opportunity.stage,
)}`}
>
{getOpportunityStageText(opportunity.stage)}
@ -153,27 +142,23 @@ const OpportunityManagement: React.FC = () => {
),
},
{
key: "estimatedValue",
header: "Tahmini Değer",
key: 'estimatedValue',
header: 'Tahmini Değer',
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-400" />
<span className="font-medium">
{opportunity.estimatedValue.toLocaleString()}
</span>
<span className="font-medium">{opportunity.estimatedValue.toLocaleString()}</span>
</div>
),
},
{
key: "probability",
header: "Olasılık",
key: 'probability',
header: 'Olasılık',
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1">
<FaArrowUp className="w-4 h-4 text-gray-400" />
<span
className={`font-medium ${getOpportunityProbabilityColor(
opportunity.probability
)}`}
className={`font-medium ${getOpportunityProbabilityColor(opportunity.probability)}`}
>
%{opportunity.probability}
</span>
@ -181,22 +166,18 @@ const OpportunityManagement: React.FC = () => {
),
},
{
key: "expectedCloseDate",
header: "Beklenen Kapanış",
key: 'expectedCloseDate',
header: 'Beklenen Kapanış',
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1 text-sm">
<FaCalendar className="w-3 h-3 text-gray-400" />
<span>
{new Date(opportunity.expectedCloseDate).toLocaleDateString(
"tr-TR"
)}
</span>
<span>{new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}</span>
</div>
),
},
{
key: "assignedTo",
header: "Sorumlu",
key: 'assignedTo',
header: 'Sorumlu',
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1">
<FaUser className="w-4 h-4 text-gray-400" />
@ -205,8 +186,8 @@ const OpportunityManagement: React.FC = () => {
),
},
{
key: "actions",
header: "İşlemler",
key: 'actions',
header: 'İşlemler',
render: (opportunity: CrmOpportunity) => (
<div className="flex gap-2">
<button
@ -233,238 +214,195 @@ const OpportunityManagement: React.FC = () => {
</div>
),
},
];
]
// Calculate statistics
const totalOpportunities = opportunities.length;
const totalOpportunities = opportunities.length
const activeOpportunities = opportunities.filter(
(o) =>
o.stage !== OpportunityStageEnum.ClosedWon &&
o.stage !== OpportunityStageEnum.ClosedLost
).length;
const totalValue = opportunities.reduce(
(sum, opp) => sum + opp.estimatedValue,
0
);
o.stage !== OpportunityStageEnum.ClosedWon && o.stage !== OpportunityStageEnum.ClosedLost,
).length
const totalValue = opportunities.reduce((sum, opp) => sum + opp.estimatedValue, 0)
const averageProbability =
opportunities.reduce((sum, opp) => sum + opp.probability, 0) /
opportunities.length || 0;
opportunities.reduce((sum, opp) => sum + opp.probability, 0) / opportunities.length || 0
// Stage distribution
const stageDistribution = Object.values(OpportunityStageEnum).map(
(stage) => ({
stage,
count: opportunities.filter((o) => o.stage === stage).length,
value: opportunities
.filter((o) => o.stage === stage)
.reduce((sum, o) => sum + o.estimatedValue, 0),
})
);
const stageDistribution = Object.values(OpportunityStageEnum).map((stage) => ({
stage,
count: opportunities.filter((o) => o.stage === stage).length,
value: opportunities
.filter((o) => o.stage === stage)
.reduce((sum, o) => sum + o.estimatedValue, 0),
}))
return (
<div className="space-y-3 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">
Teklif & Fırsat Yönetimi
</h2>
<p className="text-sm text-gray-600">
Satış fırsatları ve teklif takibi
</p>
</div>
<button
onClick={handleAdd}
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"
>
<FaPlus className="w-4 h-4" />
Yeni Fırsat
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget
title="Toplam Fırsat"
value={totalOpportunities}
color="blue"
icon="FaBullseye"
/>
<Widget
title="Aktif Fırsat"
value={activeOpportunities}
color="green"
icon="FaArrowUp"
/>
<Widget
title="Toplam Değer"
value={`${totalValue.toLocaleString()}`}
color="purple"
icon="FaDollarSign"
/>
<Widget
title="Ortalama Olasılık"
value={`%${averageProbability.toFixed(0)}`}
color="orange"
icon="FaArrowUp"
/>
</div>
{/* Stage Pipeline */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Satış Aşamaları
</h3>
<div className="grid grid-cols-2 md:grid-cols-6 gap-2">
{stageDistribution.map(({ stage, count, value }) => (
<div key={stage} className="text-center p-2 border rounded-lg">
<div
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getOpportunityStageColor(
stage
)}`}
>
{getOpportunityStageText(stage)}
</div>
<div className="text-lg font-bold text-gray-900 mb-1">
{count}
</div>
<div className="text-xs text-gray-500">
{value.toLocaleString()}
</div>
</div>
))}
</div>
</div>
{/* Win Rate Analysis */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Başarı Oranı Analizi
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-green-600 mb-1">
{
opportunities.filter(
(o) => o.stage === OpportunityStageEnum.ClosedWon
).length
}
</div>
<p className="text-sm text-gray-600">Kazanılan Fırsat</p>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-600 mb-1">
{
opportunities.filter(
(o) => o.stage === OpportunityStageEnum.ClosedLost
).length
}
</div>
<p className="text-sm text-gray-600">Kaybedilen Fırsat</p>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">
{Math.round(
(opportunities.filter(
(o) => o.stage === OpportunityStageEnum.ClosedWon
).length /
Math.max(
opportunities.filter(
(o) =>
o.stage === OpportunityStageEnum.ClosedWon ||
o.stage === OpportunityStageEnum.ClosedLost
).length,
1
)) *
100
)}
%
</div>
<p className="text-sm text-gray-600">Kazanma Oranı</p>
<Container>
<div className="space-y-3 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">Teklif & Fırsat Yönetimi</h2>
<p className="text-sm text-gray-600">Satış fırsatları ve teklif takibi</p>
</div>
<button
onClick={handleAdd}
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"
>
<FaPlus className="w-4 h-4" />
Yeni Fırsat
</button>
</div>
</div>
{/* Filters */}
<div className="flex gap-3 items-center">
<div className="flex-1">
<input
type="text"
placeholder="Fırsat başlığı ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(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"
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget title="Toplam Fırsat" value={totalOpportunities} color="blue" icon="FaBullseye" />
<Widget title="Aktif Fırsat" value={activeOpportunities} color="green" icon="FaArrowUp" />
<Widget
title="Toplam Değer"
value={`${totalValue.toLocaleString()}`}
color="purple"
icon="FaDollarSign"
/>
<Widget
title="Ortalama Olasılık"
value={`%${averageProbability.toFixed(0)}`}
color="orange"
icon="FaArrowUp"
/>
</div>
<select
value={selectedStage}
onChange={(e) =>
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"
>
<option value="all">Tüm Aşamalar</option>
{Object.values(OpportunityStageEnum).map((stage) => (
<option key={stage} value={stage}>
{getOpportunityStageText(stage)}
</option>
))}
</select>
<select
value={selectedCustomer}
onChange={(e) => setSelectedCustomer(e.target.value)}
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 Müşteriler</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
</div>
{/* Data Table */}
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredOpportunities} columns={columns} />
</div>
{filteredOpportunities.length === 0 && (
<div className="text-center py-12">
<FaBullseye className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-sm font-medium text-gray-900 mb-2">
Fırsat bulunamadı
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
{/* Stage Pipeline */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Satış Aşamaları</h3>
<div className="grid grid-cols-2 md:grid-cols-6 gap-2">
{stageDistribution.map(({ stage, count, value }) => (
<div key={stage} className="text-center p-2 border rounded-lg">
<div
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getOpportunityStageColor(
stage,
)}`}
>
{getOpportunityStageText(stage)}
</div>
<div className="text-lg font-bold text-gray-900 mb-1">{count}</div>
<div className="text-xs text-gray-500">{value.toLocaleString()}</div>
</div>
))}
</div>
</div>
)}
{/* Modals */}
<OpportunityForm
isOpen={isFormOpen}
onClose={handleCloseForm}
onSave={handleSaveOpportunity}
opportunity={selectedOpportunity}
mode={formMode}
/>
{/* Win Rate Analysis */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Başarı Oranı Analizi</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-green-600 mb-1">
{opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedWon).length}
</div>
<p className="text-sm text-gray-600">Kazanılan Fırsat</p>
</div>
<OpportunityDetails
isOpen={isDetailsOpen}
onClose={handleCloseDetails}
onEdit={handleEditFromDetails}
opportunity={selectedOpportunity}
/>
</div>
);
};
<div className="text-center">
<div className="text-2xl font-bold text-red-600 mb-1">
{opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedLost).length}
</div>
<p className="text-sm text-gray-600">Kaybedilen Fırsat</p>
</div>
export default OpportunityManagement;
<div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">
{Math.round(
(opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedWon).length /
Math.max(
opportunities.filter(
(o) =>
o.stage === OpportunityStageEnum.ClosedWon ||
o.stage === OpportunityStageEnum.ClosedLost,
).length,
1,
)) *
100,
)}
%
</div>
<p className="text-sm text-gray-600">Kazanma Oranı</p>
</div>
</div>
</div>
{/* Filters */}
<div className="flex gap-3 items-center">
<div className="flex-1">
<input
type="text"
placeholder="Fırsat başlığı ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(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"
/>
</div>
<select
value={selectedStage}
onChange={(e) => 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"
>
<option value="all">Tüm Aşamalar</option>
{Object.values(OpportunityStageEnum).map((stage) => (
<option key={stage} value={stage}>
{getOpportunityStageText(stage)}
</option>
))}
</select>
<select
value={selectedCustomer}
onChange={(e) => setSelectedCustomer(e.target.value)}
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 Müşteriler</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
</div>
{/* Data Table */}
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredOpportunities} columns={columns} />
</div>
{filteredOpportunities.length === 0 && (
<div className="text-center py-12">
<FaBullseye className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-sm font-medium text-gray-900 mb-2">Fırsat bulunamadı</h3>
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</div>
)}
{/* Modals */}
<OpportunityForm
isOpen={isFormOpen}
onClose={handleCloseForm}
onSave={handleSaveOpportunity}
opportunity={selectedOpportunity}
mode={formMode}
/>
<OpportunityDetails
isOpen={isDetailsOpen}
onClose={handleCloseDetails}
onEdit={handleEditFromDetails}
opportunity={selectedOpportunity}
/>
</div>
</Container>
)
}
export default OpportunityManagement

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import React, { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import {
FaEdit,
FaArrowLeft,
@ -17,440 +17,397 @@ import {
FaTruck,
FaFileInvoiceDollar,
FaExclamationCircle,
} from "react-icons/fa";
import {
CrmSalesOrder,
CrmSalesOrderItem,
SaleOrderItemStatusEnum,
} from "../../../types/crm";
import { mockSalesOrders } from "../../../mocks/mockSalesOrders";
} from 'react-icons/fa'
import { CrmSalesOrder, CrmSalesOrderItem, SaleOrderItemStatusEnum } from '../../../types/crm'
import { mockSalesOrders } from '../../../mocks/mockSalesOrders'
import {
getSaleOrderItemStatusnfo,
getSaleOrderStatusColor,
getSaleOrderStatusText,
} from "../../../utils/erp";
} from '../../../utils/erp'
import { Container } from '@/components/shared'
const SalesOrderView: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams();
const navigate = useNavigate()
const { id } = useParams()
const [order, setOrder] = useState<CrmSalesOrder | null>(null);
const [loading, setLoading] = useState(true);
const [order, setOrder] = useState<CrmSalesOrder | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (id) {
// Find the order from mock data
setTimeout(() => {
const foundOrder = mockSalesOrders.find((order) => order.id === id);
const foundOrder = mockSalesOrders.find((order) => order.id === id)
if (foundOrder) {
setOrder(foundOrder);
setOrder(foundOrder)
}
setLoading(false);
}, 1000);
setLoading(false)
}, 1000)
}
}, [id]);
}, [id])
if (loading) {
return (
<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>
);
)
}
if (!order) {
return (
<div className="flex flex-col items-center justify-center h-64">
<FaExclamationCircle className="text-4xl text-red-500 mb-4" />
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Sipariş Bulunamadı
</h2>
<h2 className="text-xl font-semibold text-gray-900 mb-2">Sipariş Bulunamadı</h2>
<p className="text-gray-600 mb-4">Belirtilen sipariş mevcut değil.</p>
<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"
>
Sipariş Listesine Dön
</button>
</div>
);
)
}
return (
<div className="space-y-4">
{/* Header */}
<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 space-x-4">
<button
onClick={() => navigate("/admin/crm/sales-orders")}
className="flex items-center text-gray-600 hover:text-gray-800 transition-colors"
>
<FaArrowLeft className="mr-2" />
Geri
</button>
<div className="h-6 border-l border-gray-300"></div>
<Container>
<div className="space-y-4">
{/* Header */}
<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 space-x-4">
<button
onClick={() => navigate('/admin/crm/sales-orders')}
className="flex items-center text-gray-600 hover:text-gray-800 transition-colors"
>
<FaArrowLeft className="mr-2" />
Geri
</button>
<div className="h-6 border-l border-gray-300"></div>
<div>
<h1 className="text-xl font-bold text-gray-900 flex items-center">
<FaShoppingCart className="mr-3 text-blue-600" />
{order.orderNumber}
</h1>
<p className="text-xs text-gray-500 mt-1">Satış Siparişi Detayları</p>
</div>
</div>
<div className="flex items-center space-x-2">
<span
className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${getSaleOrderStatusColor(
order.status,
)}`}
>
{getSaleOrderStatusText(order.status)}
</span>
<button
onClick={() => 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"
>
<FaEdit className="mr-2" />
Düzenle
</button>
<button className="bg-gray-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-gray-700 transition-colors flex items-center">
<FaPrint className="mr-2" />
Yazdır
</button>
<button className="bg-green-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-green-700 transition-colors flex items-center">
<FaDownload className="mr-2" />
İndir
</button>
</div>
</div>
{/* Order Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
<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>
<p className="text-sm font-medium text-blue-800">Sipariş Tarihi</p>
<p className="text-base font-bold text-blue-900">
{new Date(order.orderDate).toLocaleDateString('tr-TR')}
</p>
</div>
<FaCalendar className="text-2xl text-blue-600" />
</div>
</div>
<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>
<p className="text-sm font-medium text-green-800">Talep Edilen Tarih</p>
<p className="text-base font-bold text-green-900">
{new Date(order.requestedDeliveryDate).toLocaleDateString('tr-TR')}
</p>
</div>
<FaCalendar className="text-2xl text-green-600" />
</div>
</div>
<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>
<p className="text-sm font-medium text-purple-800">Onaylanan Tarih</p>
<p className="text-base font-bold text-purple-900">
{order.confirmedDeliveryDate
? new Date(order.confirmedDeliveryDate).toLocaleDateString('tr-TR')
: 'Belirtilmemiş'}
</p>
</div>
<FaCalendar className="text-2xl text-purple-600" />
</div>
</div>
<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>
<p className="text-sm font-medium text-amber-800">Toplam Tutar</p>
<p className="text-base font-bold text-amber-900">
{order.totalAmount.toLocaleString('tr-TR')} {order.currency}
</p>
</div>
<FaCalculator className="text-2xl text-amber-600" />
</div>
</div>
</div>
{/* Additional Info */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
<div>
<h1 className="text-xl font-bold text-gray-900 flex items-center">
<FaShoppingCart className="mr-3 text-blue-600" />
{order.orderNumber}
</h1>
<p className="text-xs text-gray-500 mt-1">
Satış Siparişi Detayları
</p>
<span className="font-medium text-gray-700">Ödeme Koşulları:</span>
<p className="text-gray-900">{order.paymentTerms}</p>
</div>
<div>
<span className="font-medium text-gray-700">Para Birimi:</span>
<p className="text-gray-900">{order.currency}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<span
className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${getSaleOrderStatusColor(
order.status
)}`}
>
{getSaleOrderStatusText(order.status)}
</span>
<button
onClick={() =>
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"
>
<FaEdit className="mr-2" />
Düzenle
</button>
<button className="bg-gray-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-gray-700 transition-colors flex items-center">
<FaPrint className="mr-2" />
Yazdır
</button>
<button className="bg-green-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-green-700 transition-colors flex items-center">
<FaDownload className="mr-2" />
İndir
</button>
</div>
</div>
{/* Order Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
<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>
<p className="text-sm font-medium text-blue-800">
Sipariş Tarihi
</p>
<p className="text-base font-bold text-blue-900">
{new Date(order.orderDate).toLocaleDateString("tr-TR")}
</p>
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="text-sm text-gray-600">
<span className="font-medium">Son Güncelleme:</span>
<div className="mt-1">
{order.lastModificationTime
? new Date(order.lastModificationTime).toLocaleString('tr-TR')
: 'Güncelleme yok'}
</div>
<FaCalendar className="text-2xl text-blue-600" />
</div>
</div>
<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>
<p className="text-sm font-medium text-green-800">
Talep Edilen Tarih
</p>
<p className="text-base font-bold text-green-900">
{new Date(order.requestedDeliveryDate).toLocaleDateString(
"tr-TR"
)}
</p>
</div>
<FaCalendar className="text-2xl text-green-600" />
</div>
</div>
<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>
<p className="text-sm font-medium text-purple-800">
Onaylanan Tarih
</p>
<p className="text-base font-bold text-purple-900">
{order.confirmedDeliveryDate
? new Date(order.confirmedDeliveryDate).toLocaleDateString(
"tr-TR"
)
: "Belirtilmemiş"}
</p>
</div>
<FaCalendar className="text-2xl text-purple-600" />
</div>
</div>
<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>
<p className="text-sm font-medium text-amber-800">
Toplam Tutar
</p>
<p className="text-base font-bold text-amber-900">
{order.totalAmount.toLocaleString("tr-TR")} {order.currency}
</p>
</div>
<FaCalculator className="text-2xl text-amber-600" />
</div>
</div>
</div>
{/* Additional Info */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
<div>
<span className="font-medium text-gray-700">Ödeme Koşulları:</span>
<p className="text-gray-900">{order.paymentTerms}</p>
</div>
<div>
<span className="font-medium text-gray-700">Para Birimi:</span>
<p className="text-gray-900">{order.currency}</p>
</div>
</div>
{/* Customer Information */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<FaBuilding className="mr-3 text-blue-600" />
Müşteri Bilgileri
</h2>
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="text-sm text-gray-600">
<span className="font-medium">Son Güncelleme:</span>
<div className="mt-1">
{order.lastModificationTime
? new Date(order.lastModificationTime).toLocaleString("tr-TR")
: "Güncelleme yok"}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 className="text-base font-medium text-gray-800 mb-2 flex items-center">
<FaUser className="mr-2 text-gray-600" />
Müşteri Detayları
</h3>
<div className="space-y-2">
<div>
<span className="text-sm font-medium text-gray-600">Firma Adı:</span>
<p className="text-sm font-medium text-gray-900">
{order.customer?.name || 'Müşteri bilgisi yok'}
</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">İletişim Kişisi:</span>
<p className="text-sm text-gray-900">
{order.customer?.primaryContact?.fullName || 'Belirtilmemiş'}
</p>
</div>
<div className="flex items-center text-sm text-gray-600">
<FaPhone className="mr-2" />
<span>{order.customer?.primaryContact?.phone || 'Belirtilmemiş'}</span>
</div>
<div className="flex items-center text-sm text-gray-600">
<FaEnvelope className="mr-2" />
<span>{order.customer?.primaryContact?.email || 'Belirtilmemiş'}</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Customer Information */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<FaBuilding className="mr-3 text-blue-600" />
Müşteri Bilgileri
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 className="text-base font-medium text-gray-800 mb-2 flex items-center">
<FaUser className="mr-2 text-gray-600" />
Müşteri Detayları
</h3>
<div className="space-y-2">
<div>
<span className="text-sm font-medium text-gray-600">
Firma Adı:
</span>
<p className="text-sm font-medium text-gray-900">
{order.customer?.name || "Müşteri bilgisi yok"}
<div>
<h3 className="text-base font-medium text-gray-800 mb-2 flex items-center">
<FaMapMarkerAlt className="mr-2 text-gray-600" />
Teslimat Adresi
</h3>
<div className="text-sm text-gray-900 space-y-1">
<p>{order.deliveryAddress.street}</p>
<p>
{order.deliveryAddress.city}, {order.deliveryAddress.state}
</p>
<p>{order.deliveryAddress.postalCode}</p>
<p>{order.deliveryAddress.country}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">
İletişim Kişisi:
</span>
<p className="text-sm text-gray-900">
{order.customer?.primaryContact?.fullName || "Belirtilmemiş"}
</p>
</div>
<div className="flex items-center text-sm text-gray-600">
<FaPhone className="mr-2" />
<span>
{order.customer?.primaryContact?.phone || "Belirtilmemiş"}
</span>
</div>
<div className="flex items-center text-sm text-gray-600">
<FaEnvelope className="mr-2" />
<span>
{order.customer?.primaryContact?.email || "Belirtilmemiş"}
</span>
</div>
</div>
</div>
<div>
<h3 className="text-base font-medium text-gray-800 mb-2 flex items-center">
<FaMapMarkerAlt className="mr-2 text-gray-600" />
Teslimat Adresi
</h3>
<div className="text-sm text-gray-900 space-y-1">
<p>{order.deliveryAddress.street}</p>
<p>
{order.deliveryAddress.city}, {order.deliveryAddress.state}
</p>
<p>{order.deliveryAddress.postalCode}</p>
<p>{order.deliveryAddress.country}</p>
</div>
</div>
</div>
</div>
{/* Order Items */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<FaClipboardList className="mr-3 text-blue-600" />
Sipariş Kalemleri
</h2>
{/* Order Items */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<FaClipboardList className="mr-3 text-blue-600" />
Sipariş Kalemleri
</h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Malzeme
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Miktar
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Teslim Edilen
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Birim Fiyat
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Toplam
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Durum
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{order.items.map((item: CrmSalesOrderItem) => {
const itemStatus = getSaleOrderItemStatusnfo(item.status);
return (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-4 py-2 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{item.material?.code || item.materialId}
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Malzeme
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Miktar
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Teslim Edilen
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Birim Fiyat
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Toplam
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Durum
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{order.items.map((item: CrmSalesOrderItem) => {
const itemStatus = getSaleOrderItemStatusnfo(item.status)
return (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-4 py-2 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{item.material?.code || item.materialId}
</div>
<div className="text-xs text-gray-500">
{item.material?.name || item.description}
</div>
</div>
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{item.quantity} {item.material?.baseUnit?.name || 'Adet'}
</td>
<td className="px-4 py-2 whitespace-nowrap">
<div className="font-medium">
{item.deliveredQuantity} {item.material?.baseUnit?.name || 'Adet'}
</div>
<div className="text-xs text-gray-500">
{item.material?.name || item.description}
{((item.deliveredQuantity / item.quantity) * 100).toFixed(1)}% tamamlandı
</div>
</div>
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{item.quantity} {item.material?.baseUnit?.name || "Adet"}
</td>
<td className="px-4 py-2 whitespace-nowrap">
<div className="font-medium">
{item.deliveredQuantity}{" "}
{item.material?.baseUnit?.name || "Adet"}
</div>
<div className="text-xs text-gray-500">
{(
(item.deliveredQuantity / item.quantity) *
100
).toFixed(1)}
% tamamlandı
</div>
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{item.unitPrice.toLocaleString("tr-TR")} {order.currency}
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm font-medium text-gray-900">
{item.totalAmount.toLocaleString("tr-TR")}{" "}
{order.currency}
</td>
<td className="px-4 py-2 whitespace-nowrap">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${itemStatus.color}`}
>
<itemStatus.icon
className={`mr-1 ${itemStatus.iconColor}`}
/>
{itemStatus.label}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Financial Summary */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<FaCalculator className="mr-3 text-blue-600" />
Mali Özet
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex justify-between py-1.5 border-b border-gray-100">
<span className="text-sm font-medium text-gray-600">
Ara Toplam:
</span>
<span className="text-sm text-gray-900">
{order.subtotal.toLocaleString("tr-TR")} {order.currency}
</span>
</div>
<div className="flex justify-between py-1.5 border-b border-gray-100">
<span className="text-sm font-medium text-gray-600">
İndirim:
</span>
<span className="text-sm text-gray-900">
-{order.discountAmount.toLocaleString("tr-TR")} {order.currency}
</span>
</div>
<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 text-gray-900">
{order.taxAmount.toLocaleString("tr-TR")} {order.currency}
</span>
</div>
<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">
{order.totalAmount.toLocaleString("tr-TR")} {order.currency}
</span>
</div>
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{item.unitPrice.toLocaleString('tr-TR')} {order.currency}
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm font-medium text-gray-900">
{item.totalAmount.toLocaleString('tr-TR')} {order.currency}
</td>
<td className="px-4 py-2 whitespace-nowrap">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${itemStatus.color}`}
>
<itemStatus.icon className={`mr-1 ${itemStatus.iconColor}`} />
{itemStatus.label}
</span>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
<div className="space-y-4">
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
<h3 className="text-sm font-medium text-blue-800 mb-2 flex items-center">
<FaTruck className="mr-2" />
Teslimat Durumu
</h3>
<div className="text-sm text-blue-900">
<p>Toplam Kalem: {order.items.length}</p>
<p>
Teslim Edilenler:{" "}
{
order.items.filter(
(item) =>
item.status === SaleOrderItemStatusEnum.Delivered
).length
}
</p>
<p>
Bekleyenler:{" "}
{
order.items.filter(
(item) =>
item.status !== SaleOrderItemStatusEnum.Delivered
).length
}
</p>
{/* Financial Summary */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<FaCalculator className="mr-3 text-blue-600" />
Mali Özet
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex justify-between py-1.5 border-b border-gray-100">
<span className="text-sm font-medium text-gray-600">Ara Toplam:</span>
<span className="text-sm text-gray-900">
{order.subtotal.toLocaleString('tr-TR')} {order.currency}
</span>
</div>
<div className="flex justify-between py-1.5 border-b border-gray-100">
<span className="text-sm font-medium text-gray-600">İndirim:</span>
<span className="text-sm text-gray-900">
-{order.discountAmount.toLocaleString('tr-TR')} {order.currency}
</span>
</div>
<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 text-gray-900">
{order.taxAmount.toLocaleString('tr-TR')} {order.currency}
</span>
</div>
<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">
{order.totalAmount.toLocaleString('tr-TR')} {order.currency}
</span>
</div>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<h3 className="text-sm font-medium text-green-800 mb-2 flex items-center">
<FaFileInvoiceDollar className="mr-2" />
Faturalama Durumu
</h3>
<div className="text-sm text-green-900">
<p>Teslimat Sayısı: {order.deliveries?.length || 0}</p>
<div className="space-y-4">
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
<h3 className="text-sm font-medium text-blue-800 mb-2 flex items-center">
<FaTruck className="mr-2" />
Teslimat Durumu
</h3>
<div className="text-sm text-blue-900">
<p>Toplam Kalem: {order.items.length}</p>
<p>
Teslim Edilenler:{' '}
{
order.items.filter(
(item) => item.status === SaleOrderItemStatusEnum.Delivered,
).length
}
</p>
<p>
Bekleyenler:{' '}
{
order.items.filter(
(item) => item.status !== SaleOrderItemStatusEnum.Delivered,
).length
}
</p>
</div>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<h3 className="text-sm font-medium text-green-800 mb-2 flex items-center">
<FaFileInvoiceDollar className="mr-2" />
Faturalama Durumu
</h3>
<div className="text-sm text-green-900">
<p>Teslimat Sayısı: {order.deliveries?.length || 0}</p>
</div>
</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 { useNavigate } from "react-router-dom";
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
FaShoppingCart,
FaPlus,
@ -9,146 +9,128 @@ import {
FaCalendar,
FaBox,
FaBuilding,
} from "react-icons/fa";
import { CrmSalesOrder, SaleOrderStatusEnum } from "../../../types/crm";
import DataTable, { Column } from "../../../components/common/DataTable";
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
import { mockSalesOrders } from "../../../mocks/mockSalesOrders";
import { BusinessParty } from "../../../types/common";
import Widget from "../../../components/common/Widget";
import {
getSaleOrderStatusColor,
getSaleOrderStatusText,
} from "../../../utils/erp";
} from 'react-icons/fa'
import { CrmSalesOrder, SaleOrderStatusEnum } from '../../../types/crm'
import DataTable, { Column } from '../../../components/common/DataTable'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { mockSalesOrders } from '../../../mocks/mockSalesOrders'
import { BusinessParty } from '../../../types/common'
import Widget from '../../../components/common/Widget'
import { getSaleOrderStatusColor, getSaleOrderStatusText } from '../../../utils/erp'
import { Container } from '@/components/shared'
const SalesOrders: React.FC = () => {
const navigate = useNavigate();
const [salesOrders] = useState<CrmSalesOrder[]>(mockSalesOrders);
const [customers] = useState<BusinessParty[]>(mockBusinessParties);
const [searchTerm, setSearchTerm] = useState("");
const [selectedStatus, setSelectedStatus] = useState<
SaleOrderStatusEnum | "all"
>("all");
const [selectedCustomer, setSelectedCustomer] = useState<string>("all");
const navigate = useNavigate()
const [salesOrders] = useState<CrmSalesOrder[]>(mockSalesOrders)
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [searchTerm, setSearchTerm] = useState('')
const [selectedStatus, setSelectedStatus] = useState<SaleOrderStatusEnum | 'all'>('all')
const [selectedCustomer, setSelectedCustomer] = useState<string>('all')
const handleAdd = () => {
navigate("/admin/crm/sales-orders/new");
};
navigate('/admin/crm/sales-orders/new')
}
const handleEdit = (order: CrmSalesOrder) => {
navigate(`/admin/crm/sales-orders/edit/${order.id}`);
};
navigate(`/admin/crm/sales-orders/edit/${order.id}`)
}
const handleDelete = (id: string) => {
console.log("Delete sales order:", id);
console.log('Delete sales order:', id)
// Implement delete functionality
};
}
const handleViewDetails = (order: CrmSalesOrder) => {
navigate(`/admin/crm/sales-orders/${order.id}`);
};
navigate(`/admin/crm/sales-orders/${order.id}`)
}
const filteredOrders = salesOrders.filter((order) => {
if (
searchTerm &&
!order.orderNumber.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false;
if (searchTerm && !order.orderNumber.toLowerCase().includes(searchTerm.toLowerCase())) {
return false
}
if (selectedStatus !== "all" && order.status !== selectedStatus) {
return false;
if (selectedStatus !== 'all' && order.status !== selectedStatus) {
return false
}
if (selectedCustomer !== "all" && order.customerId !== selectedCustomer) {
return false;
if (selectedCustomer !== 'all' && order.customerId !== selectedCustomer) {
return false
}
return true;
});
return true
})
const columns: Column<CrmSalesOrder>[] = [
{
key: "orderNumber",
header: "Sipariş No",
key: 'orderNumber',
header: 'Sipariş No',
sortable: true,
render: (order: CrmSalesOrder) => (
<div>
<div className="font-medium text-gray-900">{order.orderNumber}</div>
<div className="text-sm text-gray-500">
Sipariş #{order.id.substring(0, 8)}
</div>
<div className="text-sm text-gray-500">Sipariş #{order.id.substring(0, 8)}</div>
</div>
),
},
{
key: "customer",
header: "Müşteri",
key: 'customer',
header: 'Müşteri',
render: (order: CrmSalesOrder) => {
const customer = customers.find((c) => c.id === order.customerId);
const customer = customers.find((c) => c.id === order.customerId)
return customer ? (
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-400" />
<span>{customer.name}</span>
</div>
) : (
"-"
);
'-'
)
},
},
{
key: "orderDate",
header: "Sipariş Tarihi",
key: 'orderDate',
header: 'Sipariş Tarihi',
render: (order: CrmSalesOrder) => (
<div className="flex items-center gap-1 text-sm">
<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>
),
},
{
key: "deliveryDate",
header: "Teslimat Tarihi",
key: 'deliveryDate',
header: 'Teslimat Tarihi',
render: (order: CrmSalesOrder) =>
order.confirmedDeliveryDate ? (
<div className="flex items-center gap-1 text-sm">
<FaBox className="w-3 h-3 text-gray-400" />
<span>
{new Date(order.confirmedDeliveryDate).toLocaleDateString(
"tr-TR"
)}
</span>
<span>{new Date(order.confirmedDeliveryDate).toLocaleDateString('tr-TR')}</span>
</div>
) : order.requestedDeliveryDate ? (
<div className="flex items-center gap-1 text-sm text-gray-500">
<FaBox className="w-3 h-3 text-gray-400" />
<span>
{new Date(order.requestedDeliveryDate).toLocaleDateString(
"tr-TR"
)}{" "}
(İstenen)
{new Date(order.requestedDeliveryDate).toLocaleDateString('tr-TR')} (İstenen)
</span>
</div>
) : (
"-"
'-'
),
},
{
key: "totalAmount",
header: "Toplam Tutar",
key: 'totalAmount',
header: 'Toplam Tutar',
render: (order: CrmSalesOrder) => (
<div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-400" />
<span className="font-medium">
{order.totalAmount.toLocaleString()}
</span>
<span className="font-medium">{order.totalAmount.toLocaleString()}</span>
</div>
),
},
{
key: "status",
header: "Durum",
key: 'status',
header: 'Durum',
render: (order: CrmSalesOrder) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getSaleOrderStatusColor(
order.status
order.status,
)}`}
>
{getSaleOrderStatusText(order.status)}
@ -156,8 +138,8 @@ const SalesOrders: React.FC = () => {
),
},
{
key: "actions",
header: "İşlemler",
key: 'actions',
header: 'İşlemler',
render: (order: CrmSalesOrder) => (
<div className="flex gap-2">
<button
@ -184,231 +166,185 @@ const SalesOrders: React.FC = () => {
</div>
),
},
];
]
// Calculate statistics
const totalOrders = salesOrders.length;
const totalOrders = salesOrders.length
const activeOrders = salesOrders.filter(
(o) =>
o.status === SaleOrderStatusEnum.Confirmed ||
o.status === SaleOrderStatusEnum.InProduction
).length;
o.status === SaleOrderStatusEnum.Confirmed || o.status === SaleOrderStatusEnum.InProduction,
).length
const totalRevenue = salesOrders
.filter((o) => o.status === SaleOrderStatusEnum.Delivered)
.reduce((sum, order) => sum + order.totalAmount, 0);
.reduce((sum, order) => sum + order.totalAmount, 0)
const averageOrderValue =
salesOrders.reduce((sum, order) => sum + order.totalAmount, 0) /
salesOrders.length || 0;
salesOrders.reduce((sum, order) => sum + order.totalAmount, 0) / salesOrders.length || 0
// Status distribution
const statusDistribution = Object.values(SaleOrderStatusEnum).map(
(status) => ({
status,
count: salesOrders.filter((o) => o.status === status).length,
value: salesOrders
.filter((o) => o.status === status)
.reduce((sum, o) => sum + o.totalAmount, 0),
})
);
const statusDistribution = Object.values(SaleOrderStatusEnum).map((status) => ({
status,
count: salesOrders.filter((o) => o.status === status).length,
value: salesOrders
.filter((o) => o.status === status)
.reduce((sum, o) => sum + o.totalAmount, 0),
}))
// Monthly trend
const currentMonth = new Date();
const lastMonth = new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() - 1,
1
);
const currentMonthOrders = salesOrders.filter(
(o) => new Date(o.orderDate) >= lastMonth
).length;
const currentMonth = new Date()
const lastMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)
const currentMonthOrders = salesOrders.filter((o) => new Date(o.orderDate) >= lastMonth).length
const previousMonthOrders = salesOrders.filter((o) => {
const orderDate = new Date(o.orderDate);
const orderDate = new Date(o.orderDate)
return (
orderDate >=
new Date(lastMonth.getFullYear(), lastMonth.getMonth() - 1, 1) &&
orderDate >= new Date(lastMonth.getFullYear(), lastMonth.getMonth() - 1, 1) &&
orderDate < lastMonth
);
}).length;
)
}).length
return (
<div className="space-y-3 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">Satış Siparişleri</h2>
<p className="text-sm text-gray-600">
Müşteri siparişleri ve teslimat takibi
</p>
</div>
<button
onClick={handleAdd}
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"
>
<FaPlus className="w-4 h-4" />
Yeni Sipariş
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget
title="Toplam Sipariş"
value={totalOrders}
color="blue"
icon="FaShoppingCart"
/>
<Widget
title="Aktif Sipariş"
value={activeOrders}
color="orange"
icon="FaBox"
/>
<Widget
title="Toplam Gelir"
value={`${totalRevenue.toLocaleString()}`}
color="green"
icon="FaDollarSign"
/>
<Widget
title="Ortalama Sipariş"
value={`${averageOrderValue.toLocaleString()}`}
color="purple"
icon="FaArrowUp"
/>
</div>
{/* Order Status Pipeline */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Sipariş Durumu Dağılımı
</h3>
<div className="grid grid-cols-2 md:grid-cols-7 gap-2">
{statusDistribution.map(({ status, count, value }) => (
<div
key={status as string}
className="text-center p-2 border rounded-lg"
>
<div
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getSaleOrderStatusColor(
status as SaleOrderStatusEnum
)}`}
>
{getSaleOrderStatusText(status as SaleOrderStatusEnum)}
</div>
<div className="text-xl font-bold text-gray-900 mb-1">
{count}
</div>
<div className="text-xs text-gray-500">
{value.toLocaleString()}
</div>
</div>
))}
</div>
</div>
{/* Monthly Performance */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Aylık Performans
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">
{currentMonthOrders}
</div>
<p className="text-sm text-gray-600">Bu Ay</p>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-600 mb-1">
{previousMonthOrders}
</div>
<p className="text-sm text-gray-600">Geçen Ay</p>
</div>
<div className="text-center">
<div
className={`text-2xl font-bold mb-1 ${
currentMonthOrders >= previousMonthOrders
? "text-green-600"
: "text-red-600"
}`}
>
{previousMonthOrders > 0
? `${Math.round(
((currentMonthOrders - previousMonthOrders) /
previousMonthOrders) *
100
)}%`
: "-%"}
</div>
<p className="text-sm text-gray-600">Değişim</p>
<Container>
<div className="space-y-3 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">Satış Siparişleri</h2>
<p className="text-sm text-gray-600">Müşteri siparişleri ve teslimat takibi</p>
</div>
<button
onClick={handleAdd}
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"
>
<FaPlus className="w-4 h-4" />
Yeni Sipariş
</button>
</div>
</div>
{/* Filters */}
<div className="flex gap-3 items-center">
<div className="flex-1">
<input
type="text"
placeholder="Sipariş numarası ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(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"
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget title="Toplam Sipariş" value={totalOrders} color="blue" icon="FaShoppingCart" />
<Widget title="Aktif Sipariş" value={activeOrders} color="orange" icon="FaBox" />
<Widget
title="Toplam Gelir"
value={`${totalRevenue.toLocaleString()}`}
color="green"
icon="FaDollarSign"
/>
<Widget
title="Ortalama Sipariş"
value={`${averageOrderValue.toLocaleString()}`}
color="purple"
icon="FaArrowUp"
/>
</div>
<select
value={selectedStatus}
onChange={(e) =>
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"
>
<option value="all">Tüm Durumlar</option>
{Object.values(SaleOrderStatusEnum).map((status) => (
<option key={status as string} value={status as string}>
{getSaleOrderStatusText(status as SaleOrderStatusEnum)}
</option>
))}
</select>
<select
value={selectedCustomer}
onChange={(e) => setSelectedCustomer(e.target.value)}
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 Müşteriler</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
</div>
{/* Data Table */}
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredOrders} columns={columns} />
</div>
{filteredOrders.length === 0 && (
<div className="text-center py-12">
<FaShoppingCart className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-sm font-medium text-gray-900 mb-2">
Sipariş bulunamadı
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
{/* Order Status Pipeline */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Sipariş Durumu Dağılımı</h3>
<div className="grid grid-cols-2 md:grid-cols-7 gap-2">
{statusDistribution.map(({ status, count, value }) => (
<div key={status as string} className="text-center p-2 border rounded-lg">
<div
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getSaleOrderStatusColor(
status as SaleOrderStatusEnum,
)}`}
>
{getSaleOrderStatusText(status as SaleOrderStatusEnum)}
</div>
<div className="text-xl font-bold text-gray-900 mb-1">{count}</div>
<div className="text-xs text-gray-500">{value.toLocaleString()}</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default SalesOrders;
{/* Monthly Performance */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Aylık Performans</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">{currentMonthOrders}</div>
<p className="text-sm text-gray-600">Bu Ay</p>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-600 mb-1">{previousMonthOrders}</div>
<p className="text-sm text-gray-600">Geçen Ay</p>
</div>
<div className="text-center">
<div
className={`text-2xl font-bold mb-1 ${
currentMonthOrders >= previousMonthOrders ? 'text-green-600' : 'text-red-600'
}`}
>
{previousMonthOrders > 0
? `${Math.round(
((currentMonthOrders - previousMonthOrders) / previousMonthOrders) * 100,
)}%`
: '-%'}
</div>
<p className="text-sm text-gray-600">Değişim</p>
</div>
</div>
</div>
{/* Filters */}
<div className="flex gap-3 items-center">
<div className="flex-1">
<input
type="text"
placeholder="Sipariş numarası ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(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"
/>
</div>
<select
value={selectedStatus}
onChange={(e) => 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"
>
<option value="all">Tüm Durumlar</option>
{Object.values(SaleOrderStatusEnum).map((status) => (
<option key={status as string} value={status as string}>
{getSaleOrderStatusText(status as SaleOrderStatusEnum)}
</option>
))}
</select>
<select
value={selectedCustomer}
onChange={(e) => setSelectedCustomer(e.target.value)}
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 Müşteriler</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
</div>
{/* Data Table */}
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredOrders} columns={columns} />
</div>
{filteredOrders.length === 0 && (
<div className="text-center py-12">
<FaShoppingCart className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-sm font-medium text-gray-900 mb-2">Sipariş bulunamadı</h3>
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</div>
)}
</div>
</Container>
)
}
export default SalesOrders

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState } from 'react'
import {
FaUsers,
FaArrowLeft,
@ -7,475 +7,421 @@ import {
FaPlus,
FaTrash,
FaMapMarkerAlt,
} from "react-icons/fa";
import { useNavigate } from "react-router-dom";
import { CrmTerritory } from "../../../types/crm";
import { mockEmployees } from "../../../mocks/mockEmployees";
import { Team, TeamMember, TeamRoleEnum } from "../../../types/common";
} from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { CrmTerritory } from '../../../types/crm'
import { mockEmployees } from '../../../mocks/mockEmployees'
import { Team, TeamMember, TeamRoleEnum } from '../../../types/common'
import { Container } from '@/components/shared'
const SalesTeamCreate: React.FC = () => {
const navigate = useNavigate();
const navigate = useNavigate()
const [formData, setFormData] = useState({
teamCode: "",
name: "",
description: "",
managerId: "",
teamCode: '',
name: '',
description: '',
managerId: '',
isActive: true,
});
})
const [members, setMembers] = useState<Partial<TeamMember>[]>([]);
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([]);
const [errors, setErrors] = useState<Record<string, string>>({});
const [members, setMembers] = useState<Partial<TeamMember>[]>([])
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([])
const [errors, setErrors] = useState<Record<string, string>>({})
const handleInputChange = (field: string, value: string | boolean) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setFormData((prev) => ({ ...prev, [field]: value }))
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" }));
setErrors((prev) => ({ ...prev, [field]: '' }))
}
};
}
const addMember = () => {
setMembers((prev) => [
...prev,
{
employeeId: "",
employeeId: '',
role: TeamRoleEnum.Member,
isActive: true,
},
]);
};
])
}
const removeMember = (index: number) => {
setMembers((prev) => prev.filter((_, i) => i !== index));
};
setMembers((prev) => prev.filter((_, i) => i !== index))
}
const updateMember = (
index: number,
field: string,
value: string | TeamRoleEnum | boolean
) => {
const updateMember = (index: number, field: string, value: string | TeamRoleEnum | boolean) => {
setMembers((prev) =>
prev.map((member, i) =>
i === index ? { ...member, [field]: value } : member
)
);
};
prev.map((member, i) => (i === index ? { ...member, [field]: value } : member)),
)
}
const addTerritory = () => {
setTerritories((prev) => [
...prev,
{
territoryCode: "",
name: "",
description: "",
region: "",
territoryCode: '',
name: '',
description: '',
region: '',
countries: [],
cities: [],
isActive: true,
},
]);
};
])
}
const removeTerritory = (index: number) => {
setTerritories((prev) => prev.filter((_, i) => i !== index));
};
setTerritories((prev) => prev.filter((_, i) => i !== index))
}
const updateTerritory = (
index: number,
field: string,
value: string | string[] | boolean
) => {
const updateTerritory = (index: number, field: string, value: string | string[] | boolean) => {
setTerritories((prev) =>
prev.map((territory, i) =>
i === index ? { ...territory, [field]: value } : territory
)
);
};
prev.map((territory, i) => (i === index ? { ...territory, [field]: value } : territory)),
)
}
const validateForm = () => {
const newErrors: Record<string, string> = {};
const newErrors: Record<string, string> = {}
if (!formData.teamCode.trim()) {
newErrors.teamCode = "Takım kodu zorunludur";
newErrors.teamCode = 'Takım kodu zorunludur'
}
if (!formData.name.trim()) {
newErrors.name = "Takım adı zorunludur";
newErrors.name = 'Takım adı zorunludur'
}
if (!formData.managerId.trim()) {
newErrors.managerId = "Takım lideri seçimi zorunludur";
newErrors.managerId = 'Takım lideri seçimi zorunludur'
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSave = () => {
if (!validateForm()) return;
if (!validateForm()) return
const newTeam: Partial<Team> = {
...formData,
members: members.map((member, index) => ({
...member,
id: `member-new-${index}`,
teamId: "new-team",
teamId: 'new-team',
joinDate: new Date(),
})) as TeamMember[],
territories: territories.map((territory, index) => ({
...territory,
id: `territory-new-${index}`,
assignedTeamId: "new-team",
assignedTeamId: 'new-team',
})) as CrmTerritory[],
targets: [],
creationTime: new Date(),
lastModificationTime: new Date(),
};
}
console.log("Creating new sales team:", newTeam);
alert("Satış ekibi başarıyla oluşturuldu!");
navigate("/admin/crm/sales-teams");
};
console.log('Creating new sales team:', newTeam)
alert('Satış ekibi başarıyla oluşturuldu!')
navigate('/admin/crm/sales-teams')
}
const handleCancel = () => {
if (
confirm("Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?")
) {
navigate("/admin/crm/sales-teams");
if (confirm('Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?')) {
navigate('/admin/crm/sales-teams')
}
};
}
return (
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
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"
>
<FaArrowLeft className="w-5 h-5" />
</button>
<div>
<h2 className="text-xl font-bold text-gray-900">
Yeni Satış Ekibi
</h2>
<p className="text-sm text-gray-600 mt-1">
Yeni satış ekibi oluşturun ve ekip üyelerini atayın
</p>
<Container>
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
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"
>
<FaArrowLeft className="w-5 h-5" />
</button>
<div>
<h2 className="text-xl font-bold text-gray-900">Yeni Satış Ekibi</h2>
<p className="text-sm text-gray-600 mt-1">
Yeni satış ekibi oluşturun ve ekip üyelerini atayın
</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={handleCancel}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
>
<FaTimes className="w-4 h-4" />
İptal
</button>
<button
onClick={handleSave}
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"
>
<FaSave className="w-4 h-4" />
Kaydet
</button>
</div>
</div>
<div className="flex gap-2">
<button
onClick={handleCancel}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
>
<FaTimes className="w-4 h-4" />
İptal
</button>
<button
onClick={handleSave}
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"
>
<FaSave className="w-4 h-4" />
Kaydet
</button>
</div>
</div>
{/* Form */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Basic Information */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Temel Bilgiler
</h3>
{/* Form */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Basic Information */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Temel Bilgiler
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Kodu *
</label>
<input
type="text"
value={formData.teamCode}
onChange={(e) => 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 ${
errors.teamCode ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="ST-001"
/>
{errors.teamCode && (
<p className="text-red-500 text-sm mt-1">{errors.teamCode}</p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Kodu *
</label>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Lideri *
</label>
<select
value={formData.managerId}
onChange={(e) => 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 ${
errors.managerId ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">Çalışan seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName}
</option>
))}
</select>
{errors.managerId && (
<p className="text-red-500 text-sm mt-1">{errors.managerId}</p>
)}
</div>
</div>
<div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1">Takım Adı *</label>
<input
type="text"
value={formData.teamCode}
onChange={(e) =>
handleInputChange("teamCode", e.target.value)
}
value={formData.name}
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 ${
errors.teamCode ? "border-red-500" : "border-gray-300"
errors.name ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="ST-001"
placeholder="Kurumsal Satış Ekibi"
/>
{errors.teamCode && (
<p className="text-red-500 text-sm mt-1">{errors.teamCode}</p>
)}
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Lideri *
<div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1">ıklama</label>
<textarea
value={formData.description}
onChange={(e) => 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"
rows={3}
placeholder="Ekip açıklaması..."
/>
</div>
<div className="mt-4">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.isActive}
onChange={(e) => handleInputChange('isActive', e.target.checked)}
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>
</label>
<select
value={formData.managerId}
onChange={(e) =>
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 ${
errors.managerId ? "border-red-500" : "border-gray-300"
}`}
>
<option value="">Çalışan seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName}
</option>
))}
</select>
{errors.managerId && (
<p className="text-red-500 text-sm mt-1">
{errors.managerId}
</p>
)}
</div>
</div>
<div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Adı *
</label>
<input
type="text"
value={formData.name}
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 ${
errors.name ? "border-red-500" : "border-gray-300"
}`}
placeholder="Kurumsal Satış Ekibi"
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</div>
<div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1">
ıklama
</label>
<textarea
value={formData.description}
onChange={(e) =>
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"
rows={3}
placeholder="Ekip açıklaması..."
/>
</div>
<div className="mt-4">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.isActive}
onChange={(e) =>
handleInputChange("isActive", e.target.checked)
}
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>
</label>
</div>
</div>
{/* Team Members */}
<div className="bg-white rounded-lg shadow-sm border p-4 mt-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-900 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Ekip Üyeleri
</h3>
<button
onClick={addMember}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaPlus className="w-4 h-4" />
Üye Ekle
</button>
</div>
<div className="space-y-3">
{members.map((member, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 border border-gray-200 rounded-lg"
{/* Team Members */}
<div className="bg-white rounded-lg shadow-sm border p-4 mt-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-900 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Ekip Üyeleri
</h3>
<button
onClick={addMember}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Çalışan
</label>
<select
value={member.employeeId || ""}
onChange={(e) =>
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"
>
<option value="">Çalışan seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Rol
</label>
<select
value={member.role || TeamRoleEnum.Member}
onChange={(e) =>
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"
>
<option value={TeamRoleEnum.Member}>Üye</option>
<option value={TeamRoleEnum.Lead}>Takım Lideri</option>
<option value={TeamRoleEnum.Manager}>Yönetici</option>
</select>
</div>
</div>
<button
onClick={() => removeMember(index)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
<FaPlus className="w-4 h-4" />
Üye Ekle
</button>
</div>
<div className="space-y-3">
{members.map((member, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 border border-gray-200 rounded-lg"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
{members.length === 0 && (
<div className="text-center py-8 text-gray-500">
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz ekip üyesi eklenmedi</p>
<p className="text-sm">
Üye eklemek için yukarıdaki butona tıklayın
</p>
</div>
)}
</div>
</div>
</div>
{/* Territories */}
<div>
<div className="bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-900 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Bölgeler
</h3>
<button
onClick={addTerritory}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaPlus className="w-4 h-4" />
Bölge Ekle
</button>
</div>
<div className="space-y-3">
{territories.map((territory, index) => (
<div
key={index}
className="p-3 border border-gray-200 rounded-lg"
>
<div className="flex justify-between items-start mb-3">
<h4 className="font-medium text-gray-900">
Bölge {index + 1}
</h4>
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Çalışan
</label>
<select
value={member.employeeId || ''}
onChange={(e) => 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"
>
<option value="">Çalışan seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Rol</label>
<select
value={member.role || TeamRoleEnum.Member}
onChange={(e) => 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"
>
<option value={TeamRoleEnum.Member}>Üye</option>
<option value={TeamRoleEnum.Lead}>Takım Lideri</option>
<option value={TeamRoleEnum.Manager}>Yönetici</option>
</select>
</div>
</div>
<button
onClick={() => removeTerritory(index)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded"
onClick={() => removeMember(index)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
<div className="space-y-2">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge Kodu
</label>
<input
type="text"
value={territory.territoryCode || ""}
onChange={(e) =>
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"
placeholder="TR-IST"
/>
{members.length === 0 && (
<div className="text-center py-8 text-gray-500">
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz ekip üyesi eklenmedi</p>
<p className="text-sm">Üye eklemek için yukarıdaki butona tıklayın</p>
</div>
)}
</div>
</div>
</div>
{/* Territories */}
<div>
<div className="bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-900 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Bölgeler
</h3>
<button
onClick={addTerritory}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaPlus className="w-4 h-4" />
Bölge Ekle
</button>
</div>
<div className="space-y-3">
{territories.map((territory, index) => (
<div key={index} className="p-3 border border-gray-200 rounded-lg">
<div className="flex justify-between items-start mb-3">
<h4 className="font-medium text-gray-900">Bölge {index + 1}</h4>
<button
onClick={() => removeTerritory(index)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge Adı
</label>
<input
type="text"
value={territory.name || ""}
onChange={(e) =>
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"
placeholder="İstanbul Anadolu"
/>
</div>
<div className="space-y-2">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge Kodu
</label>
<input
type="text"
value={territory.territoryCode || ''}
onChange={(e) => 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"
placeholder="TR-IST"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge
</label>
<input
type="text"
value={territory.region || ""}
onChange={(e) =>
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"
placeholder="Marmara"
/>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge Adı
</label>
<input
type="text"
value={territory.name || ''}
onChange={(e) => 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"
placeholder="İstanbul Anadolu"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge
</label>
<input
type="text"
value={territory.region || ''}
onChange={(e) => 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"
placeholder="Marmara"
/>
</div>
</div>
</div>
</div>
))}
))}
{territories.length === 0 && (
<div className="text-center py-8 text-gray-500">
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz bölge eklenmedi</p>
<p className="text-sm">
Bölge eklemek için yukarıdaki butona tıklayın
</p>
</div>
)}
{territories.length === 0 && (
<div className="text-center py-8 text-gray-500">
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz bölge eklenmedi</p>
<p className="text-sm">Bölge eklemek için yukarıdaki butona tıklayın</p>
</div>
)}
</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 {
FaUsers,
FaArrowLeft,
@ -7,131 +7,120 @@ import {
FaPlus,
FaTrash,
FaMapMarkerAlt,
} from "react-icons/fa";
import { useNavigate, useParams } from "react-router-dom";
import { CrmTerritory } from "../../../types/crm";
import mockSalesTeams from "../../../mocks/mockSalesTeams";
import { mockEmployees } from "../../../mocks/mockEmployees";
import { Team, TeamMember, TeamRoleEnum } from "../../../types/common";
} from 'react-icons/fa'
import { useNavigate, useParams } from 'react-router-dom'
import { CrmTerritory } from '../../../types/crm'
import mockSalesTeams from '../../../mocks/mockSalesTeams'
import { mockEmployees } from '../../../mocks/mockEmployees'
import { Team, TeamMember, TeamRoleEnum } from '../../../types/common'
import { Container } from '@/components/shared'
const SalesTeamEdit: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const navigate = useNavigate()
const { id } = useParams<{ id: string }>()
const [formData, setFormData] = useState({
teamCode: "",
name: "",
description: "",
managerId: "",
teamCode: '',
name: '',
description: '',
managerId: '',
isActive: true,
});
})
const [members, setMembers] = useState<Partial<TeamMember>[]>([]);
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([]);
const [errors, setErrors] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(true);
const [members, setMembers] = useState<Partial<TeamMember>[]>([])
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([])
const [errors, setErrors] = useState<Record<string, string>>({})
const [loading, setLoading] = useState(true)
// Load team data on component mount
useEffect(() => {
const team = mockSalesTeams.find((t) => t.id === id);
const team = mockSalesTeams.find((t) => t.id === id)
if (team) {
setFormData({
teamCode: team.code,
name: team.name,
description: team.description || "",
description: team.description || '',
managerId: team.managerId,
isActive: team.isActive,
});
setMembers(team.members || []);
setTerritories(team.territories || []);
})
setMembers(team.members || [])
setTerritories(team.territories || [])
}
setLoading(false);
}, [id]);
setLoading(false)
}, [id])
const handleInputChange = (field: string, value: string | boolean) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setFormData((prev) => ({ ...prev, [field]: value }))
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" }));
setErrors((prev) => ({ ...prev, [field]: '' }))
}
};
}
const addMember = () => {
setMembers((prev) => [
...prev,
{
employeeId: "",
employeeId: '',
role: TeamRoleEnum.Member,
isActive: true,
},
]);
};
])
}
const removeMember = (index: number) => {
setMembers((prev) => prev.filter((_, i) => i !== index));
};
setMembers((prev) => prev.filter((_, i) => i !== index))
}
const updateMember = (
index: number,
field: string,
value: string | TeamRoleEnum | boolean
) => {
const updateMember = (index: number, field: string, value: string | TeamRoleEnum | boolean) => {
setMembers((prev) =>
prev.map((member, i) =>
i === index ? { ...member, [field]: value } : member
)
);
};
prev.map((member, i) => (i === index ? { ...member, [field]: value } : member)),
)
}
const addTerritory = () => {
setTerritories((prev) => [
...prev,
{
territoryCode: "",
name: "",
description: "",
region: "",
territoryCode: '',
name: '',
description: '',
region: '',
countries: [],
cities: [],
isActive: true,
},
]);
};
])
}
const removeTerritory = (index: number) => {
setTerritories((prev) => prev.filter((_, i) => i !== index));
};
setTerritories((prev) => prev.filter((_, i) => i !== index))
}
const updateTerritory = (
index: number,
field: string,
value: string | string[] | boolean
) => {
const updateTerritory = (index: number, field: string, value: string | string[] | boolean) => {
setTerritories((prev) =>
prev.map((territory, i) =>
i === index ? { ...territory, [field]: value } : territory
)
);
};
prev.map((territory, i) => (i === index ? { ...territory, [field]: value } : territory)),
)
}
const validateForm = () => {
const newErrors: Record<string, string> = {};
const newErrors: Record<string, string> = {}
if (!formData.teamCode.trim()) {
newErrors.teamCode = "Takım kodu zorunludur";
newErrors.teamCode = 'Takım kodu zorunludur'
}
if (!formData.name.trim()) {
newErrors.name = "Takım adı zorunludur";
newErrors.name = 'Takım adı zorunludur'
}
if (!formData.managerId.trim()) {
newErrors.managerId = "Takım lideri seçimi zorunludur";
newErrors.managerId = 'Takım lideri seçimi zorunludur'
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSave = () => {
if (!validateForm()) return;
if (!validateForm()) return
const updatedTeam: Partial<Team> = {
id,
@ -139,29 +128,27 @@ const SalesTeamEdit: React.FC = () => {
members: members.map((member, index) => ({
...member,
id: member.id || `member-updated-${index}`,
teamId: id || "",
teamId: id || '',
joinDate: member.joinDate || new Date(),
})) as TeamMember[],
territories: territories.map((territory, index) => ({
...territory,
id: territory.id || `territory-updated-${index}`,
assignedTeamId: id || "",
assignedTeamId: id || '',
})) as CrmTerritory[],
lastModificationTime: new Date(),
};
}
console.log("Updating sales team:", updatedTeam);
alert("Satış ekibi başarıyla güncellendi!");
navigate("/admin/crm/sales-teams");
};
console.log('Updating sales team:', updatedTeam)
alert('Satış ekibi başarıyla güncellendi!')
navigate('/admin/crm/sales-teams')
}
const handleCancel = () => {
if (
confirm("Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?")
) {
navigate("/admin/crm/sales-teams");
if (confirm('Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?')) {
navigate('/admin/crm/sales-teams')
}
};
}
if (loading) {
return (
@ -171,365 +158,320 @@ const SalesTeamEdit: React.FC = () => {
<p className="mt-4 text-gray-600">Yükleniyor...</p>
</div>
</div>
);
)
}
const team = mockSalesTeams.find((t) => t.id === id);
const team = mockSalesTeams.find((t) => t.id === id)
if (!team) {
return (
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2">
Satış ekibi bulunamadı
</h3>
<h3 className="text-lg font-medium text-gray-900 mb-2">Satış ekibi bulunamadı</h3>
<p className="text-gray-500 mb-4">
Aradığınız satış ekibi mevcut değil veya silinmiş olabilir.
</p>
<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"
>
Geri Dön
</button>
</div>
);
)
}
return (
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
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"
>
<FaArrowLeft className="w-5 h-5" />
</button>
<div>
<h2 className="text-xl font-bold text-gray-900">
Satış Ekibi Düzenle
</h2>
<p className="text-sm text-gray-600 mt-1">
{team.name} ekibini düzenleyin
</p>
<Container>
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
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"
>
<FaArrowLeft className="w-5 h-5" />
</button>
<div>
<h2 className="text-xl font-bold text-gray-900">Satış Ekibi Düzenle</h2>
<p className="text-sm text-gray-600 mt-1">{team.name} ekibini düzenleyin</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={handleCancel}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
>
<FaTimes className="w-4 h-4" />
İptal
</button>
<button
onClick={handleSave}
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"
>
<FaSave className="w-4 h-4" />
Güncelle
</button>
</div>
</div>
<div className="flex gap-2">
<button
onClick={handleCancel}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
>
<FaTimes className="w-4 h-4" />
İptal
</button>
<button
onClick={handleSave}
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"
>
<FaSave className="w-4 h-4" />
Güncelle
</button>
</div>
</div>
{/* Form */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Basic Information */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Temel Bilgiler
</h3>
{/* Form */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Basic Information */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Temel Bilgiler
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Kodu *
</label>
<input
type="text"
value={formData.teamCode}
onChange={(e) => 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 ${
errors.teamCode ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="ST-001"
/>
{errors.teamCode && (
<p className="text-red-500 text-sm mt-1">{errors.teamCode}</p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Kodu *
</label>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Lideri *
</label>
<select
value={formData.managerId}
onChange={(e) => 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 ${
errors.managerId ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">Lider seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName}
</option>
))}
</select>
{errors.managerId && (
<p className="text-red-500 text-sm mt-1">{errors.managerId}</p>
)}
</div>
</div>
<div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1">Takım Adı *</label>
<input
type="text"
value={formData.teamCode}
onChange={(e) =>
handleInputChange("teamCode", e.target.value)
}
value={formData.name}
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 ${
errors.teamCode ? "border-red-500" : "border-gray-300"
errors.name ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="ST-001"
placeholder="Kurumsal Satış Ekibi"
/>
{errors.teamCode && (
<p className="text-red-500 text-sm mt-1">{errors.teamCode}</p>
)}
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Lideri *
<div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1">ıklama</label>
<textarea
value={formData.description}
onChange={(e) => 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"
rows={3}
placeholder="Ekip açıklaması..."
/>
</div>
<div className="mt-4">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.isActive}
onChange={(e) => handleInputChange('isActive', e.target.checked)}
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>
</label>
<select
value={formData.managerId}
onChange={(e) =>
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 ${
errors.managerId ? "border-red-500" : "border-gray-300"
}`}
>
<option value="">Lider seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName}
</option>
))}
</select>
{errors.managerId && (
<p className="text-red-500 text-sm mt-1">
{errors.managerId}
</p>
)}
</div>
</div>
<div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1">
Takım Adı *
</label>
<input
type="text"
value={formData.name}
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 ${
errors.name ? "border-red-500" : "border-gray-300"
}`}
placeholder="Kurumsal Satış Ekibi"
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</div>
<div className="mt-4">
<label className="block text-xs font-medium text-gray-700 mb-1">
ıklama
</label>
<textarea
value={formData.description}
onChange={(e) =>
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"
rows={3}
placeholder="Ekip açıklaması..."
/>
</div>
<div className="mt-4">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.isActive}
onChange={(e) =>
handleInputChange("isActive", e.target.checked)
}
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>
</label>
</div>
</div>
{/* Team Members */}
<div className="bg-white rounded-lg shadow-sm border p-4 mt-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-900 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Ekip Üyeleri
</h3>
<button
onClick={addMember}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaPlus className="w-4 h-4" />
Üye Ekle
</button>
</div>
<div className="space-y-3">
{members.map((member, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 border border-gray-200 rounded-lg"
{/* Team Members */}
<div className="bg-white rounded-lg shadow-sm border p-4 mt-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-900 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Ekip Üyeleri
</h3>
<button
onClick={addMember}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Çalışan
</label>
<FaPlus className="w-4 h-4" />
Üye Ekle
</button>
</div>
<select
value={member.employeeId || ""}
onChange={(e) =>
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"
>
<option value="">Çalışan seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Rol
</label>
<select
value={member.role || TeamRoleEnum.Member}
onChange={(e) =>
updateMember(
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"
>
<option value={TeamRoleEnum.Member}>Üye</option>
<option value={TeamRoleEnum.Lead}>Takım Lideri</option>
<option value={TeamRoleEnum.Manager}>Yönetici</option>
</select>
</div>
</div>
<button
onClick={() => removeMember(index)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
<div className="space-y-3">
{members.map((member, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 border border-gray-200 rounded-lg"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Çalışan
</label>
{members.length === 0 && (
<div className="text-center py-8 text-gray-500">
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz ekip üyesi yok</p>
<p className="text-sm">
Üye eklemek için yukarıdaki butona tıklayın
</p>
</div>
)}
</div>
</div>
</div>
{/* Territories */}
<div>
<div className="bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-900 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Bölgeler
</h3>
<button
onClick={addTerritory}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaPlus className="w-4 h-4" />
Bölge Ekle
</button>
</div>
<div className="space-y-3">
{territories.map((territory, index) => (
<div
key={index}
className="p-3 border border-gray-200 rounded-lg"
>
<div className="flex justify-between items-start mb-3">
<h4 className="font-medium text-gray-900">
{territory.name || `Bölge ${index + 1}`}
</h4>
<select
value={member.employeeId || ''}
onChange={(e) => 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"
>
<option value="">Çalışan seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Rol</label>
<select
value={member.role || TeamRoleEnum.Member}
onChange={(e) =>
updateMember(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"
>
<option value={TeamRoleEnum.Member}>Üye</option>
<option value={TeamRoleEnum.Lead}>Takım Lideri</option>
<option value={TeamRoleEnum.Manager}>Yönetici</option>
</select>
</div>
</div>
<button
onClick={() => removeTerritory(index)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded"
onClick={() => removeMember(index)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
<div className="space-y-2">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge Kodu
</label>
<input
type="text"
value={territory.territoryCode || ""}
onChange={(e) =>
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"
placeholder="TR-IST"
/>
{members.length === 0 && (
<div className="text-center py-8 text-gray-500">
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz ekip üyesi yok</p>
<p className="text-sm">Üye eklemek için yukarıdaki butona tıklayın</p>
</div>
)}
</div>
</div>
</div>
{/* Territories */}
<div>
<div className="bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-900 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Bölgeler
</h3>
<button
onClick={addTerritory}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaPlus className="w-4 h-4" />
Bölge Ekle
</button>
</div>
<div className="space-y-3">
{territories.map((territory, index) => (
<div key={index} className="p-3 border border-gray-200 rounded-lg">
<div className="flex justify-between items-start mb-3">
<h4 className="font-medium text-gray-900">
{territory.name || `Bölge ${index + 1}`}
</h4>
<button
onClick={() => removeTerritory(index)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge Adı
</label>
<input
type="text"
value={territory.name || ""}
onChange={(e) =>
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"
placeholder="İstanbul Anadolu"
/>
</div>
<div className="space-y-2">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge Kodu
</label>
<input
type="text"
value={territory.territoryCode || ''}
onChange={(e) => 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"
placeholder="TR-IST"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge
</label>
<input
type="text"
value={territory.region || ""}
onChange={(e) =>
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"
placeholder="Marmara"
/>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge Adı
</label>
<input
type="text"
value={territory.name || ''}
onChange={(e) => 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"
placeholder="İstanbul Anadolu"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Bölge
</label>
<input
type="text"
value={territory.region || ''}
onChange={(e) => 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"
placeholder="Marmara"
/>
</div>
</div>
</div>
</div>
))}
))}
{territories.length === 0 && (
<div className="text-center py-8 text-gray-500">
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz bölge yok</p>
<p className="text-sm">
Bölge eklemek için yukarıdaki butona tıklayın
</p>
</div>
)}
{territories.length === 0 && (
<div className="text-center py-8 text-gray-500">
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz bölge yok</p>
<p className="text-sm">Bölge eklemek için yukarıdaki butona tıklayın</p>
</div>
)}
</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 {
FaUsers,
FaArrowLeft,
@ -11,475 +11,439 @@ import {
FaAward,
FaTrophy,
FaChartLine,
} from "react-icons/fa";
import { useNavigate, useParams } from "react-router-dom";
import mockSalesTeams from "../../../mocks/mockSalesTeams";
import dayjs from "dayjs";
import { Team, TeamRoleEnum } from "../../../types/common";
import { getTeamRoleText } from "../../../utils/erp";
} from 'react-icons/fa'
import { useNavigate, useParams } from 'react-router-dom'
import mockSalesTeams from '../../../mocks/mockSalesTeams'
import dayjs from 'dayjs'
import { Team, TeamRoleEnum } from '../../../types/common'
import { getTeamRoleText } from '../../../utils/erp'
import { Container } from '@/components/shared'
const SalesTeamView: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const navigate = useNavigate()
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 => {
if (!team.targets || team.targets.length === 0) return 0;
const activeTarget = team.targets.find((t) => t.status === "ACTIVE");
if (!activeTarget || activeTarget.targetValue === 0) return 0;
return (activeTarget.actualValue / activeTarget.targetValue) * 100;
};
if (!team.targets || team.targets.length === 0) return 0
const activeTarget = team.targets.find((t) => t.status === 'ACTIVE')
if (!activeTarget || activeTarget.targetValue === 0) return 0
return (activeTarget.actualValue / activeTarget.targetValue) * 100
}
const calculateTeamRevenue = (team: Team): number => {
if (!team.targets || team.targets.length === 0) return 0;
return team.targets.reduce((sum, target) => sum + target.actualValue, 0);
};
if (!team.targets || team.targets.length === 0) return 0
return team.targets.reduce((sum, target) => sum + target.actualValue, 0)
}
const handleEdit = () => {
navigate(`/admin/crm/sales-teams/edit/${id}`);
};
navigate(`/admin/crm/sales-teams/edit/${id}`)
}
const handleDelete = () => {
if (confirm(`${team?.name} ekibini silmek istediğinizden emin misiniz?`)) {
console.log("Delete sales team:", id);
alert("Ekip silindi (mock)");
navigate("/admin/crm/sales-teams");
console.log('Delete sales team:', id)
alert('Ekip silindi (mock)')
navigate('/admin/crm/sales-teams')
}
};
}
if (!team) {
return (
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2">
Satış ekibi bulunamadı
</h3>
<h3 className="text-lg font-medium text-gray-900 mb-2">Satış ekibi bulunamadı</h3>
<p className="text-gray-500 mb-4">
Aradığınız satış ekibi mevcut değil veya silinmiş olabilir.
</p>
<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"
>
Geri Dön
</button>
</div>
);
)
}
const performance = calculateTeamPerformance(team);
const revenue = calculateTeamRevenue(team);
const activeTarget = team.targets?.find((t) => t.status === "ACTIVE");
const performance = calculateTeamPerformance(team)
const revenue = calculateTeamRevenue(team)
const activeTarget = team.targets?.find((t) => t.status === 'ACTIVE')
return (
<div className="space-y-6 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => navigate("/admin/crm/sales-teams")}
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
>
<FaArrowLeft className="w-5 h-5" />
</button>
<div>
<h2 className="text-xl font-bold text-gray-900">{team.name}</h2>
<p className="text-gray-600 mt-1">
{team.code} Satış ekibi detayları
</p>
</div>
</div>
<div className="flex gap-3">
<button
onClick={handleEdit}
className="flex items-center gap-2 px-4 py-2 text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaEdit className="w-4 h-4" />
Düzenle
</button>
<button
onClick={handleDelete}
className="flex items-center gap-2 px-4 py-2 text-red-600 border border-red-200 rounded-md hover:bg-red-50 transition-colors"
>
<FaTrash className="w-4 h-4" />
Sil
</button>
</div>
</div>
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaUsers className="w-6 h-6 text-blue-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Üye Sayısı</p>
<p className="text-xl font-bold text-gray-900">
{team.members?.length || 0}
</p>
<Container>
<div className="space-y-6 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/admin/crm/sales-teams')}
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
>
<FaArrowLeft className="w-5 h-5" />
</button>
<div>
<h2 className="text-xl font-bold text-gray-900">{team.name}</h2>
<p className="text-gray-600 mt-1">{team.code} Satış ekibi detayları</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaMapMarkerAlt className="w-6 h-6 text-green-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Bölge Sayısı</p>
<p className="text-xl font-bold text-gray-900">
{team.territories?.length || 0}
</p>
</div>
<div className="flex gap-3">
<button
onClick={handleEdit}
className="flex items-center gap-2 px-4 py-2 text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaEdit className="w-4 h-4" />
Düzenle
</button>
<button
onClick={handleDelete}
className="flex items-center gap-2 px-4 py-2 text-red-600 border border-red-200 rounded-md hover:bg-red-50 transition-colors"
>
<FaTrash className="w-4 h-4" />
Sil
</button>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaArrowUp
className={`w-6 h-6 ${
performance >= 100
? "text-green-600"
: performance >= 80
? "text-yellow-600"
: "text-red-600"
}`}
/>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Performans</p>
<p
className={`text-xl font-bold ${
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaUsers className="w-6 h-6 text-blue-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Üye Sayısı</p>
<p className="text-xl font-bold text-gray-900">{team.members?.length || 0}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaMapMarkerAlt className="w-6 h-6 text-green-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Bölge Sayısı</p>
<p className="text-xl font-bold text-gray-900">{team.territories?.length || 0}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaArrowUp
className={`w-6 h-6 ${
performance >= 100
? "text-green-600"
? 'text-green-600'
: performance >= 80
? "text-yellow-600"
: "text-red-600"
? 'text-yellow-600'
: 'text-red-600'
}`}
>
%{performance.toFixed(1)}
</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaDollarSign className="w-6 h-6 text-purple-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Toplam Gelir</p>
<p className="text-lg font-bold text-gray-900">
{revenue.toLocaleString()}
</p>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Team Information */}
<div className="lg:col-span-2 space-y-4">
{/* Basic Info */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Ekip Bilgileri
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Ekip Kodu
</label>
<p className="text-gray-900">{team.code}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Durum
</label>
<span
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
team.isActive
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{team.isActive ? "Aktif" : "Pasif"}
</span>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
ıklama
</label>
<p className="text-gray-900">
{team.description || "Açıklama girilmemiş"}
</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Yönetici
</label>
<p className="text-gray-900">
{team.manager?.fullName || "Yönetici bilgisi girilmemiş"}
</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Oluşturulma Tarihi
</label>
<p className="text-gray-900">
{dayjs(team.creationTime).format("DD.MM.YYYY")}
</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Son Güncelleme
</label>
<p className="text-gray-900">
{dayjs(team.lastModificationTime).format("DD.MM.YYYY")}
</p>
</div>
</div>
</div>
{/* Team Members */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaUser className="w-5 h-5 mr-2" />
Ekip Üyeleri ({team.members?.length || 0})
</h3>
{team.members && team.members.length > 0 ? (
<div className="space-y-3">
{team.members.map((member, index) => (
<div
key={member.id || index}
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<FaUser className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="font-medium text-gray-900">
{member.employee?.fullName}
</p>
<p className="text-sm text-gray-500">
Katılım: {dayjs(member.joinDate).format("DD.MM.YYYY")}
</p>
</div>
</div>
<div className="text-right">
<span
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
member.role === TeamRoleEnum.Manager
? "bg-purple-100 text-purple-800"
: member.role === TeamRoleEnum.Lead
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-800"
}`}
>
{getTeamRoleText(member.role)}
</span>
<p className="text-xs text-gray-500 mt-1">
{member.isActive ? "Aktif" : "Pasif"}
</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz ekip üyesi eklenmemiş</p>
</div>
)}
</div>
{/* Territories */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Sorumlu Bölgeler ({team.territories?.length || 0})
</h3>
{team.territories && team.territories.length > 0 ? (
<div className="space-y-3">
{team.territories.map((territory, index) => (
<div
key={territory.id || index}
className="p-3 border border-gray-200 rounded-lg"
>
<div className="flex justify-between items-start mb-2">
<h4 className="font-medium text-gray-900">
{territory.name}
</h4>
<span className="text-sm text-gray-500">
{territory.territoryCode}
</span>
</div>
<p className="text-sm text-gray-600 mb-2">
{territory.description}
</p>
<div className="flex gap-4 text-sm text-gray-500">
<span>Bölge: {territory.region}</span>
<span>Ülke: {territory.countries?.join(", ")}</span>
{territory.cities && territory.cities.length > 0 && (
<span>Şehir: {territory.cities.join(", ")}</span>
)}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz bölge atanmamış</p>
</div>
)}
</div>
</div>
{/* Performance & Targets */}
<div className="space-y-4">
{/* Performance Overview */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
<FaChartLine className="w-5 h-5 mr-2" />
Performans
</h3>
<div className="space-y-3">
<div className="text-center">
<div
className={`text-3xl font-bold mb-2 ${
/>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Performans</p>
<p
className={`text-xl font-bold ${
performance >= 100
? "text-green-600"
? 'text-green-600'
: performance >= 80
? "text-yellow-600"
: "text-red-600"
? 'text-yellow-600'
: 'text-red-600'
}`}
>
%{performance.toFixed(1)}
</div>
<p className="text-sm text-gray-600">Hedef Başarımı</p>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className={`h-2.5 rounded-full ${
performance >= 100
? "bg-green-500"
: performance >= 80
? "bg-yellow-500"
: "bg-red-500"
}`}
style={{ width: `${Math.min(performance, 100)}%` }}
/>
</div>
<div className="flex justify-between text-sm text-gray-600">
<span>0%</span>
<span>100%</span>
</p>
</div>
</div>
</div>
{/* Current Target */}
{activeTarget && (
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaDollarSign className="w-6 h-6 text-purple-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Toplam Gelir</p>
<p className="text-lg font-bold text-gray-900">{revenue.toLocaleString()}</p>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Team Information */}
<div className="lg:col-span-2 space-y-4">
{/* Basic Info */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaUsers className="w-5 h-5 mr-2" />
Ekip Bilgileri
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Ekip Kodu</label>
<p className="text-gray-900">{team.code}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Durum</label>
<span
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
team.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{team.isActive ? 'Aktif' : 'Pasif'}
</span>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">ıklama</label>
<p className="text-gray-900">{team.description || 'Açıklama girilmemiş'}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Yönetici</label>
<p className="text-gray-900">
{team.manager?.fullName || 'Yönetici bilgisi girilmemiş'}
</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Oluşturulma Tarihi
</label>
<p className="text-gray-900">{dayjs(team.creationTime).format('DD.MM.YYYY')}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Son Güncelleme
</label>
<p className="text-gray-900">
{dayjs(team.lastModificationTime).format('DD.MM.YYYY')}
</p>
</div>
</div>
</div>
{/* Team Members */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaUser className="w-5 h-5 mr-2" />
Ekip Üyeleri ({team.members?.length || 0})
</h3>
{team.members && team.members.length > 0 ? (
<div className="space-y-3">
{team.members.map((member, index) => (
<div
key={member.id || index}
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<FaUser className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="font-medium text-gray-900">{member.employee?.fullName}</p>
<p className="text-sm text-gray-500">
Katılım: {dayjs(member.joinDate).format('DD.MM.YYYY')}
</p>
</div>
</div>
<div className="text-right">
<span
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
member.role === TeamRoleEnum.Manager
? 'bg-purple-100 text-purple-800'
: member.role === TeamRoleEnum.Lead
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{getTeamRoleText(member.role)}
</span>
<p className="text-xs text-gray-500 mt-1">
{member.isActive ? 'Aktif' : 'Pasif'}
</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz ekip üyesi eklenmemiş</p>
</div>
)}
</div>
{/* Territories */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
Sorumlu Bölgeler ({team.territories?.length || 0})
</h3>
{team.territories && team.territories.length > 0 ? (
<div className="space-y-3">
{team.territories.map((territory, index) => (
<div
key={territory.id || index}
className="p-3 border border-gray-200 rounded-lg"
>
<div className="flex justify-between items-start mb-2">
<h4 className="font-medium text-gray-900">{territory.name}</h4>
<span className="text-sm text-gray-500">{territory.territoryCode}</span>
</div>
<p className="text-sm text-gray-600 mb-2">{territory.description}</p>
<div className="flex gap-4 text-sm text-gray-500">
<span>Bölge: {territory.region}</span>
<span>Ülke: {territory.countries?.join(', ')}</span>
{territory.cities && territory.cities.length > 0 && (
<span>Şehir: {territory.cities.join(', ')}</span>
)}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p>Henüz bölge atanmamış</p>
</div>
)}
</div>
</div>
{/* Performance & Targets */}
<div className="space-y-4">
{/* Performance Overview */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
<FaTrophy className="w-5 h-5 mr-2" />
Aktif Hedef
<FaChartLine className="w-5 h-5 mr-2" />
Performans
</h3>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Hedef Dönem
</label>
<p className="text-gray-900">{activeTarget.targetPeriod}</p>
<div className="text-center">
<div
className={`text-3xl font-bold mb-2 ${
performance >= 100
? 'text-green-600'
: performance >= 80
? 'text-yellow-600'
: 'text-red-600'
}`}
>
%{performance.toFixed(1)}
</div>
<p className="text-sm text-gray-600">Hedef Başarımı</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Hedef Tutar
</label>
<p className="text-lg font-bold text-gray-900">
{activeTarget.targetValue.toLocaleString()}
</p>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className={`h-2.5 rounded-full ${
performance >= 100
? 'bg-green-500'
: performance >= 80
? 'bg-yellow-500'
: 'bg-red-500'
}`}
style={{ width: `${Math.min(performance, 100)}%` }}
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Gerçekleşen
</label>
<p className="text-lg font-bold text-blue-600">
{activeTarget.actualValue.toLocaleString()}
</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Başlangıç - Bitiş
</label>
<p className="text-sm text-gray-900">
{dayjs(activeTarget.startDate).format("DD.MM.YYYY")} -{" "}
{dayjs(activeTarget.endDate).format("DD.MM.YYYY")}
</p>
<div className="flex justify-between text-sm text-gray-600">
<span>0%</span>
<span>100%</span>
</div>
</div>
</div>
)}
{/* Achievement Badge */}
{performance >= 100 && (
<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" />
<h3 className="text-base font-bold mb-1">Tebrikler!</h3>
<p className="text-sm">
Bu ekip hedefini aştı ve başarı ödülünü hak etti!
</p>
</div>
)}
{/* Current Target */}
{activeTarget && (
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
<FaTrophy className="w-5 h-5 mr-2" />
Aktif Hedef
</h3>
{/* Quick Stats */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">
Hızlı İstatistikler
</h3>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Hedef Dönem
</label>
<p className="text-gray-900">{activeTarget.targetPeriod}</p>
</div>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600">Toplam Hedef:</span>
<span className="font-medium">{team.targets?.length || 0}</span>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Hedef Tutar
</label>
<p className="text-lg font-bold text-gray-900">
{activeTarget.targetValue.toLocaleString()}
</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Gerçekleşen
</label>
<p className="text-lg font-bold text-blue-600">
{activeTarget.actualValue.toLocaleString()}
</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">
Başlangıç - Bitiş
</label>
<p className="text-sm text-gray-900">
{dayjs(activeTarget.startDate).format('DD.MM.YYYY')} -{' '}
{dayjs(activeTarget.endDate).format('DD.MM.YYYY')}
</p>
</div>
</div>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Aktif Üye:</span>
<span className="font-medium">
{team.members?.filter((m) => m.isActive).length || 0}
</span>
)}
{/* Achievement Badge */}
{performance >= 100 && (
<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" />
<h3 className="text-base font-bold mb-1">Tebrikler!</h3>
<p className="text-sm">Bu ekip hedefini aştı ve başarı ödülünü hak etti!</p>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Kapsam Alanı:</span>
<span className="font-medium">
{team.territories?.reduce(
(acc, t) => acc + (t.cities?.length || 0),
0
) || 0}{" "}
şehir
</span>
)}
{/* Quick Stats */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">Hızlı İstatistikler</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600">Toplam Hedef:</span>
<span className="font-medium">{team.targets?.length || 0}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Aktif Üye:</span>
<span className="font-medium">
{team.members?.filter((m) => m.isActive).length || 0}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Kapsam Alanı:</span>
<span className="font-medium">
{team.territories?.reduce((acc, t) => acc + (t.cities?.length || 0), 0) || 0}{' '}
şehir
</span>
</div>
</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 {
FaUsers,
FaPlus,
@ -12,96 +12,93 @@ import {
FaList,
FaSearch,
FaFilter,
} from "react-icons/fa";
import { useNavigate } from "react-router-dom";
import DataTable, { Column } from "../../../components/common/DataTable";
import mockSalesTeams from "../../../mocks/mockSalesTeams";
import Widget from "../../../components/common/Widget";
import { Team } from "../../../types/common";
} from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import DataTable, { Column } from '../../../components/common/DataTable'
import mockSalesTeams from '../../../mocks/mockSalesTeams'
import Widget from '../../../components/common/Widget'
import { Team } from '../../../types/common'
import { Container } from '@/components/shared'
const SalesTeams: React.FC = () => {
const navigate = useNavigate();
const [teams] = useState<Team[]>(mockSalesTeams);
const [searchTerm, setSearchTerm] = useState("");
const [viewMode, setViewMode] = useState<"cards" | "list">("cards");
const [filterStatus, setFilterStatus] = useState<
"all" | "active" | "inactive"
>("all");
const navigate = useNavigate()
const [teams] = useState<Team[]>(mockSalesTeams)
const [searchTerm, setSearchTerm] = useState('')
const [viewMode, setViewMode] = useState<'cards' | 'list'>('cards')
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all')
const handleAdd = () => {
navigate("/admin/crm/sales-teams/new");
};
navigate('/admin/crm/sales-teams/new')
}
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 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?`)) {
console.log("Delete sales team:", id);
console.log('Delete sales team:', id)
// Implement delete logic here
alert("Ekip silindi (mock)");
alert('Ekip silindi (mock)')
}
};
}
const handleViewDetails = (team: Team) => {
navigate(`/admin/crm/sales-teams/${team.id}`);
};
navigate(`/admin/crm/sales-teams/${team.id}`)
}
// Calculate team performance based on targets
const calculateTeamPerformance = (team: Team): number => {
if (!team.targets || team.targets.length === 0) return 0;
const activeTarget = team.targets.find((t) => t.status === "ACTIVE");
if (!activeTarget || activeTarget.targetValue === 0) return 0;
return (activeTarget.actualValue / activeTarget.targetValue) * 100;
};
if (!team.targets || team.targets.length === 0) return 0
const activeTarget = team.targets.find((t) => t.status === 'ACTIVE')
if (!activeTarget || activeTarget.targetValue === 0) return 0
return (activeTarget.actualValue / activeTarget.targetValue) * 100
}
// Calculate team revenue
const calculateTeamRevenue = (team: Team): number => {
if (!team.targets || team.targets.length === 0) return 0;
return team.targets.reduce((sum, target) => sum + target.actualValue, 0);
};
if (!team.targets || team.targets.length === 0) return 0
return team.targets.reduce((sum, target) => sum + target.actualValue, 0)
}
// Filter teams based on search and status
const filteredTeams = teams.filter((team) => {
const matchesSearch =
searchTerm === "" ||
searchTerm === '' ||
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
team.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
team.description?.toLowerCase().includes(searchTerm.toLowerCase());
team.description?.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus =
filterStatus === "all" ||
(filterStatus === "active" && team.isActive) ||
(filterStatus === "inactive" && !team.isActive);
filterStatus === 'all' ||
(filterStatus === 'active' && team.isActive) ||
(filterStatus === 'inactive' && !team.isActive)
return matchesSearch && matchesStatus;
});
return matchesSearch && matchesStatus
})
// Table columns for list view
const columns: Column<Team>[] = [
{
key: "teamCode",
header: "Takım Kodu",
key: 'teamCode',
header: 'Takım Kodu',
sortable: true,
},
{
key: "name",
header: "Takım Adı",
key: 'name',
header: 'Takım Adı',
sortable: true,
render: (team: Team) => (
<div>
<div className="font-medium text-gray-900">{team.name}</div>
<div className="text-sm text-gray-500 truncate max-w-xs">
{team.description}
</div>
<div className="text-sm text-gray-500 truncate max-w-xs">{team.description}</div>
</div>
),
},
{
key: "memberCount",
header: "Üye Sayısı",
key: 'memberCount',
header: 'Üye Sayısı',
render: (team: Team) => (
<div className="flex items-center gap-1">
<FaUsers className="w-4 h-4 text-gray-500" />
@ -110,8 +107,8 @@ const SalesTeams: React.FC = () => {
),
},
{
key: "territories",
header: "Bölge Sayısı",
key: 'territories',
header: 'Bölge Sayısı',
render: (team: Team) => (
<div className="flex items-center gap-1">
<FaMapMarkerAlt className="w-4 h-4 text-gray-500" />
@ -120,55 +117,53 @@ const SalesTeams: React.FC = () => {
),
},
{
key: "performance",
header: "Performans",
key: 'performance',
header: 'Performans',
render: (team: Team) => {
const performance = calculateTeamPerformance(team);
const performance = calculateTeamPerformance(team)
const color =
performance >= 100
? "text-green-600"
? 'text-green-600'
: performance >= 80
? "text-yellow-600"
: "text-red-600";
? 'text-yellow-600'
: 'text-red-600'
return (
<div className={`flex items-center gap-1 ${color}`}>
<FaArrowUp className="w-4 h-4" />
<span className="font-medium">%{performance.toFixed(1)}</span>
</div>
);
)
},
},
{
key: "totalRevenue",
header: "Toplam Gelir",
key: 'totalRevenue',
header: 'Toplam Gelir',
render: (team: Team) => {
const revenue = calculateTeamRevenue(team);
const revenue = calculateTeamRevenue(team)
return (
<div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-500" />
<span>{revenue.toLocaleString()}</span>
</div>
);
)
},
},
{
key: "status",
header: "Durum",
key: 'status',
header: 'Durum',
render: (team: Team) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
team.isActive
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
team.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{team.isActive ? "Aktif" : "Pasif"}
{team.isActive ? 'Aktif' : 'Pasif'}
</span>
),
},
{
key: "actions",
header: "İşlemler",
key: 'actions',
header: 'İşlemler',
render: (team: Team) => (
<div className="flex gap-2">
<button
@ -195,302 +190,258 @@ const SalesTeams: React.FC = () => {
</div>
),
},
];
]
// Calculate overall statistics
const totalMembers = teams.reduce(
(sum, team) => sum + (team.members?.length || 0),
0
);
const totalRevenue = teams.reduce(
(sum, team) => sum + calculateTeamRevenue(team),
0
);
const totalMembers = teams.reduce((sum, team) => sum + (team.members?.length || 0), 0)
const totalRevenue = teams.reduce((sum, team) => sum + calculateTeamRevenue(team), 0)
const averagePerformance =
teams.reduce((sum, team) => sum + calculateTeamPerformance(team), 0) /
teams.length || 0;
const topPerformingTeams = teams.filter(
(team) => calculateTeamPerformance(team) >= 100
).length;
teams.reduce((sum, team) => sum + calculateTeamPerformance(team), 0) / teams.length || 0
const topPerformingTeams = teams.filter((team) => calculateTeamPerformance(team) >= 100).length
return (
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">Satış Ekipleri</h2>
<p className="text-gray-600 mt-1">
Satış ekipleri ve performans yönetimi
</p>
<Container>
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">Satış Ekipleri</h2>
<p className="text-gray-600 mt-1">Satış ekipleri ve performans yönetimi</p>
</div>
<button
onClick={handleAdd}
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"
>
<FaPlus className="w-4 h-4" />
Yeni Ekip
</button>
</div>
<button
onClick={handleAdd}
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"
>
<FaPlus className="w-4 h-4" />
Yeni Ekip
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Widget
title="Toplam Ekip"
value={teams.length}
color="blue"
icon="FaUsers"
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Widget title="Toplam Ekip" value={teams.length} color="blue" icon="FaUsers" />
<Widget
title="Toplam Üye"
value={totalMembers}
color="green"
icon="FaUser"
/>
<Widget title="Toplam Üye" value={totalMembers} color="green" icon="FaUser" />
<Widget
title="Toplam Gelir"
value={`${totalRevenue.toLocaleString()}`}
color="purple"
icon="FaDollarSign"
/>
<Widget
title="Toplam Gelir"
value={`${totalRevenue.toLocaleString()}`}
color="purple"
icon="FaDollarSign"
/>
<Widget
title="Hedef Aşan Ekip"
value={topPerformingTeams}
color="orange"
icon="FaAward"
/>
</div>
<Widget
title="Hedef Aşan Ekip"
value={topPerformingTeams}
color="orange"
icon="FaAward"
/>
</div>
{/* Performance Overview */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">
Performans Özeti
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">
%{averagePerformance.toFixed(1)}
{/* Performance Overview */}
<div className="bg-white rounded-lg shadow-sm border p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">Performans Özeti</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">
%{averagePerformance.toFixed(1)}
</div>
<p className="text-sm text-gray-600">Ortalama Performans</p>
</div>
<p className="text-sm text-gray-600">Ortalama Performans</p>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600 mb-1">
{topPerformingTeams}
<div className="text-center">
<div className="text-2xl font-bold text-green-600 mb-1">{topPerformingTeams}</div>
<p className="text-sm text-gray-600">Hedef Aşan Ekip</p>
</div>
<p className="text-sm text-gray-600">Hedef Aşan Ekip</p>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-600 mb-1">
{Math.round(totalRevenue / teams.length || 0).toLocaleString()}
<div className="text-center">
<div className="text-2xl font-bold text-purple-600 mb-1">
{Math.round(totalRevenue / teams.length || 0).toLocaleString()}
</div>
<p className="text-sm text-gray-600">Ekip Başına Ortalama</p>
</div>
<p className="text-sm text-gray-600">Ekip Başına Ortalama</p>
</div>
</div>
</div>
{/* Controls */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
{/* Search */}
<div className="flex items-center gap-2 flex-1">
<FaSearch className="w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Ekip adı, kod veya açıklama ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Controls */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
{/* Search */}
<div className="flex items-center gap-2 flex-1">
<FaSearch className="w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Ekip adı, kod veya açıklama ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Status Filter */}
<div className="flex items-center gap-2">
<FaFilter className="w-4 h-4 text-gray-400" />
<select
value={filterStatus}
onChange={(e) =>
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"
>
<option value="all">Tüm Durumlar</option>
<option value="active">Aktif</option>
<option value="inactive">Pasif</option>
</select>
</div>
{/* View Mode Toggle */}
<div className="flex bg-gray-100 rounded-lg p-1">
<button
onClick={() => setViewMode("list")}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
viewMode === "list"
? "bg-white text-blue-600 shadow-sm"
: "text-gray-600 hover:text-gray-900"
}`}
>
<FaList className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode("cards")}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
viewMode === "cards"
? "bg-white text-blue-600 shadow-sm"
: "text-gray-600 hover:text-gray-900"
}`}
>
<FaTh className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Content */}
{viewMode === "cards" ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredTeams.map((team) => {
const performance = calculateTeamPerformance(team);
const revenue = calculateTeamRevenue(team);
return (
<div
key={team.id}
className="bg-white rounded-lg shadow-sm border p-4"
{/* Status Filter */}
<div className="flex items-center gap-2">
<FaFilter className="w-4 h-4 text-gray-400" />
<select
value={filterStatus}
onChange={(e) => 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"
>
{/* Card Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="text-base font-semibold text-gray-900">
{team.name}
</h3>
<p className="text-sm text-gray-500">{team.code}</p>
{team.description && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
{team.description}
</p>
)}
</div>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
team.isActive
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{team.isActive ? "Aktif" : "Pasif"}
</span>
</div>
<option value="all">Tüm Durumlar</option>
<option value="active">Aktif</option>
<option value="inactive">Pasif</option>
</select>
</div>
{/* Card Stats */}
<div className="space-y-2 mb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Yönetici</span>
</div>
<span className="font-medium">
{team.manager?.fullName}
</span>
</div>
{/* View Mode Toggle */}
<div className="flex bg-gray-100 rounded-lg p-1">
<button
onClick={() => setViewMode('list')}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
viewMode === 'list'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<FaList className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('cards')}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
viewMode === 'cards'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<FaTh className="w-4 h-4" />
</button>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Üye Sayısı</span>
</div>
<span className="font-medium">
{team.members?.length || 0}
</span>
</div>
{/* Content */}
{viewMode === 'cards' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredTeams.map((team) => {
const performance = calculateTeamPerformance(team)
const revenue = calculateTeamRevenue(team)
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaMapMarkerAlt className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Bölge</span>
</div>
<span className="font-medium">
{team.territories?.length || 0}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaArrowUp className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Performans</span>
return (
<div key={team.id} className="bg-white rounded-lg shadow-sm border p-4">
{/* Card Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="text-base font-semibold text-gray-900">{team.name}</h3>
<p className="text-sm text-gray-500">{team.code}</p>
{team.description && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
{team.description}
</p>
)}
</div>
<span
className={`font-medium ${
performance >= 100
? "text-green-600"
: performance >= 80
? "text-yellow-600"
: "text-red-600"
className={`px-2 py-1 text-xs font-medium rounded-full ${
team.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
%{performance.toFixed(1)}
{team.isActive ? 'Aktif' : 'Pasif'}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaDollarSign className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Gelir</span>
{/* Card Stats */}
<div className="space-y-2 mb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Yönetici</span>
</div>
<span className="font-medium">{team.manager?.fullName}</span>
</div>
<span className="font-medium">
{revenue.toLocaleString()}
</span>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Üye Sayısı</span>
</div>
<span className="font-medium">{team.members?.length || 0}</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaMapMarkerAlt className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Bölge</span>
</div>
<span className="font-medium">{team.territories?.length || 0}</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaArrowUp className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Performans</span>
</div>
<span
className={`font-medium ${
performance >= 100
? 'text-green-600'
: performance >= 80
? 'text-yellow-600'
: 'text-red-600'
}`}
>
%{performance.toFixed(1)}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaDollarSign className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">Gelir</span>
</div>
<span className="font-medium">{revenue.toLocaleString()}</span>
</div>
</div>
{/* Card Actions */}
<div className="flex gap-2 pt-3 border-t justify-end">
<button
onClick={() => handleViewDetails(team)}
className="flex items-center justify-center gap-2 px-2.5 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaEye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(team)}
className="flex items-center justify-center gap-2 px-2.5 py-1.5 text-sm text-green-600 border border-green-200 rounded-md hover:bg-green-50 transition-colors"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(team.id)}
className="px-2.5 py-1.5 text-red-600 border border-red-200 rounded-md hover:bg-red-50 transition-colors"
title="Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
)
})}
</div>
) : (
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredTeams} columns={columns} />
</div>
)}
{/* Card Actions */}
<div className="flex gap-2 pt-3 border-t justify-end">
<button
onClick={() => handleViewDetails(team)}
className="flex items-center justify-center gap-2 px-2.5 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50 transition-colors"
>
<FaEye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(team)}
className="flex items-center justify-center gap-2 px-2.5 py-1.5 text-sm text-green-600 border border-green-200 rounded-md hover:bg-green-50 transition-colors"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(team.id)}
className="px-2.5 py-1.5 text-red-600 border border-red-200 rounded-md hover:bg-red-50 transition-colors"
title="Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</div>
) : (
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredTeams} columns={columns} />
</div>
)}
{filteredTeams.length === 0 && (
<div className="text-center py-12">
<FaUsers className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-base font-medium text-gray-900 mb-2">Ekip bulunamadı</h3>
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</div>
)}
</div>
</Container>
)
}
{filteredTeams.length === 0 && (
<div className="text-center py-12">
<FaUsers className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-base font-medium text-gray-900 mb-2">
Ekip bulunamadı
</h3>
<p className="text-gray-500">
Arama kriterlerinizi değiştirmeyi deneyin.
</p>
</div>
)}
</div>
);
};
export default SalesTeams;
export default SalesTeams