Container Crm Management
This commit is contained in:
parent
ecb32cf6cf
commit
7d52573765
23 changed files with 6155 additions and 7366 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
FaTimes,
|
FaTimes,
|
||||||
FaCalendar,
|
FaCalendar,
|
||||||
|
|
@ -12,10 +12,10 @@ import {
|
||||||
FaFlag,
|
FaFlag,
|
||||||
FaFileAlt,
|
FaFileAlt,
|
||||||
FaHistory,
|
FaHistory,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import { CrmActivity } from "../../../types/crm";
|
import { CrmActivity } from '../../../types/crm'
|
||||||
import { mockEmployees } from "../../../mocks/mockEmployees";
|
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import {
|
import {
|
||||||
getActivityStatusColor,
|
getActivityStatusColor,
|
||||||
getActivityStatusText,
|
getActivityStatusText,
|
||||||
|
|
@ -23,28 +23,22 @@ import {
|
||||||
getPsActivityTypeText,
|
getPsActivityTypeText,
|
||||||
getPriorityColor,
|
getPriorityColor,
|
||||||
getPriorityText,
|
getPriorityText,
|
||||||
} from "../../../utils/erp";
|
getActivityTypeText,
|
||||||
|
} from '../../../utils/erp'
|
||||||
|
|
||||||
interface ActivityDetailsProps {
|
interface ActivityDetailsProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onEdit: (activity: CrmActivity) => void;
|
onEdit: (activity: CrmActivity) => void
|
||||||
activity: CrmActivity | null;
|
activity: CrmActivity | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
const ActivityDetails: React.FC<ActivityDetailsProps> = ({ isOpen, onClose, onEdit, activity }) => {
|
||||||
isOpen,
|
if (!isOpen || !activity) return null
|
||||||
onClose,
|
|
||||||
onEdit,
|
|
||||||
activity,
|
|
||||||
}) => {
|
|
||||||
if (!isOpen || !activity) return null;
|
|
||||||
|
|
||||||
const customer = mockBusinessParties.find(
|
const customer = mockBusinessParties.find((c) => c.id === activity.customerId)
|
||||||
(c) => c.id === activity.customerId
|
|
||||||
);
|
|
||||||
|
|
||||||
const ActivityIcon = getActivityTypeIcon(activity.activityType);
|
const ActivityIcon = getActivityTypeIcon(activity.activityType)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
|
@ -56,12 +50,8 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<ActivityIcon className="w-6 h-6 text-blue-600" />
|
<ActivityIcon className="w-6 h-6 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
<h2 className="text-lg font-semibold text-gray-900">{activity.subject}</h2>
|
||||||
{activity.subject}
|
<p className="text-sm text-gray-600">{getActivityTypeText(activity.activityType)}</p>
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{getPsActivityTypeText(activity.activityType)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
@ -72,10 +62,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<FaEdit className="w-4 h-4" />
|
<FaEdit className="w-4 h-4" />
|
||||||
Düzenle
|
Düzenle
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-2">
|
||||||
onClick={onClose}
|
|
||||||
className="text-gray-400 hover:text-gray-600 p-2"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5" />
|
<FaTimes className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -87,7 +74,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div
|
||||||
className={`inline-flex px-3 py-1.5 rounded-full text-xs font-medium border ${getActivityStatusColor(
|
className={`inline-flex px-3 py-1.5 rounded-full text-xs font-medium border ${getActivityStatusColor(
|
||||||
activity.status
|
activity.status,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getActivityStatusText(activity.status)}
|
{getActivityStatusText(activity.status)}
|
||||||
|
|
@ -96,11 +83,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div className={`text-base font-semibold ${getPriorityColor(activity.priority)}`}>
|
||||||
className={`text-base font-semibold ${getPriorityColor(
|
|
||||||
activity.priority
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<FaFlag className="w-4 h-4" />
|
<FaFlag className="w-4 h-4" />
|
||||||
{getPriorityText(activity.priority)}
|
{getPriorityText(activity.priority)}
|
||||||
|
|
@ -112,7 +95,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="flex items-center justify-center gap-2 text-base font-semibold text-gray-900">
|
<div className="flex items-center justify-center gap-2 text-base font-semibold text-gray-900">
|
||||||
<FaCalendar className="w-4 h-4 text-gray-400" />
|
<FaCalendar className="w-4 h-4 text-gray-400" />
|
||||||
{new Date(activity.activityDate).toLocaleDateString("tr-TR")}
|
{new Date(activity.activityDate).toLocaleDateString('tr-TR')}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">Tarih</p>
|
<p className="text-xs text-gray-500 mt-1">Tarih</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -139,9 +122,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">Konu</label>
|
||||||
Konu
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-900">{activity.subject}</p>
|
<p className="text-gray-900">{activity.subject}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -150,9 +131,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
Açıklama
|
Açıklama
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900 whitespace-pre-wrap">
|
<p className="text-gray-900 whitespace-pre-wrap">{activity.description}</p>
|
||||||
{activity.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -163,13 +142,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
Başlangıç Saati
|
Başlangıç Saati
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">
|
||||||
{new Date(activity.startTime).toLocaleTimeString(
|
{new Date(activity.startTime).toLocaleTimeString('tr-TR', {
|
||||||
"tr-TR",
|
hour: '2-digit',
|
||||||
{
|
minute: '2-digit',
|
||||||
hour: "2-digit",
|
})}
|
||||||
minute: "2-digit",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -180,13 +156,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
Bitiş Saati
|
Bitiş Saati
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">
|
||||||
{new Date(activity.endTime).toLocaleTimeString(
|
{new Date(activity.endTime).toLocaleTimeString('tr-TR', {
|
||||||
"tr-TR",
|
hour: '2-digit',
|
||||||
{
|
minute: '2-digit',
|
||||||
hour: "2-digit",
|
})}
|
||||||
minute: "2-digit",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -232,16 +205,12 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaEnvelope className="w-4 h-4 text-gray-400" />
|
<FaEnvelope className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">{customer.primaryContact?.email}</span>
|
||||||
{customer.primaryContact?.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{customer.primaryContact?.phone && (
|
{customer.primaryContact?.phone && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaPhone className="w-4 h-4 text-gray-400" />
|
<FaPhone className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">{customer.primaryContact.phone}</span>
|
||||||
{customer.primaryContact.phone}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -261,14 +230,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">Sorumlu</label>
|
||||||
Sorumlu
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaUser className="w-4 h-4 text-gray-400" />
|
<FaUser className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">{activity.assigned?.fullName}</span>
|
||||||
{activity.assigned?.fullName}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -279,20 +244,15 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
</label>
|
</label>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{activity.participants.map((participantId, index) => {
|
{activity.participants.map((participantId, index) => {
|
||||||
const employee = mockEmployees.find(
|
const employee = mockEmployees.find((emp) => emp.id === participantId)
|
||||||
(emp) => emp.id === participantId
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={index} className="flex items-center gap-2">
|
||||||
key={index}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FaUsers className="w-4 h-4 text-gray-400" />
|
<FaUsers className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">
|
||||||
{employee ? employee.fullName : participantId}
|
{employee ? employee.fullName : participantId}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -316,9 +276,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaCalendar className="w-4 h-4 text-gray-400" />
|
<FaCalendar className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">
|
||||||
{new Date(activity.followUpDate).toLocaleDateString(
|
{new Date(activity.followUpDate).toLocaleDateString('tr-TR')}
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -329,9 +287,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
Takip Aktivitesi
|
Takip Aktivitesi
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">{activity.followUpActivity}</p>
|
||||||
{activity.followUpActivity}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -351,9 +307,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
Sonuç
|
Sonuç
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900 whitespace-pre-wrap">
|
<p className="text-gray-900 whitespace-pre-wrap">{activity.outcome}</p>
|
||||||
{activity.outcome}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -362,9 +316,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
Sonraki Adımlar
|
Sonraki Adımlar
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900 whitespace-pre-wrap">
|
<p className="text-gray-900 whitespace-pre-wrap">{activity.nextSteps}</p>
|
||||||
{activity.nextSteps}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -381,14 +333,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<div className="flex items-start gap-3 border-l-2 border-blue-200 pl-4">
|
<div className="flex items-start gap-3 border-l-2 border-blue-200 pl-4">
|
||||||
<div className="w-3 h-3 bg-blue-600 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
|
<div className="w-3 h-3 bg-blue-600 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-900">
|
<p className="text-sm font-medium text-gray-900">Aktivite Oluşturuldu</p>
|
||||||
Aktivite Oluşturuldu
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||||
<FaClock className="w-3 h-3" />
|
<FaClock className="w-3 h-3" />
|
||||||
{new Date(activity.creationTime).toLocaleDateString(
|
{new Date(activity.creationTime).toLocaleDateString('tr-TR')}
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -396,14 +344,10 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
<div className="flex items-start gap-3 border-l-2 border-gray-200 pl-4">
|
<div className="flex items-start gap-3 border-l-2 border-gray-200 pl-4">
|
||||||
<div className="w-3 h-3 bg-gray-400 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
|
<div className="w-3 h-3 bg-gray-400 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-900">
|
<p className="text-sm font-medium text-gray-900">Son Güncelleme</p>
|
||||||
Son Güncelleme
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||||
<FaClock className="w-3 h-3" />
|
<FaClock className="w-3 h-3" />
|
||||||
{new Date(
|
{new Date(activity.lastModificationTime).toLocaleDateString('tr-TR')}
|
||||||
activity.lastModificationTime
|
|
||||||
).toLocaleDateString("tr-TR")}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -414,7 +358,7 @@ const ActivityDetails: React.FC<ActivityDetailsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ActivityDetails;
|
export default ActivityDetails
|
||||||
|
|
|
||||||
|
|
@ -1,163 +1,140 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react'
|
||||||
import { FaTimes, FaCalendar, FaFileAlt, FaFlag } from "react-icons/fa";
|
import { FaTimes, FaCalendar, FaFileAlt, FaFlag } from 'react-icons/fa'
|
||||||
import {
|
import { CrmActivity, CrmActivityTypeEnum, ActivityStatusEnum } from '../../../types/crm'
|
||||||
CrmActivity,
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
CrmActivityTypeEnum,
|
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||||
ActivityStatusEnum,
|
import MultiSelectEmployee from '../../../components/common/MultiSelectEmployee'
|
||||||
} from "../../../types/crm";
|
import { BusinessParty, PriorityEnum } from '../../../types/common'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { getActivityStatusText, getPriorityText, getActivityTypeText } from '../../../utils/erp'
|
||||||
import { mockEmployees } from "../../../mocks/mockEmployees";
|
|
||||||
import MultiSelectEmployee from "../../../components/common/MultiSelectEmployee";
|
|
||||||
import { BusinessParty, PriorityEnum } from "../../../types/common";
|
|
||||||
import {
|
|
||||||
getActivityStatusText,
|
|
||||||
getPriorityText,
|
|
||||||
getActivityTypeText,
|
|
||||||
} from "../../../utils/erp";
|
|
||||||
|
|
||||||
interface ActivityFormProps {
|
interface ActivityFormProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onSave: (activity: CrmActivity) => void;
|
onSave: (activity: CrmActivity) => void
|
||||||
activity?: CrmActivity | null;
|
activity?: CrmActivity | null
|
||||||
mode: "create" | "edit";
|
mode: 'create' | 'edit'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActivityForm: React.FC<ActivityFormProps> = ({
|
const ActivityForm: React.FC<ActivityFormProps> = ({ isOpen, onClose, onSave, activity, mode }) => {
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onSave,
|
|
||||||
activity,
|
|
||||||
mode,
|
|
||||||
}) => {
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
id: "",
|
id: '',
|
||||||
activityType: CrmActivityTypeEnum.Call,
|
activityType: CrmActivityTypeEnum.Call,
|
||||||
subject: "",
|
subject: '',
|
||||||
description: "",
|
description: '',
|
||||||
customerId: "",
|
customerId: '',
|
||||||
contactId: "",
|
contactId: '',
|
||||||
activityDate: "",
|
activityDate: '',
|
||||||
startTime: "",
|
startTime: '',
|
||||||
endTime: "",
|
endTime: '',
|
||||||
duration: "",
|
duration: '',
|
||||||
assignedTo: "",
|
assignedTo: '',
|
||||||
participants: [] as string[], // Array of employee IDs
|
participants: [] as string[], // Array of employee IDs
|
||||||
status: ActivityStatusEnum.Planned,
|
status: ActivityStatusEnum.Planned,
|
||||||
priority: PriorityEnum.Normal,
|
priority: PriorityEnum.Normal,
|
||||||
followUpDate: "",
|
followUpDate: '',
|
||||||
followUpActivity: "",
|
followUpActivity: '',
|
||||||
outcome: "",
|
outcome: '',
|
||||||
nextSteps: "",
|
nextSteps: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const [customers] = useState<BusinessParty[]>(mockBusinessParties);
|
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activity && mode === "edit") {
|
if (activity && mode === 'edit') {
|
||||||
setFormData({
|
setFormData({
|
||||||
id: activity.id,
|
id: activity.id,
|
||||||
activityType: activity.activityType,
|
activityType: activity.activityType,
|
||||||
subject: activity.subject,
|
subject: activity.subject,
|
||||||
description: activity.description || "",
|
description: activity.description || '',
|
||||||
customerId: activity.customerId || "",
|
customerId: activity.customerId || '',
|
||||||
contactId: activity.contactId || "",
|
contactId: activity.contactId || '',
|
||||||
activityDate: activity.activityDate.toISOString().split("T")[0],
|
activityDate: activity.activityDate.toISOString().split('T')[0],
|
||||||
startTime: activity.startTime
|
startTime: activity.startTime ? activity.startTime.toISOString().slice(11, 16) : '',
|
||||||
? activity.startTime.toISOString().slice(11, 16)
|
endTime: activity.endTime ? activity.endTime.toISOString().slice(11, 16) : '',
|
||||||
: "",
|
duration: activity.duration?.toString() || '',
|
||||||
endTime: activity.endTime
|
|
||||||
? activity.endTime.toISOString().slice(11, 16)
|
|
||||||
: "",
|
|
||||||
duration: activity.duration?.toString() || "",
|
|
||||||
assignedTo: activity.assignedTo,
|
assignedTo: activity.assignedTo,
|
||||||
participants: activity.participants || [], // Keep as array of IDs
|
participants: activity.participants || [], // Keep as array of IDs
|
||||||
status: activity.status,
|
status: activity.status,
|
||||||
priority: activity.priority,
|
priority: activity.priority,
|
||||||
followUpDate: activity.followUpDate
|
followUpDate: activity.followUpDate
|
||||||
? activity.followUpDate.toISOString().split("T")[0]
|
? activity.followUpDate.toISOString().split('T')[0]
|
||||||
: "",
|
: '',
|
||||||
followUpActivity: activity.followUpActivity || "",
|
followUpActivity: activity.followUpActivity || '',
|
||||||
outcome: activity.outcome || "",
|
outcome: activity.outcome || '',
|
||||||
nextSteps: activity.nextSteps || "",
|
nextSteps: activity.nextSteps || '',
|
||||||
});
|
})
|
||||||
} else if (mode === "create") {
|
} else if (mode === 'create') {
|
||||||
const today = new Date().toISOString().split("T")[0];
|
const today = new Date().toISOString().split('T')[0]
|
||||||
setFormData({
|
setFormData({
|
||||||
id: `act_${Date.now()}`,
|
id: `act_${Date.now()}`,
|
||||||
activityType: CrmActivityTypeEnum.Call,
|
activityType: CrmActivityTypeEnum.Call,
|
||||||
subject: "",
|
subject: '',
|
||||||
description: "",
|
description: '',
|
||||||
customerId: "",
|
customerId: '',
|
||||||
contactId: "",
|
contactId: '',
|
||||||
activityDate: today,
|
activityDate: today,
|
||||||
startTime: "",
|
startTime: '',
|
||||||
endTime: "",
|
endTime: '',
|
||||||
duration: "",
|
duration: '',
|
||||||
assignedTo: "Mevcut Kullanıcı",
|
assignedTo: 'Mevcut Kullanıcı',
|
||||||
participants: [], // Empty array for new activities
|
participants: [], // Empty array for new activities
|
||||||
status: ActivityStatusEnum.Planned,
|
status: ActivityStatusEnum.Planned,
|
||||||
priority: PriorityEnum.Normal,
|
priority: PriorityEnum.Normal,
|
||||||
followUpDate: "",
|
followUpDate: '',
|
||||||
followUpActivity: "",
|
followUpActivity: '',
|
||||||
outcome: "",
|
outcome: '',
|
||||||
nextSteps: "",
|
nextSteps: '',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}, [activity, mode, isOpen]);
|
}, [activity, mode, isOpen])
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
e: React.ChangeEvent<
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
|
||||||
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
||||||
>
|
|
||||||
) => {
|
) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
// Clear error when user starts typing
|
// Clear error when user starts typing
|
||||||
if (errors[name]) {
|
if (errors[name]) {
|
||||||
setErrors((prev) => ({
|
setErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: "",
|
[name]: '',
|
||||||
}));
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {}
|
||||||
|
|
||||||
if (!formData.subject.trim()) {
|
if (!formData.subject.trim()) {
|
||||||
newErrors.subject = "Aktivite konusu zorunludur";
|
newErrors.subject = 'Aktivite konusu zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.activityDate) {
|
if (!formData.activityDate) {
|
||||||
newErrors.activityDate = "Aktivite tarihi zorunludur";
|
newErrors.activityDate = 'Aktivite tarihi zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.assignedTo.trim()) {
|
if (!formData.assignedTo.trim()) {
|
||||||
newErrors.assignedTo = "Sorumlu kişi zorunludur";
|
newErrors.assignedTo = 'Sorumlu kişi zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (formData.duration && (isNaN(Number(formData.duration)) || Number(formData.duration) <= 0)) {
|
||||||
formData.duration &&
|
newErrors.duration = 'Süre geçerli bir sayı olmalıdır'
|
||||||
(isNaN(Number(formData.duration)) || Number(formData.duration) <= 0)
|
|
||||||
) {
|
|
||||||
newErrors.duration = "Süre geçerli bir sayı olmalıdır";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors)
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const activityData: CrmActivity = {
|
const activityData: CrmActivity = {
|
||||||
|
|
@ -179,21 +156,19 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
participants: formData.participants, // Already an array of IDs
|
participants: formData.participants, // Already an array of IDs
|
||||||
status: formData.status,
|
status: formData.status,
|
||||||
priority: formData.priority,
|
priority: formData.priority,
|
||||||
followUpDate: formData.followUpDate
|
followUpDate: formData.followUpDate ? new Date(formData.followUpDate) : undefined,
|
||||||
? new Date(formData.followUpDate)
|
|
||||||
: undefined,
|
|
||||||
followUpActivity: formData.followUpActivity || undefined,
|
followUpActivity: formData.followUpActivity || undefined,
|
||||||
outcome: formData.outcome || undefined,
|
outcome: formData.outcome || undefined,
|
||||||
nextSteps: formData.nextSteps || undefined,
|
nextSteps: formData.nextSteps || undefined,
|
||||||
creationTime: activity?.creationTime || new Date(),
|
creationTime: activity?.creationTime || new Date(),
|
||||||
lastModificationTime: new Date(),
|
lastModificationTime: new Date(),
|
||||||
};
|
}
|
||||||
|
|
||||||
onSave(activityData);
|
onSave(activityData)
|
||||||
onClose();
|
onClose()
|
||||||
};
|
}
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
|
@ -201,12 +176,9 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-4 border-b">
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
{mode === "create" ? "Yeni Aktivite Oluştur" : "Aktivite Düzenle"}
|
{mode === 'create' ? 'Yeni Aktivite Oluştur' : 'Aktivite Düzenle'}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||||
onClick={onClose}
|
|
||||||
className="text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5" />
|
<FaTimes className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -240,28 +212,22 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Konu *</label>
|
||||||
Konu *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="subject"
|
name="subject"
|
||||||
value={formData.subject}
|
value={formData.subject}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.subject ? "border-red-500" : "border-gray-300"
|
errors.subject ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Aktivite konusunu girin"
|
placeholder="Aktivite konusunu girin"
|
||||||
/>
|
/>
|
||||||
{errors.subject && (
|
{errors.subject && <p className="text-red-500 text-sm mt-1">{errors.subject}</p>}
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.subject}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Açıklama</label>
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
name="description"
|
name="description"
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
|
|
@ -273,9 +239,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Müşteri</label>
|
||||||
Müşteri
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
name="customerId"
|
name="customerId"
|
||||||
value={formData.customerId}
|
value={formData.customerId}
|
||||||
|
|
@ -292,9 +256,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Öncelik</label>
|
||||||
Öncelik
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
name="priority"
|
name="priority"
|
||||||
value={formData.priority}
|
value={formData.priority}
|
||||||
|
|
@ -327,13 +289,11 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
value={formData.activityDate}
|
value={formData.activityDate}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.activityDate ? "border-red-500" : "border-gray-300"
|
errors.activityDate ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.activityDate && (
|
{errors.activityDate && (
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<p className="text-red-500 text-sm mt-1">{errors.activityDate}</p>
|
||||||
{errors.activityDate}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -376,19 +336,15 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
min="1"
|
min="1"
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.duration ? "border-red-500" : "border-gray-300"
|
errors.duration ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="60"
|
placeholder="60"
|
||||||
/>
|
/>
|
||||||
{errors.duration && (
|
{errors.duration && <p className="text-red-500 text-sm mt-1">{errors.duration}</p>}
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.duration}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Sorumlu *</label>
|
||||||
Sorumlu *
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={formData.assignedTo}
|
value={formData.assignedTo}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
|
@ -402,16 +358,12 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{errors.assignedTo && (
|
{errors.assignedTo && (
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<p className="text-red-500 text-sm mt-1">{errors.assignedTo}</p>
|
||||||
{errors.assignedTo}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Katılımcılar</label>
|
||||||
Katılımcılar
|
|
||||||
</label>
|
|
||||||
<MultiSelectEmployee
|
<MultiSelectEmployee
|
||||||
selectedEmployees={formData.participants}
|
selectedEmployees={formData.participants}
|
||||||
onChange={(employeeIds) =>
|
onChange={(employeeIds) =>
|
||||||
|
|
@ -426,9 +378,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Durum</label>
|
||||||
Durum
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
name="status"
|
name="status"
|
||||||
value={formData.status}
|
value={formData.status}
|
||||||
|
|
@ -454,9 +404,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Takip Tarihi</label>
|
||||||
Takip Tarihi
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
name="followUpDate"
|
name="followUpDate"
|
||||||
|
|
@ -482,9 +430,7 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Sonuç</label>
|
||||||
Sonuç
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
name="outcome"
|
name="outcome"
|
||||||
value={formData.outcome}
|
value={formData.outcome}
|
||||||
|
|
@ -523,13 +469,13 @@ const ActivityForm: React.FC<ActivityFormProps> = ({
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
{mode === "create" ? "Oluştur" : "Güncelle"}
|
{mode === 'create' ? 'Oluştur' : 'Güncelle'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ActivityForm;
|
export default ActivityForm
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
FaCalendar,
|
FaCalendar,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
|
|
@ -10,93 +10,82 @@ import {
|
||||||
FaList,
|
FaList,
|
||||||
FaTh,
|
FaTh,
|
||||||
FaFlag,
|
FaFlag,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import {
|
import { CrmActivity, CrmActivityTypeEnum, ActivityStatusEnum } from '../../../types/crm'
|
||||||
CrmActivity,
|
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||||
CrmActivityTypeEnum,
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
ActivityStatusEnum,
|
import { mockActivities } from '../../../mocks/mockActivities'
|
||||||
} from "../../../types/crm";
|
import ActivityForm from './ActivityForm'
|
||||||
import DataTable, { Column } from "../../../components/common/DataTable";
|
import ActivityDetails from './ActivityDetails'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { BusinessParty, PriorityEnum } from '../../../types/common'
|
||||||
import { mockActivities } from "../../../mocks/mockActivities";
|
import Widget from '../../../components/common/Widget'
|
||||||
import ActivityForm from "./ActivityForm";
|
|
||||||
import ActivityDetails from "./ActivityDetails";
|
|
||||||
import { BusinessParty, PriorityEnum } from "../../../types/common";
|
|
||||||
import Widget from "../../../components/common/Widget";
|
|
||||||
import {
|
import {
|
||||||
getActivityStatusColor,
|
getActivityStatusColor,
|
||||||
getActivityStatusText,
|
getActivityStatusText,
|
||||||
getActivityTypeIcon,
|
getActivityTypeIcon,
|
||||||
getPsActivityTypeText,
|
getActivityTypeText,
|
||||||
} from "../../../utils/erp";
|
} from '../../../utils/erp'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const ActivityRecords: React.FC = () => {
|
const ActivityRecords: React.FC = () => {
|
||||||
const [activities, setActivities] = useState<CrmActivity[]>(mockActivities);
|
const [activities, setActivities] = useState<CrmActivity[]>(mockActivities)
|
||||||
const [customers] = useState<BusinessParty[]>(mockBusinessParties);
|
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [selectedType, setSelectedType] = useState<CrmActivityTypeEnum | "all">(
|
const [selectedType, setSelectedType] = useState<CrmActivityTypeEnum | 'all'>('all')
|
||||||
"all"
|
const [selectedStatus, setSelectedStatus] = useState<ActivityStatusEnum | 'all'>('all')
|
||||||
);
|
const [viewMode, setViewMode] = useState<'list' | 'cards'>('list')
|
||||||
const [selectedStatus, setSelectedStatus] = useState<
|
|
||||||
ActivityStatusEnum | "all"
|
|
||||||
>("all");
|
|
||||||
const [viewMode, setViewMode] = useState<"list" | "cards">("list");
|
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
const [isFormOpen, setIsFormOpen] = useState(false)
|
||||||
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
const [isDetailsOpen, setIsDetailsOpen] = useState(false)
|
||||||
const [selectedActivity, setSelectedActivity] = useState<CrmActivity | null>(
|
const [selectedActivity, setSelectedActivity] = useState<CrmActivity | null>(null)
|
||||||
null
|
const [formMode, setFormMode] = useState<'create' | 'edit'>('create')
|
||||||
);
|
|
||||||
const [formMode, setFormMode] = useState<"create" | "edit">("create");
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
setSelectedActivity(null);
|
setSelectedActivity(null)
|
||||||
setFormMode("create");
|
setFormMode('create')
|
||||||
setIsFormOpen(true);
|
setIsFormOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEdit = (activity: CrmActivity) => {
|
const handleEdit = (activity: CrmActivity) => {
|
||||||
setSelectedActivity(activity);
|
setSelectedActivity(activity)
|
||||||
setFormMode("edit");
|
setFormMode('edit')
|
||||||
setIsFormOpen(true);
|
setIsFormOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
if (confirm("Bu aktiviteyi silmek istediğinizden emin misiniz?")) {
|
if (confirm('Bu aktiviteyi silmek istediğinizden emin misiniz?')) {
|
||||||
setActivities((prev) => prev.filter((act) => act.id !== id));
|
setActivities((prev) => prev.filter((act) => act.id !== id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewDetails = (activity: CrmActivity) => {
|
const handleViewDetails = (activity: CrmActivity) => {
|
||||||
setSelectedActivity(activity);
|
setSelectedActivity(activity)
|
||||||
setIsDetailsOpen(true);
|
setIsDetailsOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSaveActivity = (activity: CrmActivity) => {
|
const handleSaveActivity = (activity: CrmActivity) => {
|
||||||
if (formMode === "create") {
|
if (formMode === 'create') {
|
||||||
setActivities((prev) => [...prev, activity]);
|
setActivities((prev) => [...prev, activity])
|
||||||
} else {
|
} else {
|
||||||
setActivities((prev) =>
|
setActivities((prev) => prev.map((act) => (act.id === activity.id ? activity : act)))
|
||||||
prev.map((act) => (act.id === activity.id ? activity : act))
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseForm = () => {
|
const handleCloseForm = () => {
|
||||||
setIsFormOpen(false);
|
setIsFormOpen(false)
|
||||||
setSelectedActivity(null);
|
setSelectedActivity(null)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleCloseDetails = () => {
|
const handleCloseDetails = () => {
|
||||||
setIsDetailsOpen(false);
|
setIsDetailsOpen(false)
|
||||||
setSelectedActivity(null);
|
setSelectedActivity(null)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEditFromDetails = (activity: CrmActivity) => {
|
const handleEditFromDetails = (activity: CrmActivity) => {
|
||||||
setIsDetailsOpen(false);
|
setIsDetailsOpen(false)
|
||||||
handleEdit(activity);
|
handleEdit(activity)
|
||||||
};
|
}
|
||||||
|
|
||||||
const filteredActivities = activities.filter((activity) => {
|
const filteredActivities = activities.filter((activity) => {
|
||||||
if (
|
if (
|
||||||
|
|
@ -104,81 +93,77 @@ const ActivityRecords: React.FC = () => {
|
||||||
!activity.subject.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
!activity.subject.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||||
!activity.description?.toLowerCase().includes(searchTerm.toLowerCase())
|
!activity.description?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
) {
|
) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if (selectedType !== "all" && activity.activityType !== selectedType) {
|
if (selectedType !== 'all' && activity.activityType !== selectedType) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if (selectedStatus !== "all" && activity.status !== selectedStatus) {
|
if (selectedStatus !== 'all' && activity.status !== selectedStatus) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
return true;
|
return true
|
||||||
});
|
})
|
||||||
|
|
||||||
const columns: Column<CrmActivity>[] = [
|
const columns: Column<CrmActivity>[] = [
|
||||||
{
|
{
|
||||||
key: "subject",
|
key: 'subject',
|
||||||
header: "Aktivite",
|
header: 'Aktivite',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (activity: CrmActivity) => {
|
render: (activity: CrmActivity) => {
|
||||||
const Icon = getActivityTypeIcon(activity.activityType);
|
const Icon = getActivityTypeIcon(activity.activityType)
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
<Icon className="w-5 h-5 text-blue-600" />
|
<Icon className="w-5 h-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900">
|
<div className="font-medium text-gray-900">{activity.subject}</div>
|
||||||
{activity.subject}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{getPsActivityTypeText(activity.activityType)}
|
{getActivityTypeText(activity.activityType)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "customer",
|
key: 'customer',
|
||||||
header: "Müşteri",
|
header: 'Müşteri',
|
||||||
render: (activity: CrmActivity) => {
|
render: (activity: CrmActivity) => {
|
||||||
const customer = customers.find((c) => c.id === activity.customerId);
|
const customer = customers.find((c) => c.id === activity.customerId)
|
||||||
return customer ? (
|
return customer ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaBuilding className="w-4 h-4 text-gray-400" />
|
<FaBuilding className="w-4 h-4 text-gray-400" />
|
||||||
<span>{customer.name}</span>
|
<span>{customer.name}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
'-'
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "activityDate",
|
key: 'activityDate',
|
||||||
header: "Tarih & Saat",
|
header: 'Tarih & Saat',
|
||||||
render: (activity: CrmActivity) => (
|
render: (activity: CrmActivity) => (
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="flex items-center gap-1 text-sm">
|
||||||
<FaCalendar className="w-3 h-3 text-gray-400" />
|
<FaCalendar className="w-3 h-3 text-gray-400" />
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>{new Date(activity.activityDate).toLocaleDateString('tr-TR')}</div>
|
||||||
{new Date(activity.activityDate).toLocaleDateString("tr-TR")}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-500">
|
<div className="text-gray-500">
|
||||||
{activity.startTime
|
{activity.startTime
|
||||||
? new Date(activity.startTime).toLocaleTimeString("tr-TR", {
|
? new Date(activity.startTime).toLocaleTimeString('tr-TR', {
|
||||||
hour: "2-digit",
|
hour: '2-digit',
|
||||||
minute: "2-digit",
|
minute: '2-digit',
|
||||||
})
|
})
|
||||||
: "-"}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "duration",
|
key: 'duration',
|
||||||
header: "Süre",
|
header: 'Süre',
|
||||||
render: (activity: CrmActivity) =>
|
render: (activity: CrmActivity) =>
|
||||||
activity.duration ? (
|
activity.duration ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|
@ -186,12 +171,12 @@ const ActivityRecords: React.FC = () => {
|
||||||
<span>{activity.duration} dk</span>
|
<span>{activity.duration} dk</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
'-'
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "assignedTo",
|
key: 'assignedTo',
|
||||||
header: "Sorumlu",
|
header: 'Sorumlu',
|
||||||
render: (activity: CrmActivity) => (
|
render: (activity: CrmActivity) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FaUser className="w-4 h-4 text-gray-400" />
|
<FaUser className="w-4 h-4 text-gray-400" />
|
||||||
|
|
@ -200,12 +185,12 @@ const ActivityRecords: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: 'status',
|
||||||
header: "Durum",
|
header: 'Durum',
|
||||||
render: (activity: CrmActivity) => (
|
render: (activity: CrmActivity) => (
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor(
|
className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor(
|
||||||
activity.status
|
activity.status,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getActivityStatusText(activity.status)}
|
{getActivityStatusText(activity.status)}
|
||||||
|
|
@ -213,8 +198,8 @@ const ActivityRecords: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: 'actions',
|
||||||
header: "İşlemler",
|
header: 'İşlemler',
|
||||||
render: (activity: CrmActivity) => (
|
render: (activity: CrmActivity) => (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -241,45 +226,39 @@ const ActivityRecords: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
// Calculate statistics
|
// Calculate statistics
|
||||||
const totalActivities = activities.length;
|
const totalActivities = activities.length
|
||||||
const completedActivities = activities.filter(
|
const completedActivities = activities.filter(
|
||||||
(a) => a.status === ActivityStatusEnum.Completed
|
(a) => a.status === ActivityStatusEnum.Completed,
|
||||||
).length;
|
).length
|
||||||
const pendingActivities = activities.filter(
|
const pendingActivities = activities.filter(
|
||||||
(a) =>
|
(a) => a.status === ActivityStatusEnum.Planned || a.status === ActivityStatusEnum.InProgress,
|
||||||
a.status === ActivityStatusEnum.Planned ||
|
).length
|
||||||
a.status === ActivityStatusEnum.InProgress
|
|
||||||
).length;
|
|
||||||
const todayActivities = activities.filter((a) => {
|
const todayActivities = activities.filter((a) => {
|
||||||
const today = new Date();
|
const today = new Date()
|
||||||
const activityDate = new Date(a.activityDate);
|
const activityDate = new Date(a.activityDate)
|
||||||
return activityDate.toDateString() === today.toDateString();
|
return activityDate.toDateString() === today.toDateString()
|
||||||
}).length;
|
}).length
|
||||||
|
|
||||||
// Activity type distribution
|
// Activity type distribution
|
||||||
const typeDistribution = Object.values(CrmActivityTypeEnum).map((type) => ({
|
const typeDistribution = Object.values(CrmActivityTypeEnum).map((type) => ({
|
||||||
type,
|
type,
|
||||||
count: activities.filter((a) => a.activityType === type).length,
|
count: activities.filter((a) => a.activityType === type).length,
|
||||||
completed: activities.filter(
|
completed: activities.filter(
|
||||||
(a) =>
|
(a) => a.activityType === type && a.status === ActivityStatusEnum.Completed,
|
||||||
a.activityType === type && a.status === ActivityStatusEnum.Completed
|
|
||||||
).length,
|
).length,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-4 pt-2">
|
<div className="space-y-4 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
<h2 className="text-xl font-bold text-gray-900">Görüşme / Aktivite Kayıtları</h2>
|
||||||
Görüşme / Aktivite Kayıtları
|
<p className="text-sm text-gray-600 mt-1">Müşteri etkileşimleri ve aktivite takibi</p>
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
|
||||||
Müşteri etkileşimleri ve aktivite takibi
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
|
|
@ -294,45 +273,22 @@ const ActivityRecords: React.FC = () => {
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<Widget
|
<Widget title="Toplam Aktivite" value={totalActivities} color="blue" icon="FaCalendar" />
|
||||||
title="Toplam Aktivite"
|
|
||||||
value={totalActivities}
|
|
||||||
color="blue"
|
|
||||||
icon="FaCalendar"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget title="Bekleyen" value={pendingActivities} color="orange" icon="FaClock" />
|
||||||
title="Bekleyen"
|
|
||||||
value={pendingActivities}
|
|
||||||
color="orange"
|
|
||||||
icon="FaClock"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget title="Tamamlanan" value={completedActivities} color="green" icon="FaUsers" />
|
||||||
title="Tamamlanan"
|
|
||||||
value={completedActivities}
|
|
||||||
color="green"
|
|
||||||
icon="FaUsers"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget title="Bugün" value={todayActivities} color="purple" icon="FaCalendar" />
|
||||||
title="Bugün"
|
|
||||||
value={todayActivities}
|
|
||||||
color="purple"
|
|
||||||
icon="FaCalendar"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Activity Types Distribution */}
|
{/* Activity Types Distribution */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3">
|
<h3 className="text-base font-semibold text-gray-900 mb-3">Aktivite Türü Dağılımı</h3>
|
||||||
Aktivite Türü Dağılımı
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-7 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-7 gap-3">
|
||||||
{typeDistribution.map(({ type, count, completed }) => {
|
{typeDistribution.map(({ type, count, completed }) => {
|
||||||
const Icon = getActivityTypeIcon(type);
|
const Icon = getActivityTypeIcon(type)
|
||||||
const completionRate =
|
const completionRate = count > 0 ? Math.round((completed / count) * 100) : 0
|
||||||
count > 0 ? Math.round((completed / count) * 100) : 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={type} className="text-center p-4 border rounded-lg">
|
<div key={type} className="text-center p-4 border rounded-lg">
|
||||||
|
|
@ -340,38 +296,28 @@ const ActivityRecords: React.FC = () => {
|
||||||
<Icon className="w-5 h-5 text-blue-600" />
|
<Icon className="w-5 h-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-medium text-gray-600 mb-1">
|
<div className="text-sm font-medium text-gray-600 mb-1">
|
||||||
{getPsActivityTypeText(type)}
|
{getActivityTypeText(type)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold text-gray-900 mb-1">
|
<div className="text-xl font-bold text-gray-900 mb-1">{count}</div>
|
||||||
{count}
|
<div className="text-xxs text-gray-500">%{completionRate} tamamlandı</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xxs text-gray-500">
|
)
|
||||||
%{completionRate} tamamlandı
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Activities */}
|
{/* Recent Activities */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3">
|
<h3 className="text-base font-semibold text-gray-900 mb-3">Son Aktiviteler</h3>
|
||||||
Son Aktiviteler
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4 pt-2">
|
<div className="space-y-4 pt-2">
|
||||||
{activities
|
{activities
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) => new Date(b.activityDate).getTime() - new Date(a.activityDate).getTime(),
|
||||||
new Date(b.activityDate).getTime() -
|
|
||||||
new Date(a.activityDate).getTime()
|
|
||||||
)
|
)
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map((activity) => {
|
.map((activity) => {
|
||||||
const Icon = getActivityTypeIcon(activity.activityType);
|
const Icon = getActivityTypeIcon(activity.activityType)
|
||||||
const customer = customers.find(
|
const customer = customers.find((c) => c.id === activity.customerId)
|
||||||
(c) => c.id === activity.customerId
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -383,31 +329,26 @@ const ActivityRecords: React.FC = () => {
|
||||||
<Icon className="w-5 h-5 text-blue-600" />
|
<Icon className="w-5 h-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900">
|
<h4 className="font-medium text-gray-900">{activity.subject}</h4>
|
||||||
{activity.subject}
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{customer?.name} •{" "}
|
{customer?.name} • {getActivityTypeText(activity.activityType)}
|
||||||
{getPsActivityTypeText(activity.activityType)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-sm text-gray-900">
|
||||||
{new Date(activity.activityDate).toLocaleDateString(
|
{new Date(activity.activityDate).toLocaleDateString('tr-TR')}
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor(
|
className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor(
|
||||||
activity.status
|
activity.status,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getActivityStatusText(activity.status)}
|
{getActivityStatusText(activity.status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -426,24 +367,20 @@ const ActivityRecords: React.FC = () => {
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={selectedType}
|
value={selectedType}
|
||||||
onChange={(e) =>
|
onChange={(e) => setSelectedType(e.target.value as CrmActivityTypeEnum | 'all')}
|
||||||
setSelectedType(e.target.value as CrmActivityTypeEnum | "all")
|
|
||||||
}
|
|
||||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">Tüm Türler</option>
|
<option value="all">Tüm Türler</option>
|
||||||
{Object.values(CrmActivityTypeEnum).map((type) => (
|
{Object.values(CrmActivityTypeEnum).map((type) => (
|
||||||
<option key={type} value={type}>
|
<option key={type} value={type}>
|
||||||
{getPsActivityTypeText(type)}
|
{getActivityTypeText(type)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={selectedStatus}
|
value={selectedStatus}
|
||||||
onChange={(e) =>
|
onChange={(e) => setSelectedStatus(e.target.value as ActivityStatusEnum | 'all')}
|
||||||
setSelectedStatus(e.target.value as ActivityStatusEnum | "all")
|
|
||||||
}
|
|
||||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">Tüm Durumlar</option>
|
<option value="all">Tüm Durumlar</option>
|
||||||
|
|
@ -456,21 +393,21 @@ const ActivityRecords: React.FC = () => {
|
||||||
|
|
||||||
<div className="flex bg-gray-100 rounded-md p-1">
|
<div className="flex bg-gray-100 rounded-md p-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("list")}
|
onClick={() => setViewMode('list')}
|
||||||
className={`px-3 py-1.5 flex items-center gap-2 text-sm ${
|
className={`px-3 py-1.5 flex items-center gap-2 text-sm ${
|
||||||
viewMode === "list"
|
viewMode === 'list'
|
||||||
? "bg-blue-600 text-white"
|
? 'bg-blue-600 text-white'
|
||||||
: "bg-white text-gray-700 hover:bg-gray-50"
|
: 'bg-white text-gray-700 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaList className="w-4 h-4" />
|
<FaList className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("cards")}
|
onClick={() => setViewMode('cards')}
|
||||||
className={`px-3 py-1.5 flex items-center gap-2 text-sm ${
|
className={`px-3 py-1.5 flex items-center gap-2 text-sm ${
|
||||||
viewMode === "cards"
|
viewMode === 'cards'
|
||||||
? "bg-blue-600 text-white"
|
? 'bg-blue-600 text-white'
|
||||||
: "bg-white text-gray-700 hover:bg-gray-50"
|
: 'bg-white text-gray-700 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaTh className="w-4 h-4" />
|
<FaTh className="w-4 h-4" />
|
||||||
|
|
@ -479,17 +416,15 @@ const ActivityRecords: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Display */}
|
{/* Data Display */}
|
||||||
{viewMode === "list" ? (
|
{viewMode === 'list' ? (
|
||||||
<div className="bg-white rounded-lg shadow-sm border">
|
<div className="bg-white rounded-lg shadow-sm border">
|
||||||
<DataTable data={filteredActivities} columns={columns} />
|
<DataTable data={filteredActivities} columns={columns} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{filteredActivities.map((activity) => {
|
{filteredActivities.map((activity) => {
|
||||||
const customer = customers.find(
|
const customer = customers.find((c) => c.id === activity.customerId)
|
||||||
(c) => c.id === activity.customerId
|
const ActivityIcon = getActivityTypeIcon(activity.activityType)
|
||||||
);
|
|
||||||
const ActivityIcon = getActivityTypeIcon(activity.activityType);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -507,13 +442,13 @@ const ActivityRecords: React.FC = () => {
|
||||||
{activity.subject}
|
{activity.subject}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{getPsActivityTypeText(activity.activityType)}
|
{getActivityTypeText(activity.activityType)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor(
|
className={`px-2 py-1 text-xs font-medium rounded-full ${getActivityStatusColor(
|
||||||
activity.status
|
activity.status,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getActivityStatusText(activity.status)}
|
{getActivityStatusText(activity.status)}
|
||||||
|
|
@ -525,28 +460,21 @@ const ActivityRecords: React.FC = () => {
|
||||||
{customer && (
|
{customer && (
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<FaBuilding className="w-4 h-4 text-gray-400" />
|
<FaBuilding className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900 truncate">
|
<span className="text-gray-900 truncate">{customer.name}</span>
|
||||||
{customer.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<FaCalendar className="w-4 h-4 text-gray-400" />
|
<FaCalendar className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">
|
||||||
{new Date(activity.activityDate).toLocaleDateString(
|
{new Date(activity.activityDate).toLocaleDateString('tr-TR')}
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
{activity.startTime && (
|
{activity.startTime && (
|
||||||
<span className="text-gray-500 ml-1">
|
<span className="text-gray-500 ml-1">
|
||||||
•{" "}
|
•{' '}
|
||||||
{new Date(activity.startTime).toLocaleTimeString(
|
{new Date(activity.startTime).toLocaleTimeString('tr-TR', {
|
||||||
"tr-TR",
|
hour: '2-digit',
|
||||||
{
|
minute: '2-digit',
|
||||||
hour: "2-digit",
|
})}
|
||||||
minute: "2-digit",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -560,9 +488,7 @@ const ActivityRecords: React.FC = () => {
|
||||||
{activity.duration && (
|
{activity.duration && (
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<FaClock className="w-4 h-4 text-gray-400" />
|
<FaClock className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">{activity.duration} dk</span>
|
||||||
{activity.duration} dk
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -571,29 +497,29 @@ const ActivityRecords: React.FC = () => {
|
||||||
className={`w-4 h-4 ${
|
className={`w-4 h-4 ${
|
||||||
activity.priority === PriorityEnum.High ||
|
activity.priority === PriorityEnum.High ||
|
||||||
activity.priority === PriorityEnum.Urgent
|
activity.priority === PriorityEnum.Urgent
|
||||||
? "text-red-500"
|
? 'text-red-500'
|
||||||
: activity.priority === PriorityEnum.Normal
|
: activity.priority === PriorityEnum.Normal
|
||||||
? "text-blue-500"
|
? 'text-blue-500'
|
||||||
: "text-gray-400"
|
: 'text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${
|
||||||
activity.priority === PriorityEnum.High ||
|
activity.priority === PriorityEnum.High ||
|
||||||
activity.priority === PriorityEnum.Urgent
|
activity.priority === PriorityEnum.Urgent
|
||||||
? "text-red-600"
|
? 'text-red-600'
|
||||||
: activity.priority === PriorityEnum.Normal
|
: activity.priority === PriorityEnum.Normal
|
||||||
? "text-blue-600"
|
? 'text-blue-600'
|
||||||
: "text-gray-600"
|
: 'text-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{activity.priority === PriorityEnum.Low
|
{activity.priority === PriorityEnum.Low
|
||||||
? "Düşük"
|
? 'Düşük'
|
||||||
: activity.priority === PriorityEnum.Normal
|
: activity.priority === PriorityEnum.Normal
|
||||||
? "Normal"
|
? 'Normal'
|
||||||
: activity.priority === PriorityEnum.High
|
: activity.priority === PriorityEnum.High
|
||||||
? "Yüksek"
|
? 'Yüksek'
|
||||||
: "Acil"}
|
: 'Acil'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -601,11 +527,11 @@ const ActivityRecords: React.FC = () => {
|
||||||
<p
|
<p
|
||||||
className="text-sm text-gray-600 overflow-hidden"
|
className="text-sm text-gray-600 overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
display: "-webkit-box",
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 2,
|
||||||
WebkitBoxOrient: "vertical",
|
WebkitBoxOrient: 'vertical',
|
||||||
lineHeight: "1.4em",
|
lineHeight: '1.4em',
|
||||||
maxHeight: "2.8em",
|
maxHeight: '2.8em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activity.description}
|
{activity.description}
|
||||||
|
|
@ -635,7 +561,7 @@ const ActivityRecords: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -643,12 +569,8 @@ const ActivityRecords: React.FC = () => {
|
||||||
{filteredActivities.length === 0 && (
|
{filteredActivities.length === 0 && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaCalendar className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
<FaCalendar className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Aktivite bulunamadı</h3>
|
||||||
Aktivite bulunamadı
|
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500">
|
|
||||||
Arama kriterlerinizi değiştirmeyi deneyin.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -668,7 +590,8 @@ const ActivityRecords: React.FC = () => {
|
||||||
activity={selectedActivity}
|
activity={selectedActivity}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default ActivityRecords;
|
export default ActivityRecords
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom'
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
FaUserCheck,
|
FaUserCheck,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
|
|
@ -19,62 +19,55 @@ import {
|
||||||
FaGlobe,
|
FaGlobe,
|
||||||
FaUser,
|
FaUser,
|
||||||
FaChartLine,
|
FaChartLine,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames'
|
||||||
import { CustomerSegmentEnum } from "../../../types/crm";
|
import { CustomerSegmentEnum } from '../../../types/crm'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import Widget from "../../../components/common/Widget";
|
import Widget from '../../../components/common/Widget'
|
||||||
import { BusinessPartyStatusEnum, PartyType } from "../../../types/common";
|
import { BusinessPartyStatusEnum, PartyType } from '../../../types/common'
|
||||||
import {
|
import {
|
||||||
getBusinessPartyStatusColor,
|
getBusinessPartyStatusColor,
|
||||||
getBusinessPartyStatusName,
|
getBusinessPartyStatusName,
|
||||||
getCustomerSegmentName,
|
getCustomerSegmentName,
|
||||||
} from "../../../utils/erp";
|
} from '../../../utils/erp'
|
||||||
|
|
||||||
const CustomerCards: React.FC = () => {
|
const CustomerCards: React.FC = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [filterStatus, setFilterStatus] = useState("all");
|
const [filterStatus, setFilterStatus] = useState('all')
|
||||||
const [filterSegment, setFilterSegment] = useState("all");
|
const [filterSegment, setFilterSegment] = useState('all')
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: customers,
|
data: customers,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["customers", searchTerm, filterStatus, filterSegment],
|
queryKey: ['customers', searchTerm, filterStatus, filterSegment],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
const mockCustomers = mockBusinessParties.filter(
|
const mockCustomers = mockBusinessParties.filter(
|
||||||
(customer) => customer.partyType === PartyType.Customer
|
(customer) => customer.partyType === PartyType.Customer,
|
||||||
);
|
)
|
||||||
|
|
||||||
return mockCustomers.filter((customer) => {
|
return mockCustomers.filter((customer) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
customer.code?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
customer.code?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
customer.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
customer.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
customer.primaryContact?.fullName
|
customer.primaryContact?.fullName?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
?.toLowerCase()
|
const matchesStatus = filterStatus === 'all' || customer.status === filterStatus
|
||||||
.includes(searchTerm.toLowerCase());
|
const matchesSegment = filterSegment === 'all' || customer.customerSegment === filterSegment
|
||||||
const matchesStatus =
|
return matchesSearch && matchesStatus && matchesSegment
|
||||||
filterStatus === "all" || customer.status === filterStatus;
|
})
|
||||||
const matchesSegment =
|
|
||||||
filterSegment === "all" || customer.customerSegment === filterSegment;
|
|
||||||
return matchesSearch && matchesStatus && matchesSegment;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
||||||
{[...Array(12)].map((_, index) => (
|
{[...Array(12)].map((_, index) => (
|
||||||
<div
|
<div key={index} className="bg-white rounded-xl shadow-sm border animate-pulse">
|
||||||
key={index}
|
|
||||||
className="bg-white rounded-xl shadow-sm border animate-pulse"
|
|
||||||
>
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
|
|
@ -105,7 +98,7 @@ const CustomerCards: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -113,12 +106,10 @@ const CustomerCards: React.FC = () => {
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" />
|
<FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" />
|
||||||
<span className="text-red-800">
|
<span className="text-red-800">Müşteriler yüklenirken hata oluştu.</span>
|
||||||
Müşteriler yüklenirken hata oluştu.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -143,10 +134,10 @@ const CustomerCards: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors",
|
'flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors',
|
||||||
showFilters
|
showFilters
|
||||||
? "border-blue-500 bg-blue-50 text-blue-700"
|
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||||
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FaFilter size={14} className="mr-2" />
|
<FaFilter size={14} className="mr-2" />
|
||||||
|
|
@ -156,7 +147,7 @@ const CustomerCards: React.FC = () => {
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => alert("Dışa aktarma özelliği yakında eklenecek")}
|
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
|
||||||
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<FaDownload size={14} className="mr-2" />
|
<FaDownload size={14} className="mr-2" />
|
||||||
|
|
@ -178,18 +169,14 @@ const CustomerCards: React.FC = () => {
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
|
||||||
Durum
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={filterStatus}
|
value={filterStatus}
|
||||||
onChange={(e) => setFilterStatus(e.target.value)}
|
onChange={(e) => setFilterStatus(e.target.value)}
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value="all">Tümü</option>
|
<option value="all">Tümü</option>
|
||||||
<option value={BusinessPartyStatusEnum.Prospect}>
|
<option value={BusinessPartyStatusEnum.Prospect}>Potansiyel</option>
|
||||||
Potansiyel
|
|
||||||
</option>
|
|
||||||
<option value={BusinessPartyStatusEnum.Active}>Aktif</option>
|
<option value={BusinessPartyStatusEnum.Active}>Aktif</option>
|
||||||
<option value={BusinessPartyStatusEnum.Inactive}>Pasif</option>
|
<option value={BusinessPartyStatusEnum.Inactive}>Pasif</option>
|
||||||
<option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option>
|
<option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option>
|
||||||
|
|
@ -197,9 +184,7 @@ const CustomerCards: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Segment</label>
|
||||||
Segment
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={filterSegment}
|
value={filterSegment}
|
||||||
onChange={(e) => setFilterSegment(e.target.value)}
|
onChange={(e) => setFilterSegment(e.target.value)}
|
||||||
|
|
@ -216,9 +201,9 @@ const CustomerCards: React.FC = () => {
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFilterStatus("all");
|
setFilterStatus('all')
|
||||||
setFilterSegment("all");
|
setFilterSegment('all')
|
||||||
setSearchTerm("");
|
setSearchTerm('')
|
||||||
}}
|
}}
|
||||||
className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
|
|
@ -240,11 +225,7 @@ const CustomerCards: React.FC = () => {
|
||||||
|
|
||||||
<Widget
|
<Widget
|
||||||
title="Aktif Müşteri"
|
title="Aktif Müşteri"
|
||||||
value={
|
value={customers?.filter((c) => c.status === BusinessPartyStatusEnum.Active).length || 0}
|
||||||
customers?.filter(
|
|
||||||
(c) => c.status === BusinessPartyStatusEnum.Active
|
|
||||||
).length || 0
|
|
||||||
}
|
|
||||||
color="green"
|
color="green"
|
||||||
icon="FaUserCheck"
|
icon="FaUserCheck"
|
||||||
/>
|
/>
|
||||||
|
|
@ -252,9 +233,7 @@ const CustomerCards: React.FC = () => {
|
||||||
<Widget
|
<Widget
|
||||||
title="Toplam Ciro"
|
title="Toplam Ciro"
|
||||||
value={`₺${
|
value={`₺${
|
||||||
customers
|
customers?.reduce((acc, c) => acc + (c.totalRevenue ?? 0), 0).toLocaleString() || 0
|
||||||
?.reduce((acc, c) => acc + (c.totalRevenue ?? 0), 0)
|
|
||||||
.toLocaleString() || 0
|
|
||||||
}`}
|
}`}
|
||||||
color="purple"
|
color="purple"
|
||||||
icon="FaDollarSign"
|
icon="FaDollarSign"
|
||||||
|
|
@ -265,10 +244,8 @@ const CustomerCards: React.FC = () => {
|
||||||
value={`₺${
|
value={`₺${
|
||||||
customers?.length
|
customers?.length
|
||||||
? Math.round(
|
? Math.round(
|
||||||
customers.reduce(
|
customers.reduce((acc, c) => acc + (c.averageOrderValue ?? 0), 0) /
|
||||||
(acc, c) => acc + (c.averageOrderValue ?? 0),
|
customers.length,
|
||||||
0
|
|
||||||
) / customers.length
|
|
||||||
).toLocaleString()
|
).toLocaleString()
|
||||||
: 0
|
: 0
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -288,14 +265,14 @@ const CustomerCards: React.FC = () => {
|
||||||
{/* Status Indicator */}
|
{/* Status Indicator */}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"absolute top-0 left-0 w-full h-1",
|
'absolute top-0 left-0 w-full h-1',
|
||||||
customer.status === BusinessPartyStatusEnum.Active
|
customer.status === BusinessPartyStatusEnum.Active
|
||||||
? "bg-green-500"
|
? 'bg-green-500'
|
||||||
: customer.status === BusinessPartyStatusEnum.Prospect
|
: customer.status === BusinessPartyStatusEnum.Prospect
|
||||||
? "bg-blue-500"
|
? 'bg-blue-500'
|
||||||
: customer.status === BusinessPartyStatusEnum.Blocked
|
: customer.status === BusinessPartyStatusEnum.Blocked
|
||||||
? "bg-red-500"
|
? 'bg-red-500'
|
||||||
: "bg-gray-400"
|
: 'bg-gray-400',
|
||||||
)}
|
)}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
|
@ -317,8 +294,8 @@ const CustomerCards: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border",
|
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border',
|
||||||
getBusinessPartyStatusColor(customer.status!)
|
getBusinessPartyStatusColor(customer.status!),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getBusinessPartyStatusName(customer.status!)}
|
{getBusinessPartyStatusName(customer.status!)}
|
||||||
|
|
@ -331,9 +308,7 @@ const CustomerCards: React.FC = () => {
|
||||||
{customer.name}
|
{customer.name}
|
||||||
</h2>
|
</h2>
|
||||||
{customer.industry && (
|
{customer.industry && (
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">{customer.industry}</p>
|
||||||
{customer.industry}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -341,15 +316,11 @@ const CustomerCards: React.FC = () => {
|
||||||
<div className="space-y-1.5 mb-3 text-xs">
|
<div className="space-y-1.5 mb-3 text-xs">
|
||||||
<div className="flex items-center text-sm text-gray-600">
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
<FaUser className="w-4 h-4 mr-2 text-gray-400" />
|
<FaUser className="w-4 h-4 mr-2 text-gray-400" />
|
||||||
<span className="truncate">
|
<span className="truncate">{customer.primaryContact?.fullName}</span>
|
||||||
{customer.primaryContact?.fullName}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-sm text-gray-600">
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
<FaEnvelope className="w-4 h-4 mr-2 text-gray-400" />
|
<FaEnvelope className="w-4 h-4 mr-2 text-gray-400" />
|
||||||
<span className="truncate">
|
<span className="truncate">{customer.primaryContact?.email}</span>
|
||||||
{customer.primaryContact?.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{customer.primaryContact?.phone && (
|
{customer.primaryContact?.phone && (
|
||||||
<div className="flex items-center text-sm text-gray-600">
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
|
|
@ -384,17 +355,13 @@ const CustomerCards: React.FC = () => {
|
||||||
<div className="text-base font-bold text-green-700">
|
<div className="text-base font-bold text-green-700">
|
||||||
₺{(customer.totalRevenue! / 1000).toLocaleString()}K
|
₺{(customer.totalRevenue! / 1000).toLocaleString()}K
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-green-600 font-medium">
|
<div className="text-xs text-green-600 font-medium">Toplam Ciro</div>
|
||||||
Toplam Ciro
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-2 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-lg border border-blue-100">
|
<div className="text-center p-2 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-lg border border-blue-100">
|
||||||
<div className="text-base font-bold text-blue-700">
|
<div className="text-base font-bold text-blue-700">
|
||||||
₺{(customer.averageOrderValue! / 1000).toLocaleString()}K
|
₺{(customer.averageOrderValue! / 1000).toLocaleString()}K
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-blue-600 font-medium">
|
<div className="text-xs text-blue-600 font-medium">Ort. Sipariş</div>
|
||||||
Ort. Sipariş
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -402,16 +369,12 @@ const CustomerCards: React.FC = () => {
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500 mb-3">
|
<div className="flex items-center justify-between text-xs text-gray-500 mb-3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FaStar className="w-4 h-4 text-yellow-500 mr-1" />
|
<FaStar className="w-4 h-4 text-yellow-500 mr-1" />
|
||||||
<span>
|
<span>LTV: ₺{(customer.lifetimeValue! / 1000000).toFixed(1)}M</span>
|
||||||
LTV: ₺{(customer.lifetimeValue! / 1000000).toFixed(1)}M
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{customer.lastOrderDate && (
|
{customer.lastOrderDate && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FaCalendar className="w-4 h-4 mr-1" />
|
<FaCalendar className="w-4 h-4 mr-1" />
|
||||||
<span>
|
<span>{dayjs(customer.lastOrderDate).format('DD.MM.YY')}</span>
|
||||||
{dayjs(customer.lastOrderDate).format("DD.MM.YY")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -419,9 +382,7 @@ const CustomerCards: React.FC = () => {
|
||||||
{/* Credit Limit Progress */}
|
{/* Credit Limit Progress */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<span className="text-gray-600 font-medium">
|
<span className="text-gray-600 font-medium">Kredi Kullanımı</span>
|
||||||
Kredi Kullanımı
|
|
||||||
</span>
|
|
||||||
<span className="font-semibold text-gray-900">
|
<span className="font-semibold text-gray-900">
|
||||||
₺{customer.creditLimit.toLocaleString()}
|
₺{customer.creditLimit.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -429,24 +390,17 @@ const CustomerCards: React.FC = () => {
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
|
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"h-2.5 rounded-full transition-all duration-500",
|
'h-2.5 rounded-full transition-all duration-500',
|
||||||
Math.min(
|
Math.min((customer.totalRevenue! / customer.creditLimit) * 100, 100) > 80
|
||||||
(customer.totalRevenue! / customer.creditLimit) * 100,
|
? 'bg-gradient-to-r from-red-500 to-red-600'
|
||||||
100
|
: Math.min((customer.totalRevenue! / customer.creditLimit) * 100, 100) > 60
|
||||||
) > 80
|
? 'bg-gradient-to-r from-yellow-400 to-yellow-500'
|
||||||
? "bg-gradient-to-r from-red-500 to-red-600"
|
: 'bg-gradient-to-r from-green-500 to-green-600',
|
||||||
: Math.min(
|
|
||||||
(customer.totalRevenue! / customer.creditLimit) *
|
|
||||||
100,
|
|
||||||
100
|
|
||||||
) > 60
|
|
||||||
? "bg-gradient-to-r from-yellow-400 to-yellow-500"
|
|
||||||
: "bg-gradient-to-r from-green-500 to-green-600"
|
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.min(
|
width: `${Math.min(
|
||||||
(customer.totalRevenue! / customer.creditLimit) * 100,
|
(customer.totalRevenue! / customer.creditLimit) * 100,
|
||||||
100
|
100,
|
||||||
)}%`,
|
)}%`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
|
@ -454,10 +408,7 @@ const CustomerCards: React.FC = () => {
|
||||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||||
<span>₺0</span>
|
<span>₺0</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{Math.round(
|
{Math.round((customer.totalRevenue! / customer.creditLimit) * 100)}% kullanıldı
|
||||||
(customer.totalRevenue! / customer.creditLimit) * 100
|
|
||||||
)}
|
|
||||||
% kullanıldı
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -485,17 +436,15 @@ const CustomerCards: React.FC = () => {
|
||||||
<div className="flex items-center text-xs">
|
<div className="flex items-center text-xs">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center px-2 py-1 rounded-full text-xs font-medium",
|
'flex items-center px-2 py-1 rounded-full text-xs font-medium',
|
||||||
customer.status === BusinessPartyStatusEnum.Active
|
customer.status === BusinessPartyStatusEnum.Active
|
||||||
? "bg-green-100 text-green-700"
|
? 'bg-green-100 text-green-700'
|
||||||
: "bg-gray-100 text-gray-600"
|
: 'bg-gray-100 text-gray-600',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FaChartLine className="w-3 h-3 mr-1" />
|
<FaChartLine className="w-3 h-3 mr-1" />
|
||||||
<span>
|
<span>
|
||||||
{customer.status === BusinessPartyStatusEnum.Active
|
{customer.status === BusinessPartyStatusEnum.Active ? 'Aktif' : 'Durgun'}
|
||||||
? "Aktif"
|
|
||||||
: "Durgun"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -509,12 +458,9 @@ const CustomerCards: React.FC = () => {
|
||||||
{(!customers || customers.length === 0) && (
|
{(!customers || customers.length === 0) && (
|
||||||
<div className="text-center py-10">
|
<div className="text-center py-10">
|
||||||
<FaUserCheck className="mx-auto h-12 w-12 text-gray-400 mb-3" />
|
<FaUserCheck className="mx-auto h-12 w-12 text-gray-400 mb-3" />
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Müşteri bulunamadı</h3>
|
||||||
Müşteri bulunamadı
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-500 mb-4">
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
Yeni müşteri ekleyerek başlayın veya arama kriterlerinizi
|
Yeni müşteri ekleyerek başlayın veya arama kriterlerinizi değiştirin.
|
||||||
değiştirin.
|
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/admin/crm/customers/new"
|
to="/admin/crm/customers/new"
|
||||||
|
|
@ -526,7 +472,7 @@ const CustomerCards: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CustomerCards;
|
export default CustomerCards
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
FaArrowLeft,
|
FaArrowLeft,
|
||||||
FaSave,
|
FaSave,
|
||||||
|
|
@ -11,117 +11,105 @@ import {
|
||||||
FaIdCard,
|
FaIdCard,
|
||||||
FaCreditCard,
|
FaCreditCard,
|
||||||
FaExclamationTriangle,
|
FaExclamationTriangle,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames'
|
||||||
import { CustomerSegmentEnum, CustomerTypeEnum } from "../../../types/crm";
|
import { CustomerSegmentEnum, CustomerTypeEnum } from '../../../types/crm'
|
||||||
import {
|
import { mockBusinessParties, mockBusinessPartyNew } from '../../../mocks/mockBusinessParties'
|
||||||
mockBusinessParties,
|
import { BusinessParty, BusinessPartyStatusEnum, PaymentTerms } from '../../../types/common'
|
||||||
mockBusinessPartyNew,
|
import { Container } from '@/components/shared'
|
||||||
} from "../../../mocks/mockBusinessParties";
|
|
||||||
import {
|
|
||||||
BusinessParty,
|
|
||||||
BusinessPartyStatusEnum,
|
|
||||||
PaymentTerms,
|
|
||||||
} from "../../../types/common";
|
|
||||||
|
|
||||||
const CustomerEdit: React.FC = () => {
|
const CustomerEdit: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient()
|
||||||
const [activeTab, setActiveTab] = useState("basic");
|
const [activeTab, setActiveTab] = useState('basic')
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: customer,
|
data: customer,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["customer", id],
|
queryKey: ['customer', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||||
const found = mockBusinessParties.find((c) => c.id === id);
|
const found = mockBusinessParties.find((c) => c.id === id)
|
||||||
if (!found) throw new Error("Müşteri bulunamadı");
|
if (!found) throw new Error('Müşteri bulunamadı')
|
||||||
return found;
|
return found
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew);
|
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew)
|
||||||
|
|
||||||
// Initialize form data when customer is loaded
|
// Initialize form data when customer is loaded
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (customer) {
|
if (customer) {
|
||||||
setFormData(
|
setFormData(mockBusinessParties.find((c) => c.id === id) || mockBusinessPartyNew)
|
||||||
mockBusinessParties.find((c) => c.id === id) || mockBusinessPartyNew
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [customer, id]);
|
}, [customer, id])
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: async (data: BusinessParty) => {
|
mutationFn: async (data: BusinessParty) => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
return { ...customer, ...data };
|
return { ...customer, ...data }
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["customers"] });
|
queryClient.invalidateQueries({ queryKey: ['customers'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ["customer", id] });
|
queryClient.invalidateQueries({ queryKey: ['customer', id] })
|
||||||
setIsDirty(false);
|
setIsDirty(false)
|
||||||
navigate(`/admin/crm/customers/${id}`);
|
navigate(`/admin/crm/customers/${id}`)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string | number) => {
|
const handleInputChange = (field: string, value: string | number) => {
|
||||||
const keys = field.split(".");
|
const keys = field.split('.')
|
||||||
if (keys.length === 2) {
|
if (keys.length === 2) {
|
||||||
const [section, subField] = keys;
|
const [section, subField] = keys
|
||||||
setFormData((prev: any) => {
|
setFormData((prev: any) => {
|
||||||
if (section === "primaryContact") {
|
if (section === 'primaryContact') {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
primaryContact: {
|
primaryContact: {
|
||||||
...prev.primaryContact,
|
...prev.primaryContact,
|
||||||
[subField]: value,
|
[subField]: value,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
} else if (section === "address") {
|
} else if (section === 'address') {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
address: {
|
address: {
|
||||||
...prev.address,
|
...prev.address,
|
||||||
[subField]: value,
|
[subField]: value,
|
||||||
},
|
},
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return prev;
|
}
|
||||||
});
|
return prev
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}));
|
}))
|
||||||
|
}
|
||||||
|
setIsDirty(true)
|
||||||
}
|
}
|
||||||
setIsDirty(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
updateMutation.mutate(formData);
|
updateMutation.mutate(formData)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (isDirty) {
|
if (isDirty) {
|
||||||
if (
|
if (window.confirm('Değişiklikleriniz kaydedilmedi. Çıkmak istediğinizden emin misiniz?')) {
|
||||||
window.confirm(
|
navigate(`/admin/crm/customers/${id}`)
|
||||||
"Değişiklikleriniz kaydedilmedi. Çıkmak istediğinizden emin misiniz?"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
navigate(`/admin/crm/customers/${id}`);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navigate(`/admin/crm/customers/${id}`);
|
navigate(`/admin/crm/customers/${id}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -139,7 +127,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !customer) {
|
if (error || !customer) {
|
||||||
|
|
@ -150,17 +138,13 @@ const CustomerEdit: React.FC = () => {
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FaExclamationTriangle className="h-8 w-8 text-red-600 mr-3" />
|
<FaExclamationTriangle className="h-8 w-8 text-red-600 mr-3" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-red-800">
|
<h3 className="text-lg font-medium text-red-800">Müşteri Bulunamadı</h3>
|
||||||
Müşteri Bulunamadı
|
<p className="text-red-600">Düzenlemek istediğiniz müşteri mevcut değil.</p>
|
||||||
</h3>
|
|
||||||
<p className="text-red-600">
|
|
||||||
Düzenlemek istediğiniz müşteri mevcut değil.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/customers")}
|
onClick={() => navigate('/admin/crm/customers')}
|
||||||
className="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
className="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-4 h-4 mr-2" />
|
<FaArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
|
@ -170,17 +154,18 @@ const CustomerEdit: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: "basic", label: "Temel Bilgiler", icon: FaBuilding },
|
{ id: 'basic', label: 'Temel Bilgiler', icon: FaBuilding },
|
||||||
{ id: "contact", label: "İletişim", icon: FaUser },
|
{ id: 'contact', label: 'İletişim', icon: FaUser },
|
||||||
{ id: "business", label: "İş Bilgileri", icon: FaIdCard },
|
{ id: 'business', label: 'İş Bilgileri', icon: FaIdCard },
|
||||||
{ id: "financial", label: "Finansal", icon: FaCreditCard },
|
{ id: 'financial', label: 'Finansal', icon: FaCreditCard },
|
||||||
];
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -192,10 +177,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
Müşteriler
|
Müşteriler
|
||||||
</Link>
|
</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<Link
|
<Link to={`/admin/crm/customers/${id}`} className="hover:text-blue-600">
|
||||||
to={`/admin/crm/customers/${id}`}
|
|
||||||
className="hover:text-blue-600"
|
|
||||||
>
|
|
||||||
{customer.name}
|
{customer.name}
|
||||||
</Link>
|
</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
|
|
@ -219,9 +201,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-gray-900">
|
<h1 className="text-xl font-bold text-gray-900">Müşteri Düzenle</h1>
|
||||||
Müşteri Düzenle
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-gray-600">{customer.name}</p>
|
<p className="text-sm text-gray-600">{customer.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -240,10 +220,10 @@ const CustomerEdit: React.FC = () => {
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || updateMutation.isPending}
|
disabled={!isDirty || updateMutation.isPending}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"inline-flex items-center px-3 py-1.5 text-sm rounded-lg transition-colors",
|
'inline-flex items-center px-3 py-1.5 text-sm rounded-lg transition-colors',
|
||||||
isDirty && !updateMutation.isPending
|
isDirty && !updateMutation.isPending
|
||||||
? "bg-blue-600 text-white hover:bg-blue-700"
|
? 'bg-blue-600 text-white hover:bg-blue-700'
|
||||||
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
: 'bg-gray-300 text-gray-500 cursor-not-allowed',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{updateMutation.isPending ? (
|
{updateMutation.isPending ? (
|
||||||
|
|
@ -273,10 +253,10 @@ const CustomerEdit: React.FC = () => {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center space-x-2 py-3 px-1 border-b-2 font-medium text-sm transition-colors",
|
'flex items-center space-x-2 py-3 px-1 border-b-2 font-medium text-sm transition-colors',
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? "border-blue-500 text-blue-600"
|
? 'border-blue-500 text-blue-600'
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<tab.icon className="w-4 h-4" />
|
<tab.icon className="w-4 h-4" />
|
||||||
|
|
@ -289,7 +269,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
|
|
||||||
{/* Form Content */}
|
{/* Form Content */}
|
||||||
<div className="mx-auto py-4">
|
<div className="mx-auto py-4">
|
||||||
{activeTab === "basic" && (
|
{activeTab === 'basic' && (
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
||||||
<FaBuilding className="w-5 h-5 mr-2" />
|
<FaBuilding className="w-5 h-5 mr-2" />
|
||||||
|
|
@ -304,7 +284,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.code}
|
value={formData.code}
|
||||||
onChange={(e) => handleInputChange("code", e.target.value)}
|
onChange={(e) => handleInputChange('code', e.target.value)}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -317,7 +297,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -329,47 +309,33 @@ const CustomerEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.customerType}
|
value={formData.customerType}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('customerType', e.target.value)}
|
||||||
handleInputChange("customerType", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value={CustomerTypeEnum.Company}>Şirket</option>
|
<option value={CustomerTypeEnum.Company}>Şirket</option>
|
||||||
<option value={CustomerTypeEnum.Individual}>
|
<option value={CustomerTypeEnum.Individual}>Bireysel</option>
|
||||||
Bireysel
|
|
||||||
</option>
|
|
||||||
<option value={CustomerTypeEnum.Government}>Kamu</option>
|
<option value={CustomerTypeEnum.Government}>Kamu</option>
|
||||||
<option value={CustomerTypeEnum.NonProfit}>
|
<option value={CustomerTypeEnum.NonProfit}>Kar Amacı Gütmeyen</option>
|
||||||
Kar Amacı Gütmeyen
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Sektör</label>
|
||||||
Sektör
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.industry}
|
value={formData.industry}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('industry', e.target.value)}
|
||||||
handleInputChange("industry", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="Teknoloji, İmalat, Hizmet vb."
|
placeholder="Teknoloji, İmalat, Hizmet vb."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Website</label>
|
||||||
Website
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.website}
|
value={formData.website}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('website', e.target.value)}
|
||||||
handleInputChange("website", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="https://www.ornek.com"
|
placeholder="https://www.ornek.com"
|
||||||
/>
|
/>
|
||||||
|
|
@ -381,23 +347,13 @@ const CustomerEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.status}
|
value={formData.status}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('status', e.target.value)}
|
||||||
handleInputChange("status", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value={BusinessPartyStatusEnum.Prospect}>
|
<option value={BusinessPartyStatusEnum.Prospect}>Potansiyel</option>
|
||||||
Potansiyel
|
<option value={BusinessPartyStatusEnum.Active}>Aktif</option>
|
||||||
</option>
|
<option value={BusinessPartyStatusEnum.Inactive}>Pasif</option>
|
||||||
<option value={BusinessPartyStatusEnum.Active}>
|
<option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option>
|
||||||
Aktif
|
|
||||||
</option>
|
|
||||||
<option value={BusinessPartyStatusEnum.Inactive}>
|
|
||||||
Pasif
|
|
||||||
</option>
|
|
||||||
<option value={BusinessPartyStatusEnum.Blocked}>
|
|
||||||
Blokeli
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -407,14 +363,10 @@ const CustomerEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.customerSegment}
|
value={formData.customerSegment}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('customerSegment', e.target.value)}
|
||||||
handleInputChange("customerSegment", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value={CustomerSegmentEnum.Enterprise}>
|
<option value={CustomerSegmentEnum.Enterprise}>Kurumsal</option>
|
||||||
Kurumsal
|
|
||||||
</option>
|
|
||||||
<option value={CustomerSegmentEnum.SMB}>KOBİ</option>
|
<option value={CustomerSegmentEnum.SMB}>KOBİ</option>
|
||||||
<option value={CustomerSegmentEnum.Startup}>Girişim</option>
|
<option value={CustomerSegmentEnum.Startup}>Girişim</option>
|
||||||
<option value={CustomerSegmentEnum.Government}>Kamu</option>
|
<option value={CustomerSegmentEnum.Government}>Kamu</option>
|
||||||
|
|
@ -424,7 +376,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "contact" && (
|
{activeTab === 'contact' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Primary Contact */}
|
{/* Primary Contact */}
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
|
@ -435,17 +387,12 @@ const CustomerEdit: React.FC = () => {
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Ad *</label>
|
||||||
Ad *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.primaryContact?.firstName}
|
value={formData.primaryContact?.firstName}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('primaryContact.firstName', e.target.value)
|
||||||
"primaryContact.firstName",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
|
|
@ -460,10 +407,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.primaryContact?.lastName}
|
value={formData.primaryContact?.lastName}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('primaryContact.lastName', e.target.value)
|
||||||
"primaryContact.lastName",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
|
|
@ -471,18 +415,11 @@ const CustomerEdit: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Ünvan</label>
|
||||||
Ünvan
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.primaryContact?.title}
|
value={formData.primaryContact?.title}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('primaryContact.title', e.target.value)}
|
||||||
handleInputChange(
|
|
||||||
"primaryContact.title",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="Genel Müdür, Satış Direktörü vb."
|
placeholder="Genel Müdür, Satış Direktörü vb."
|
||||||
/>
|
/>
|
||||||
|
|
@ -496,10 +433,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.primaryContact?.department}
|
value={formData.primaryContact?.department}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('primaryContact.department', e.target.value)
|
||||||
"primaryContact.department",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="Satış, Pazarlama, İnsan Kaynakları vb."
|
placeholder="Satış, Pazarlama, İnsan Kaynakları vb."
|
||||||
|
|
@ -513,12 +447,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={formData.primaryContact?.email}
|
value={formData.primaryContact?.email}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('primaryContact.email', e.target.value)}
|
||||||
handleInputChange(
|
|
||||||
"primaryContact.email",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -531,12 +460,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={formData.primaryContact?.phone}
|
value={formData.primaryContact?.phone}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('primaryContact.phone', e.target.value)}
|
||||||
handleInputChange(
|
|
||||||
"primaryContact.phone",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="+90 (212) 555 0123"
|
placeholder="+90 (212) 555 0123"
|
||||||
/>
|
/>
|
||||||
|
|
@ -549,12 +473,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={formData.primaryContact?.mobile}
|
value={formData.primaryContact?.mobile}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('primaryContact.mobile', e.target.value)}
|
||||||
handleInputChange(
|
|
||||||
"primaryContact.mobile",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="+90 (555) 123 4567"
|
placeholder="+90 (555) 123 4567"
|
||||||
/>
|
/>
|
||||||
|
|
@ -576,9 +495,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.address?.street}
|
value={formData.address?.street}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('address.street', e.target.value)}
|
||||||
handleInputChange("address.street", e.target.value)
|
|
||||||
}
|
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="Sokak, cadde, mahalle, bina no vb."
|
placeholder="Sokak, cadde, mahalle, bina no vb."
|
||||||
|
|
@ -594,9 +511,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.address?.city}
|
value={formData.address?.city}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('address.city', e.target.value)}
|
||||||
handleInputChange("address.city", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -609,9 +524,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.address?.state}
|
value={formData.address?.state}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('address.state', e.target.value)}
|
||||||
handleInputChange("address.state", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -623,12 +536,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.address?.postalCode}
|
value={formData.address?.postalCode}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('address.postalCode', e.target.value)}
|
||||||
handleInputChange(
|
|
||||||
"address.postalCode",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -640,9 +548,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.address?.country}
|
value={formData.address?.country}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('address.country', e.target.value)}
|
||||||
handleInputChange("address.country", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -653,7 +559,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "business" && (
|
{activeTab === 'business' && (
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
||||||
<FaIdCard className="w-5 h-5 mr-2" />
|
<FaIdCard className="w-5 h-5 mr-2" />
|
||||||
|
|
@ -668,9 +574,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.taxNumber}
|
value={formData.taxNumber}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('taxNumber', e.target.value)}
|
||||||
handleInputChange("taxNumber", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="1234567890"
|
placeholder="1234567890"
|
||||||
/>
|
/>
|
||||||
|
|
@ -683,9 +587,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.registrationNumber}
|
value={formData.registrationNumber}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('registrationNumber', e.target.value)}
|
||||||
handleInputChange("registrationNumber", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="123456789"
|
placeholder="123456789"
|
||||||
/>
|
/>
|
||||||
|
|
@ -698,9 +600,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.assignedSalesRep}
|
value={formData.assignedSalesRep}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('assignedSalesRep', e.target.value)}
|
||||||
handleInputChange("assignedSalesRep", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="Satış temsilcisi adı"
|
placeholder="Satış temsilcisi adı"
|
||||||
/>
|
/>
|
||||||
|
|
@ -709,7 +609,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "financial" && (
|
{activeTab === 'financial' && (
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
||||||
<FaCreditCard className="w-5 h-5 mr-2" />
|
<FaCreditCard className="w-5 h-5 mr-2" />
|
||||||
|
|
@ -724,9 +624,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.creditLimit}
|
value={formData.creditLimit}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('creditLimit', Number(e.target.value))}
|
||||||
handleInputChange("creditLimit", Number(e.target.value))
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
min="0"
|
min="0"
|
||||||
step="1000"
|
step="1000"
|
||||||
|
|
@ -739,9 +637,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.currency}
|
value={formData.currency}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('currency', e.target.value)}
|
||||||
handleInputChange("currency", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value="TRY">Türk Lirası (TRY)</option>
|
<option value="TRY">Türk Lirası (TRY)</option>
|
||||||
|
|
@ -757,9 +653,7 @@ const CustomerEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.paymentTerms}
|
value={formData.paymentTerms}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('paymentTerms', e.target.value)}
|
||||||
handleInputChange("paymentTerms", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value={PaymentTerms.Prepaid}>Peşin</option>
|
<option value={PaymentTerms.Prepaid}>Peşin</option>
|
||||||
|
|
@ -775,7 +669,8 @@ const CustomerEdit: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default CustomerEdit;
|
export default CustomerEdit
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
FaSave,
|
FaSave,
|
||||||
FaTimes,
|
FaTimes,
|
||||||
|
|
@ -8,142 +8,130 @@ import {
|
||||||
FaMapMarkerAlt,
|
FaMapMarkerAlt,
|
||||||
FaCreditCard,
|
FaCreditCard,
|
||||||
FaEnvelope,
|
FaEnvelope,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import LoadingSpinner from "../../../components/common/LoadingSpinner";
|
import LoadingSpinner from '../../../components/common/LoadingSpinner'
|
||||||
import {
|
import { mockBusinessParties, mockBusinessPartyNew } from '../../../mocks/mockBusinessParties'
|
||||||
mockBusinessParties,
|
import { BusinessParty } from '../../../types/common'
|
||||||
mockBusinessPartyNew,
|
import { Container } from '@/components/shared'
|
||||||
} from "../../../mocks/mockBusinessParties";
|
|
||||||
import { BusinessParty } from "../../../types/common";
|
|
||||||
|
|
||||||
interface ValidationErrors {
|
interface ValidationErrors {
|
||||||
[key: string]: string;
|
[key: string]: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomerForm: React.FC = () => {
|
const CustomerForm: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>()
|
||||||
const isEdit = Boolean(id);
|
const isEdit = Boolean(id)
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false)
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false)
|
||||||
const [errors, setErrors] = useState<ValidationErrors>({});
|
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||||
|
|
||||||
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew);
|
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew)
|
||||||
|
|
||||||
const loadFormData = useCallback(async () => {
|
const loadFormData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
if (isEdit && id) {
|
if (isEdit && id) {
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
// Mock customer data
|
// Mock customer data
|
||||||
const mockCustomer = mockBusinessParties.find(
|
const mockCustomer = mockBusinessParties.find((cust) => cust.id === id)!
|
||||||
(cust) => cust.id === id
|
setFormData(mockCustomer)
|
||||||
)!;
|
|
||||||
setFormData(mockCustomer);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading form data:", error);
|
console.error('Error loading form data:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [isEdit, id]);
|
}, [isEdit, id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFormData();
|
loadFormData()
|
||||||
}, [loadFormData]);
|
}, [loadFormData])
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: ValidationErrors = {};
|
const newErrors: ValidationErrors = {}
|
||||||
|
|
||||||
if (!formData.code.trim()) {
|
if (!formData.code.trim()) {
|
||||||
newErrors.code = "Müşteri kodu zorunludur";
|
newErrors.code = 'Müşteri kodu zorunludur'
|
||||||
}
|
}
|
||||||
if (!formData.name.trim()) {
|
if (!formData.name.trim()) {
|
||||||
newErrors.name = "Şirket adı zorunludur";
|
newErrors.name = 'Şirket adı zorunludur'
|
||||||
}
|
}
|
||||||
if (formData.creditLimit < 0) {
|
if (formData.creditLimit < 0) {
|
||||||
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz";
|
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz"
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors)
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (field: keyof BusinessParty, value: string | number | boolean) => {
|
||||||
field: keyof BusinessParty,
|
|
||||||
value: string | number | boolean
|
|
||||||
) => {
|
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
// Clear error when user starts typing
|
// Clear error when user starts typing
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors((prev) => ({
|
setErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: "",
|
[field]: '',
|
||||||
}));
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
console.log("Customer data:", {
|
console.log('Customer data:', {
|
||||||
...formData,
|
...formData,
|
||||||
id: isEdit ? id : undefined,
|
id: isEdit ? id : undefined,
|
||||||
});
|
})
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
alert(
|
alert(isEdit ? 'Müşteri başarıyla güncellendi!' : 'Müşteri başarıyla oluşturuldu!')
|
||||||
isEdit
|
|
||||||
? "Müşteri başarıyla güncellendi!"
|
|
||||||
: "Müşteri başarıyla oluşturuldu!"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Navigate back to list
|
// Navigate back to list
|
||||||
navigate("/admin/crm");
|
navigate('/admin/crm')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving customer:", error);
|
console.error('Error saving customer:', error)
|
||||||
alert("Bir hata oluştu. Lütfen tekrar deneyin.");
|
alert('Bir hata oluştu. Lütfen tekrar deneyin.')
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
navigate("/admin/crm");
|
navigate('/admin/crm')
|
||||||
};
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-4 pt-2">
|
<div className="space-y-4 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
{isEdit ? "Müşteri Düzenle" : "Yeni Müşteri"}
|
{isEdit ? 'Müşteri Düzenle' : 'Yeni Müşteri'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{isEdit
|
{isEdit ? 'Mevcut müşteri bilgilerini güncelleyin' : 'Yeni müşteri bilgilerini girin'}
|
||||||
? "Mevcut müşteri bilgilerini güncelleyin"
|
|
||||||
: "Yeni müşteri bilgilerini girin"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -168,18 +156,16 @@ const CustomerForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.code}
|
value={formData.code}
|
||||||
onChange={(e) => handleInputChange("code", e.target.value)}
|
onChange={(e) => handleInputChange('code', e.target.value)}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.code
|
errors.code
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Örn: MST001"
|
placeholder="Örn: MST001"
|
||||||
/>
|
/>
|
||||||
{errors.customerCode && (
|
{errors.customerCode && (
|
||||||
<p className="mt-1 text-sm text-red-600">
|
<p className="mt-1 text-sm text-red-600">{errors.customerCode}</p>
|
||||||
{errors.customerCode}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -190,10 +176,7 @@ const CustomerForm: React.FC = () => {
|
||||||
<select
|
<select
|
||||||
value={formData.customerType}
|
value={formData.customerType}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('customerType', e.target.value as 'INDIVIDUAL' | 'COMPANY')
|
||||||
"customerType",
|
|
||||||
e.target.value as "INDIVIDUAL" | "COMPANY"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
|
|
@ -211,30 +194,24 @@ const CustomerForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.name
|
errors.name
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Şirket adını girin"
|
placeholder="Şirket adını girin"
|
||||||
/>
|
/>
|
||||||
{errors.companyName && (
|
{errors.companyName && (
|
||||||
<p className="mt-1 text-sm text-red-600">
|
<p className="mt-1 text-sm text-red-600">{errors.companyName}</p>
|
||||||
{errors.companyName}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Sektör</label>
|
||||||
Sektör
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={formData.industry}
|
value={formData.industry}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('industry', e.target.value)}
|
||||||
handleInputChange("industry", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Sektör seçin</option>
|
<option value="">Sektör seçin</option>
|
||||||
|
|
@ -251,13 +228,11 @@ const CustomerForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Website</label>
|
||||||
Website
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.website}
|
value={formData.website}
|
||||||
onChange={(e) => handleInputChange("website", e.target.value)}
|
onChange={(e) => handleInputChange('website', e.target.value)}
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
placeholder="https://www.ornek.com"
|
placeholder="https://www.ornek.com"
|
||||||
/>
|
/>
|
||||||
|
|
@ -277,43 +252,35 @@ const CustomerForm: React.FC = () => {
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Email *</label>
|
||||||
Email *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.email
|
errors.email
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="email@ornek.com"
|
placeholder="email@ornek.com"
|
||||||
/>
|
/>
|
||||||
{errors.email && (
|
{errors.email && <p className="mt-1 text-sm text-red-600">{errors.email}</p>}
|
||||||
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Telefon *</label>
|
||||||
Telefon *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={(e) => handleInputChange("phone", e.target.value)}
|
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.phone
|
errors.phone
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="+90 212 555 0123"
|
placeholder="+90 212 555 0123"
|
||||||
/>
|
/>
|
||||||
{errors.phone && (
|
{errors.phone && <p className="mt-1 text-sm text-red-600">{errors.phone}</p>}
|
||||||
<p className="mt-1 text-sm text-red-600">{errors.phone}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -347,9 +314,7 @@ const CustomerForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.taxNumber}
|
value={formData.taxNumber}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('taxNumber', e.target.value)}
|
||||||
handleInputChange("taxNumber", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
placeholder="1234567890"
|
placeholder="1234567890"
|
||||||
/>
|
/>
|
||||||
|
|
@ -362,9 +327,7 @@ const CustomerForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.registrationNumber}
|
value={formData.registrationNumber}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('registrationNumber', e.target.value)}
|
||||||
handleInputChange("registrationNumber", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
placeholder="98765"
|
placeholder="98765"
|
||||||
/>
|
/>
|
||||||
|
|
@ -382,22 +345,17 @@ const CustomerForm: React.FC = () => {
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={formData.creditLimit}
|
value={formData.creditLimit}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('creditLimit', parseFloat(e.target.value) || 0)
|
||||||
"creditLimit",
|
|
||||||
parseFloat(e.target.value) || 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.creditLimit
|
errors.creditLimit
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
/>
|
/>
|
||||||
{errors.creditLimit && (
|
{errors.creditLimit && (
|
||||||
<p className="mt-1 text-sm text-red-600">
|
<p className="mt-1 text-sm text-red-600">{errors.creditLimit}</p>
|
||||||
{errors.creditLimit}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -407,9 +365,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.paymentTerms}
|
value={formData.paymentTerms}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('paymentTerms', e.target.value)}
|
||||||
handleInputChange("paymentTerms", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="CASH">Peşin</option>
|
<option value="CASH">Peşin</option>
|
||||||
|
|
@ -426,9 +382,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.currency}
|
value={formData.currency}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('currency', e.target.value)}
|
||||||
handleInputChange("currency", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="TRY">Türk Lirası (TRY)</option>
|
<option value="TRY">Türk Lirası (TRY)</option>
|
||||||
|
|
@ -457,7 +411,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.status}
|
value={formData.status}
|
||||||
onChange={(e) => handleInputChange("status", e.target.value)}
|
onChange={(e) => handleInputChange('status', e.target.value)}
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="ACTIVE">Aktif</option>
|
<option value="ACTIVE">Aktif</option>
|
||||||
|
|
@ -473,9 +427,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.customerSegment}
|
value={formData.customerSegment}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('customerSegment', e.target.value)}
|
||||||
handleInputChange("customerSegment", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="STANDARD">Standart</option>
|
<option value="STANDARD">Standart</option>
|
||||||
|
|
@ -491,9 +443,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.assignedSalesRep}
|
value={formData.assignedSalesRep}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('assignedSalesRep', e.target.value)}
|
||||||
handleInputChange("assignedSalesRep", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Temsilci seçin</option>
|
<option value="">Temsilci seçin</option>
|
||||||
|
|
@ -509,15 +459,10 @@ const CustomerForm: React.FC = () => {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="isActive"
|
id="isActive"
|
||||||
checked={formData.isActive}
|
checked={formData.isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('isActive', e.target.checked)}
|
||||||
handleInputChange("isActive", e.target.checked)
|
|
||||||
}
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
|
||||||
htmlFor="isActive"
|
|
||||||
className="ml-2 block text-sm text-gray-900"
|
|
||||||
>
|
|
||||||
Aktif
|
Aktif
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -547,14 +492,15 @@ const CustomerForm: React.FC = () => {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FaSave className="w-4 h-4 mr-2" />
|
<FaSave className="w-4 h-4 mr-2" />
|
||||||
{isEdit ? "Güncelle" : "Kaydet"}
|
{isEdit ? 'Güncelle' : 'Kaydet'}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default CustomerForm;
|
export default CustomerForm
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
FaSave,
|
FaSave,
|
||||||
FaTimes,
|
FaTimes,
|
||||||
|
|
@ -8,135 +8,128 @@ import {
|
||||||
FaMapMarkerAlt,
|
FaMapMarkerAlt,
|
||||||
FaCreditCard,
|
FaCreditCard,
|
||||||
FaEnvelope,
|
FaEnvelope,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import LoadingSpinner from "../../../components/common/LoadingSpinner";
|
import LoadingSpinner from '../../../components/common/LoadingSpinner'
|
||||||
import { BusinessParty } from "../../../types/common";
|
import { BusinessParty } from '../../../types/common'
|
||||||
import { mockBusinessPartyNew } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessPartyNew } from '../../../mocks/mockBusinessParties'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
interface ValidationErrors {
|
interface ValidationErrors {
|
||||||
[key: string]: string;
|
[key: string]: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomerForm: React.FC = () => {
|
const CustomerForm: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>()
|
||||||
const isEdit = Boolean(id);
|
const isEdit = Boolean(id)
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false)
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false)
|
||||||
const [errors, setErrors] = useState<ValidationErrors>({});
|
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||||
|
|
||||||
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew);
|
const [formData, setFormData] = useState<BusinessParty>(mockBusinessPartyNew)
|
||||||
|
|
||||||
const loadFormData = useCallback(async () => {
|
const loadFormData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
if (isEdit && id) {
|
if (isEdit && id) {
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
setFormData(mockBusinessPartyNew);
|
setFormData(mockBusinessPartyNew)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading form data:", error);
|
console.error('Error loading form data:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [isEdit, id]);
|
}, [isEdit, id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFormData();
|
loadFormData()
|
||||||
}, [loadFormData]);
|
}, [loadFormData])
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: ValidationErrors = {};
|
const newErrors: ValidationErrors = {}
|
||||||
|
|
||||||
if (!formData.code.trim()) {
|
if (!formData.code.trim()) {
|
||||||
newErrors.code = "Müşteri kodu zorunludur";
|
newErrors.code = 'Müşteri kodu zorunludur'
|
||||||
}
|
}
|
||||||
if (!formData.name.trim()) {
|
if (!formData.name.trim()) {
|
||||||
newErrors.name = "Şirket adı zorunludur";
|
newErrors.name = 'Şirket adı zorunludur'
|
||||||
}
|
}
|
||||||
if (formData.creditLimit < 0) {
|
if (formData.creditLimit < 0) {
|
||||||
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz";
|
newErrors.creditLimit = "Kredi limiti 0'dan küçük olamaz"
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors)
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (field: keyof BusinessParty, value: string | number | boolean) => {
|
||||||
field: keyof BusinessParty,
|
|
||||||
value: string | number | boolean
|
|
||||||
) => {
|
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
// Clear error when user starts typing
|
// Clear error when user starts typing
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors((prev) => ({
|
setErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: "",
|
[field]: '',
|
||||||
}));
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
console.log("Customer data:", {
|
console.log('Customer data:', {
|
||||||
...formData,
|
...formData,
|
||||||
id: isEdit ? id : undefined,
|
id: isEdit ? id : undefined,
|
||||||
});
|
})
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
alert(
|
alert(isEdit ? 'Müşteri başarıyla güncellendi!' : 'Müşteri başarıyla oluşturuldu!')
|
||||||
isEdit
|
|
||||||
? "Müşteri başarıyla güncellendi!"
|
|
||||||
: "Müşteri başarıyla oluşturuldu!"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Navigate back to list
|
// Navigate back to list
|
||||||
navigate("/admin/crm");
|
navigate('/admin/crm')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving customer:", error);
|
console.error('Error saving customer:', error)
|
||||||
alert("Bir hata oluştu. Lütfen tekrar deneyin.");
|
alert('Bir hata oluştu. Lütfen tekrar deneyin.')
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
navigate("/admin/crm");
|
navigate('/admin/crm')
|
||||||
};
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-4 pt-2">
|
<div className="space-y-4 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
{isEdit ? "Müşteri Düzenle" : "Yeni Müşteri"}
|
{isEdit ? 'Müşteri Düzenle' : 'Yeni Müşteri'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{isEdit
|
{isEdit ? 'Mevcut müşteri bilgilerini güncelleyin' : 'Yeni müşteri bilgilerini girin'}
|
||||||
? "Mevcut müşteri bilgilerini güncelleyin"
|
|
||||||
: "Yeni müşteri bilgilerini girin"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -161,18 +154,16 @@ const CustomerForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.code}
|
value={formData.code}
|
||||||
onChange={(e) => handleInputChange("code", e.target.value)}
|
onChange={(e) => handleInputChange('code', e.target.value)}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.code
|
errors.code
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Örn: MST001"
|
placeholder="Örn: MST001"
|
||||||
/>
|
/>
|
||||||
{errors.customerCode && (
|
{errors.customerCode && (
|
||||||
<p className="mt-1 text-sm text-red-600">
|
<p className="mt-1 text-sm text-red-600">{errors.customerCode}</p>
|
||||||
{errors.customerCode}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -183,10 +174,7 @@ const CustomerForm: React.FC = () => {
|
||||||
<select
|
<select
|
||||||
value={formData.customerType}
|
value={formData.customerType}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('customerType', e.target.value as 'INDIVIDUAL' | 'COMPANY')
|
||||||
"customerType",
|
|
||||||
e.target.value as "INDIVIDUAL" | "COMPANY"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
|
|
@ -204,30 +192,24 @@ const CustomerForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.name
|
errors.name
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Şirket adını girin"
|
placeholder="Şirket adını girin"
|
||||||
/>
|
/>
|
||||||
{errors.companyName && (
|
{errors.companyName && (
|
||||||
<p className="mt-1 text-sm text-red-600">
|
<p className="mt-1 text-sm text-red-600">{errors.companyName}</p>
|
||||||
{errors.companyName}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Sektör</label>
|
||||||
Sektör
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={formData.industry}
|
value={formData.industry}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('industry', e.target.value)}
|
||||||
handleInputChange("industry", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Sektör seçin</option>
|
<option value="">Sektör seçin</option>
|
||||||
|
|
@ -244,13 +226,11 @@ const CustomerForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Website</label>
|
||||||
Website
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.website}
|
value={formData.website}
|
||||||
onChange={(e) => handleInputChange("website", e.target.value)}
|
onChange={(e) => handleInputChange('website', e.target.value)}
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
placeholder="https://www.ornek.com"
|
placeholder="https://www.ornek.com"
|
||||||
/>
|
/>
|
||||||
|
|
@ -270,43 +250,35 @@ const CustomerForm: React.FC = () => {
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Email *</label>
|
||||||
Email *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.email
|
errors.email
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="email@ornek.com"
|
placeholder="email@ornek.com"
|
||||||
/>
|
/>
|
||||||
{errors.email && (
|
{errors.email && <p className="mt-1 text-sm text-red-600">{errors.email}</p>}
|
||||||
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Telefon *</label>
|
||||||
Telefon *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={(e) => handleInputChange("phone", e.target.value)}
|
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.phone
|
errors.phone
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="+90 212 555 0123"
|
placeholder="+90 212 555 0123"
|
||||||
/>
|
/>
|
||||||
{errors.phone && (
|
{errors.phone && <p className="mt-1 text-sm text-red-600">{errors.phone}</p>}
|
||||||
<p className="mt-1 text-sm text-red-600">{errors.phone}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -340,9 +312,7 @@ const CustomerForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.taxNumber}
|
value={formData.taxNumber}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('taxNumber', e.target.value)}
|
||||||
handleInputChange("taxNumber", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
placeholder="1234567890"
|
placeholder="1234567890"
|
||||||
/>
|
/>
|
||||||
|
|
@ -355,9 +325,7 @@ const CustomerForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.registrationNumber}
|
value={formData.registrationNumber}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('registrationNumber', e.target.value)}
|
||||||
handleInputChange("registrationNumber", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
placeholder="98765"
|
placeholder="98765"
|
||||||
/>
|
/>
|
||||||
|
|
@ -375,22 +343,17 @@ const CustomerForm: React.FC = () => {
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={formData.creditLimit}
|
value={formData.creditLimit}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('creditLimit', parseFloat(e.target.value) || 0)
|
||||||
"creditLimit",
|
|
||||||
parseFloat(e.target.value) || 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||||
errors.creditLimit
|
errors.creditLimit
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
/>
|
/>
|
||||||
{errors.creditLimit && (
|
{errors.creditLimit && (
|
||||||
<p className="mt-1 text-sm text-red-600">
|
<p className="mt-1 text-sm text-red-600">{errors.creditLimit}</p>
|
||||||
{errors.creditLimit}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -400,9 +363,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.paymentTerms}
|
value={formData.paymentTerms}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('paymentTerms', e.target.value)}
|
||||||
handleInputChange("paymentTerms", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="CASH">Peşin</option>
|
<option value="CASH">Peşin</option>
|
||||||
|
|
@ -419,9 +380,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.currency}
|
value={formData.currency}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('currency', e.target.value)}
|
||||||
handleInputChange("currency", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="TRY">Türk Lirası (TRY)</option>
|
<option value="TRY">Türk Lirası (TRY)</option>
|
||||||
|
|
@ -450,7 +409,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.status}
|
value={formData.status}
|
||||||
onChange={(e) => handleInputChange("status", e.target.value)}
|
onChange={(e) => handleInputChange('status', e.target.value)}
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="ACTIVE">Aktif</option>
|
<option value="ACTIVE">Aktif</option>
|
||||||
|
|
@ -466,9 +425,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.customerSegment}
|
value={formData.customerSegment}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('customerSegment', e.target.value)}
|
||||||
handleInputChange("customerSegment", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="STANDARD">Standart</option>
|
<option value="STANDARD">Standart</option>
|
||||||
|
|
@ -484,9 +441,7 @@ const CustomerForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.assignedSalesRep}
|
value={formData.assignedSalesRep}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('assignedSalesRep', e.target.value)}
|
||||||
handleInputChange("assignedSalesRep", e.target.value)
|
|
||||||
}
|
|
||||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Temsilci seçin</option>
|
<option value="">Temsilci seçin</option>
|
||||||
|
|
@ -502,15 +457,10 @@ const CustomerForm: React.FC = () => {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="isActive"
|
id="isActive"
|
||||||
checked={formData.isActive}
|
checked={formData.isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('isActive', e.target.checked)}
|
||||||
handleInputChange("isActive", e.target.checked)
|
|
||||||
}
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
|
||||||
htmlFor="isActive"
|
|
||||||
className="ml-2 block text-sm text-gray-900"
|
|
||||||
>
|
|
||||||
Aktif
|
Aktif
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -540,14 +490,15 @@ const CustomerForm: React.FC = () => {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FaSave className="w-4 h-4 mr-2" />
|
<FaSave className="w-4 h-4 mr-2" />
|
||||||
{isEdit ? "Güncelle" : "Kaydet"}
|
{isEdit ? 'Güncelle' : 'Kaydet'}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default CustomerForm;
|
export default CustomerForm
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom'
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
FaUserCheck,
|
FaUserCheck,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
|
|
@ -16,54 +16,50 @@ import {
|
||||||
FaExclamationTriangle,
|
FaExclamationTriangle,
|
||||||
FaStar,
|
FaStar,
|
||||||
FaCalendar,
|
FaCalendar,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames'
|
||||||
import { CustomerSegmentEnum } from "../../../types/crm";
|
import { CustomerSegmentEnum } from '../../../types/crm'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import { BusinessPartyStatusEnum, PartyType } from "../../../types/common";
|
import { BusinessPartyStatusEnum, PartyType } from '../../../types/common'
|
||||||
import Widget from "../../../components/common/Widget";
|
import Widget from '../../../components/common/Widget'
|
||||||
import {
|
import {
|
||||||
getBusinessPartyStatusColor,
|
getBusinessPartyStatusColor,
|
||||||
getBusinessPartyStatusName,
|
getBusinessPartyStatusName,
|
||||||
getCustomerSegmentColor,
|
getCustomerSegmentColor,
|
||||||
getCustomerSegmentName,
|
getCustomerSegmentName,
|
||||||
} from "../../../utils/erp";
|
} from '../../../utils/erp'
|
||||||
|
|
||||||
const CustomerList: React.FC = () => {
|
const CustomerList: React.FC = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [filterStatus, setFilterStatus] = useState("all");
|
const [filterStatus, setFilterStatus] = useState('all')
|
||||||
const [filterSegment, setFilterSegment] = useState("all");
|
const [filterSegment, setFilterSegment] = useState('all')
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: customers,
|
data: customers,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["customers", searchTerm, filterStatus, filterSegment],
|
queryKey: ['customers', searchTerm, filterStatus, filterSegment],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
const mockCustomers = mockBusinessParties.filter(
|
const mockCustomers = mockBusinessParties.filter(
|
||||||
(customer) => customer.partyType === PartyType.Customer
|
(customer) => customer.partyType === PartyType.Customer,
|
||||||
);
|
)
|
||||||
|
|
||||||
return mockCustomers.filter((customer) => {
|
return mockCustomers.filter((customer) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
customer.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
customer.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
customer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
customer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
customer.primaryContact?.fullName
|
customer.primaryContact?.fullName.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
.toLowerCase()
|
const matchesStatus = filterStatus === 'all' || customer.status === filterStatus
|
||||||
.includes(searchTerm.toLowerCase());
|
const matchesSegment = filterSegment === 'all' || customer.customerSegment === filterSegment
|
||||||
const matchesStatus =
|
return matchesSearch && matchesStatus && matchesSegment
|
||||||
filterStatus === "all" || customer.status === filterStatus;
|
})
|
||||||
const matchesSegment =
|
|
||||||
filterSegment === "all" || customer.customerSegment === filterSegment;
|
|
||||||
return matchesSearch && matchesStatus && matchesSegment;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -71,7 +67,7 @@ const CustomerList: React.FC = () => {
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
<span className="ml-3 text-gray-600">Müşteriler yükleniyor...</span>
|
<span className="ml-3 text-gray-600">Müşteriler yükleniyor...</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -79,12 +75,10 @@ const CustomerList: React.FC = () => {
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" />
|
<FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" />
|
||||||
<span className="text-red-800">
|
<span className="text-red-800">Müşteriler yüklenirken hata oluştu.</span>
|
||||||
Müşteriler yüklenirken hata oluştu.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -109,10 +103,10 @@ const CustomerList: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors",
|
'flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors',
|
||||||
showFilters
|
showFilters
|
||||||
? "border-blue-500 bg-blue-50 text-blue-700"
|
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||||
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FaFilter size={14} className="mr-2" />
|
<FaFilter size={14} className="mr-2" />
|
||||||
|
|
@ -122,7 +116,7 @@ const CustomerList: React.FC = () => {
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => alert("Dışa aktarma özelliği yakında eklenecek")}
|
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
|
||||||
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<FaDownload size={14} className="mr-2" />
|
<FaDownload size={14} className="mr-2" />
|
||||||
|
|
@ -144,18 +138,14 @@ const CustomerList: React.FC = () => {
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
|
||||||
Durum
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={filterStatus}
|
value={filterStatus}
|
||||||
onChange={(e) => setFilterStatus(e.target.value)}
|
onChange={(e) => setFilterStatus(e.target.value)}
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value="all">Tümü</option>
|
<option value="all">Tümü</option>
|
||||||
<option value={BusinessPartyStatusEnum.Prospect}>
|
<option value={BusinessPartyStatusEnum.Prospect}>Potansiyel</option>
|
||||||
Potansiyel
|
|
||||||
</option>
|
|
||||||
<option value={BusinessPartyStatusEnum.Active}>Aktif</option>
|
<option value={BusinessPartyStatusEnum.Active}>Aktif</option>
|
||||||
<option value={BusinessPartyStatusEnum.Inactive}>Pasif</option>
|
<option value={BusinessPartyStatusEnum.Inactive}>Pasif</option>
|
||||||
<option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option>
|
<option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option>
|
||||||
|
|
@ -163,9 +153,7 @@ const CustomerList: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Segment</label>
|
||||||
Segment
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={filterSegment}
|
value={filterSegment}
|
||||||
onChange={(e) => setFilterSegment(e.target.value)}
|
onChange={(e) => setFilterSegment(e.target.value)}
|
||||||
|
|
@ -182,9 +170,9 @@ const CustomerList: React.FC = () => {
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFilterStatus("all");
|
setFilterStatus('all')
|
||||||
setFilterSegment("all");
|
setFilterSegment('all')
|
||||||
setSearchTerm("");
|
setSearchTerm('')
|
||||||
}}
|
}}
|
||||||
className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
|
|
@ -206,11 +194,7 @@ const CustomerList: React.FC = () => {
|
||||||
|
|
||||||
<Widget
|
<Widget
|
||||||
title="Aktif Müşteri"
|
title="Aktif Müşteri"
|
||||||
value={
|
value={customers?.filter((c) => c.status === BusinessPartyStatusEnum.Active).length || 0}
|
||||||
customers?.filter(
|
|
||||||
(c) => c.status === BusinessPartyStatusEnum.Active
|
|
||||||
).length || 0
|
|
||||||
}
|
|
||||||
color="green"
|
color="green"
|
||||||
icon="FaUserCheck"
|
icon="FaUserCheck"
|
||||||
/>
|
/>
|
||||||
|
|
@ -218,9 +202,7 @@ const CustomerList: React.FC = () => {
|
||||||
<Widget
|
<Widget
|
||||||
title="Toplam Ciro"
|
title="Toplam Ciro"
|
||||||
value={`₺${
|
value={`₺${
|
||||||
customers
|
customers?.reduce((acc, c) => acc + (c.totalRevenue ?? 0), 0).toLocaleString() || 0
|
||||||
?.reduce((acc, c) => acc + (c.totalRevenue ?? 0), 0)
|
|
||||||
.toLocaleString() || 0
|
|
||||||
}`}
|
}`}
|
||||||
color="purple"
|
color="purple"
|
||||||
icon="FaDollarSign"
|
icon="FaDollarSign"
|
||||||
|
|
@ -231,10 +213,8 @@ const CustomerList: React.FC = () => {
|
||||||
value={`₺${
|
value={`₺${
|
||||||
customers?.length
|
customers?.length
|
||||||
? Math.round(
|
? Math.round(
|
||||||
customers.reduce(
|
customers.reduce((acc, c) => acc + (c.averageOrderValue ?? 0), 0) /
|
||||||
(acc, c) => acc + (c.averageOrderValue ?? 0),
|
customers.length,
|
||||||
0
|
|
||||||
) / customers.length
|
|
||||||
).toLocaleString()
|
).toLocaleString()
|
||||||
: 0
|
: 0
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -279,10 +259,7 @@ const CustomerList: React.FC = () => {
|
||||||
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{customers?.map((customer) => (
|
{customers?.map((customer) => (
|
||||||
<tr
|
<tr key={customer.id} className="hover:bg-gray-50 transition-colors">
|
||||||
key={customer.id}
|
|
||||||
className="hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-shrink-0 h-8 w-8">
|
<div className="flex-shrink-0 h-8 w-8">
|
||||||
|
|
@ -291,16 +268,10 @@ const CustomerList: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<div className="text-xs font-medium text-gray-900">
|
<div className="text-xs font-medium text-gray-900">{customer.code}</div>
|
||||||
{customer.code}
|
<div className="text-sm text-gray-500">{customer.name}</div>
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{customer.name}
|
|
||||||
</div>
|
|
||||||
{customer.industry && (
|
{customer.industry && (
|
||||||
<div className="text-xs text-gray-400 mt-1">
|
<div className="text-xs text-gray-400 mt-1">{customer.industry}</div>
|
||||||
{customer.industry}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -332,26 +303,26 @@ const CustomerList: React.FC = () => {
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-sm font-medium",
|
'text-sm font-medium',
|
||||||
getCustomerSegmentColor(
|
getCustomerSegmentColor(
|
||||||
customer.customerSegment || CustomerSegmentEnum.SMB
|
customer.customerSegment || CustomerSegmentEnum.SMB,
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getCustomerSegmentName(
|
{getCustomerSegmentName(
|
||||||
customer.customerSegment || CustomerSegmentEnum.SMB
|
customer.customerSegment || CustomerSegmentEnum.SMB,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||||
getBusinessPartyStatusColor(
|
getBusinessPartyStatusColor(
|
||||||
customer.status ?? BusinessPartyStatusEnum.Prospect
|
customer.status ?? BusinessPartyStatusEnum.Prospect,
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getBusinessPartyStatusName(
|
{getBusinessPartyStatusName(
|
||||||
customer.status ?? BusinessPartyStatusEnum.Prospect
|
customer.status ?? BusinessPartyStatusEnum.Prospect,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -363,15 +334,12 @@ const CustomerList: React.FC = () => {
|
||||||
₺{(customer.totalRevenue ?? 0).toLocaleString()}
|
₺{(customer.totalRevenue ?? 0).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
Ort. Sipariş: ₺
|
Ort. Sipariş: ₺{(customer.averageOrderValue ?? 0).toLocaleString()}
|
||||||
{(customer.averageOrderValue ?? 0).toLocaleString()}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FaStar size={14} className="text-yellow-500 mr-1" />
|
<FaStar size={14} className="text-yellow-500 mr-1" />
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
LTV: ₺
|
LTV: ₺{((customer.lifetimeValue ?? 0) / 1000000).toFixed(1)}M
|
||||||
{((customer.lifetimeValue ?? 0) / 1000000).toFixed(1)}
|
|
||||||
M
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -381,9 +349,7 @@ const CustomerList: React.FC = () => {
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="text-sm font-medium text-gray-900">
|
||||||
₺{customer.creditLimit.toLocaleString()}
|
₺{customer.creditLimit.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">{customer.paymentTerms}</div>
|
||||||
{customer.paymentTerms}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
|
|
@ -391,7 +357,7 @@ const CustomerList: React.FC = () => {
|
||||||
{customer.lastOrderDate && (
|
{customer.lastOrderDate && (
|
||||||
<div className="flex items-center text-sm text-gray-900">
|
<div className="flex items-center text-sm text-gray-900">
|
||||||
<FaCalendar size={14} className="mr-1" />
|
<FaCalendar size={14} className="mr-1" />
|
||||||
{dayjs(customer.lastOrderDate).format("DD.MM.YYYY")}
|
{dayjs(customer.lastOrderDate).format('DD.MM.YYYY')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm text-gray-500">Son sipariş</div>
|
<div className="text-sm text-gray-500">Son sipariş</div>
|
||||||
|
|
@ -426,12 +392,8 @@ const CustomerList: React.FC = () => {
|
||||||
{(!customers || customers.length === 0) && (
|
{(!customers || customers.length === 0) && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaUserCheck className="mx-auto h-10 w-10 text-gray-400" />
|
<FaUserCheck className="mx-auto h-10 w-10 text-gray-400" />
|
||||||
<h3 className="mt-2 text-xs font-medium text-gray-900">
|
<h3 className="mt-2 text-xs font-medium text-gray-900">Müşteri bulunamadı</h3>
|
||||||
Müşteri bulunamadı
|
<p className="mt-1 text-xs text-gray-500">Yeni müşteri ekleyerek başlayın.</p>
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
|
||||||
Yeni müşteri ekleyerek başlayın.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Link
|
<Link
|
||||||
to="/admin/crm/customers/new"
|
to="/admin/crm/customers/new"
|
||||||
|
|
@ -445,7 +407,7 @@ const CustomerList: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CustomerList;
|
export default CustomerList
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,46 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import { FaList, FaTh } from "react-icons/fa";
|
import { FaList, FaTh } from 'react-icons/fa'
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames'
|
||||||
import CustomerList from "./CustomerList";
|
import CustomerList from './CustomerList'
|
||||||
import CustomerCards from "./CustomerCards";
|
import CustomerCards from './CustomerCards'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
export type ViewMode = "list" | "cards";
|
export type ViewMode = 'list' | 'cards'
|
||||||
|
|
||||||
const CustomerListWithToggle: React.FC = () => {
|
const CustomerListWithToggle: React.FC = () => {
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>("list");
|
const [viewMode, setViewMode] = useState<ViewMode>('list')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-4 pt-2">
|
<div className="space-y-4 pt-2">
|
||||||
{/* Header with View Toggle */}
|
{/* Header with View Toggle */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-gray-900">Müşteri Listesi</h1>
|
<h1 className="text-xl font-bold text-gray-900">Müşteri Listesi</h1>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">Müşteri listesi ve detay bilgileri</p>
|
||||||
Müşteri listesi ve detay bilgileri
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View Mode Toggle */}
|
{/* View Mode Toggle */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex bg-gray-100 rounded-lg p-1">
|
<div className="flex bg-gray-100 rounded-lg p-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("list")}
|
onClick={() => setViewMode('list')}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center space-x-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
'flex items-center space-x-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors',
|
||||||
viewMode === "list"
|
viewMode === 'list'
|
||||||
? "bg-white text-gray-900 shadow-sm"
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: 'text-gray-600 hover:text-gray-900',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FaList className="w-4 h-4" />
|
<FaList className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("cards")}
|
onClick={() => setViewMode('cards')}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center space-x-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
'flex items-center space-x-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors',
|
||||||
viewMode === "cards"
|
viewMode === 'cards'
|
||||||
? "bg-white text-gray-900 shadow-sm"
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: 'text-gray-600 hover:text-gray-900',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FaTh className="w-4 h-4" />
|
<FaTh className="w-4 h-4" />
|
||||||
|
|
@ -51,10 +51,11 @@ const CustomerListWithToggle: React.FC = () => {
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="transition-all duration-200">
|
<div className="transition-all duration-200">
|
||||||
{viewMode === "list" ? <CustomerList /> : <CustomerCards />}
|
{viewMode === 'list' ? <CustomerList /> : <CustomerCards />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default CustomerListWithToggle;
|
export default CustomerListWithToggle
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
import { useParams, Link, useNavigate } from 'react-router-dom'
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
FaArrowLeft,
|
FaArrowLeft,
|
||||||
FaEdit,
|
FaEdit,
|
||||||
|
|
@ -23,36 +23,37 @@ import {
|
||||||
FaUsers,
|
FaUsers,
|
||||||
FaChartBar,
|
FaChartBar,
|
||||||
FaArrowUp,
|
FaArrowUp,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames'
|
||||||
import { CustomerSegmentEnum } from "../../../types/crm";
|
import { CustomerSegmentEnum } from '../../../types/crm'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import {
|
import {
|
||||||
getBusinessPartyStatusColor,
|
getBusinessPartyStatusColor,
|
||||||
getBusinessPartyStatusName,
|
getBusinessPartyStatusName,
|
||||||
getCustomerSegmentName,
|
getCustomerSegmentName,
|
||||||
} from "../../../utils/erp";
|
} from '../../../utils/erp'
|
||||||
import { BusinessPartyStatusEnum } from "../../../types/common";
|
import { BusinessPartyStatusEnum } from '../../../types/common'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const CustomerView: React.FC = () => {
|
const CustomerView: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const [activeTab, setActiveTab] = useState("overview");
|
const [activeTab, setActiveTab] = useState('overview')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: customer,
|
data: customer,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["customer", id],
|
queryKey: ['customer', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||||
const found = mockBusinessParties.find((c) => c.id === id);
|
const found = mockBusinessParties.find((c) => c.id === id)
|
||||||
if (!found) throw new Error("Müşteri bulunamadı");
|
if (!found) throw new Error('Müşteri bulunamadı')
|
||||||
return found;
|
return found
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -85,7 +86,7 @@ const CustomerView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !customer) {
|
if (error || !customer) {
|
||||||
|
|
@ -96,18 +97,15 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FaExclamationTriangle className="h-8 w-8 text-red-600 mr-3" />
|
<FaExclamationTriangle className="h-8 w-8 text-red-600 mr-3" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-red-800">
|
<h3 className="text-lg font-medium text-red-800">Müşteri Bulunamadı</h3>
|
||||||
Müşteri Bulunamadı
|
|
||||||
</h3>
|
|
||||||
<p className="text-red-600">
|
<p className="text-red-600">
|
||||||
Aradığınız müşteri mevcut değil veya erişim izniniz
|
Aradığınız müşteri mevcut değil veya erişim izniniz bulunmuyor.
|
||||||
bulunmuyor.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/customers")}
|
onClick={() => navigate('/admin/crm/customers')}
|
||||||
className="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
className="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-4 h-4 mr-2" />
|
<FaArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
|
@ -117,18 +115,19 @@ const CustomerView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: "overview", label: "Genel Bakış", icon: FaChartLine },
|
{ id: 'overview', label: 'Genel Bakış', icon: FaChartLine },
|
||||||
{ id: "contact", label: "İletişim", icon: FaUser },
|
{ id: 'contact', label: 'İletişim', icon: FaUser },
|
||||||
{ id: "business", label: "İş Bilgileri", icon: FaBuilding },
|
{ id: 'business', label: 'İş Bilgileri', icon: FaBuilding },
|
||||||
{ id: "financial", label: "Finansal", icon: FaDollarSign },
|
{ id: 'financial', label: 'Finansal', icon: FaDollarSign },
|
||||||
{ id: "history", label: "Geçmiş", icon: FaHistory },
|
{ id: 'history', label: 'Geçmiş', icon: FaHistory },
|
||||||
];
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<div className="bg-white border-b border-gray-200">
|
<div className="bg-white border-b border-gray-200">
|
||||||
<div className="mx-auto px-4 py-3">
|
<div className="mx-auto px-4 py-3">
|
||||||
|
|
@ -145,7 +144,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/customers")}
|
onClick={() => navigate('/admin/crm/customers')}
|
||||||
className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-5 h-5" />
|
<FaArrowLeft className="w-5 h-5" />
|
||||||
|
|
@ -158,38 +157,32 @@ const CustomerView: React.FC = () => {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<h1 className="text-xl font-bold text-gray-900">
|
<h1 className="text-xl font-bold text-gray-900">{customer.name}</h1>
|
||||||
{customer.name}
|
|
||||||
</h1>
|
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border",
|
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border',
|
||||||
getBusinessPartyStatusColor(
|
getBusinessPartyStatusColor(
|
||||||
customer.status || BusinessPartyStatusEnum.Active
|
customer.status || BusinessPartyStatusEnum.Active,
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getBusinessPartyStatusName(
|
{getBusinessPartyStatusName(
|
||||||
customer.status || BusinessPartyStatusEnum.Active
|
customer.status || BusinessPartyStatusEnum.Active,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4 mt-1">
|
<div className="flex items-center space-x-4 mt-1">
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">{customer.code}</span>
|
||||||
{customer.code}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-gray-400">•</span>
|
<span className="text-sm text-gray-400">•</span>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{getCustomerSegmentName(
|
{getCustomerSegmentName(
|
||||||
customer.customerSegment || CustomerSegmentEnum.Startup
|
customer.customerSegment || CustomerSegmentEnum.Startup,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{customer.industry && (
|
{customer.industry && (
|
||||||
<>
|
<>
|
||||||
<span className="text-sm text-gray-400">•</span>
|
<span className="text-sm text-gray-400">•</span>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">{customer.industry}</span>
|
||||||
{customer.industry}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -213,9 +206,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="bg-gray-50 rounded-lg p-3">
|
<div className="bg-gray-50 rounded-lg p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">
|
<p className="text-sm font-medium text-gray-600">Toplam Ciro</p>
|
||||||
Toplam Ciro
|
|
||||||
</p>
|
|
||||||
<p className="text-lg font-bold text-gray-900">
|
<p className="text-lg font-bold text-gray-900">
|
||||||
₺{customer.totalRevenue?.toLocaleString()}
|
₺{customer.totalRevenue?.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -227,9 +218,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="bg-gray-50 rounded-lg p-3">
|
<div className="bg-gray-50 rounded-lg p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">
|
<p className="text-sm font-medium text-gray-600">Ort. Sipariş</p>
|
||||||
Ort. Sipariş
|
|
||||||
</p>
|
|
||||||
<p className="text-lg font-bold text-gray-900">
|
<p className="text-lg font-bold text-gray-900">
|
||||||
₺{customer.averageOrderValue?.toLocaleString()}
|
₺{customer.averageOrderValue?.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -241,9 +230,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="bg-gray-50 rounded-lg p-3">
|
<div className="bg-gray-50 rounded-lg p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">
|
<p className="text-sm font-medium text-gray-600">Yaşam Boyu Değer</p>
|
||||||
Yaşam Boyu Değer
|
|
||||||
</p>
|
|
||||||
<p className="text-lg font-bold text-gray-900">
|
<p className="text-lg font-bold text-gray-900">
|
||||||
₺{((customer.lifetimeValue ?? 0) / 1000000).toFixed(1)}M
|
₺{((customer.lifetimeValue ?? 0) / 1000000).toFixed(1)}M
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -255,9 +242,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="bg-gray-50 rounded-lg p-3">
|
<div className="bg-gray-50 rounded-lg p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">
|
<p className="text-sm font-medium text-gray-600">Kredi Limiti</p>
|
||||||
Kredi Limiti
|
|
||||||
</p>
|
|
||||||
<p className="text-lg font-bold text-gray-900">
|
<p className="text-lg font-bold text-gray-900">
|
||||||
₺{customer.creditLimit.toLocaleString()}
|
₺{customer.creditLimit.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -278,10 +263,10 @@ const CustomerView: React.FC = () => {
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center space-x-2 py-3 px-1 border-b-2 font-medium text-sm transition-colors",
|
'flex items-center space-x-2 py-3 px-1 border-b-2 font-medium text-sm transition-colors',
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? "border-blue-500 text-blue-600"
|
? 'border-blue-500 text-blue-600'
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<tab.icon className="w-4 h-4" />
|
<tab.icon className="w-4 h-4" />
|
||||||
|
|
@ -294,7 +279,7 @@ const CustomerView: React.FC = () => {
|
||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
<div className="mx-auto py-4">
|
<div className="mx-auto py-4">
|
||||||
{activeTab === "overview" && (
|
{activeTab === 'overview' && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
{/* Main Overview */}
|
{/* Main Overview */}
|
||||||
<div className="lg:col-span-2 space-y-4">
|
<div className="lg:col-span-2 space-y-4">
|
||||||
|
|
@ -306,9 +291,7 @@ const CustomerView: React.FC = () => {
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 mb-3">
|
<h4 className="font-medium text-gray-900 mb-3">Temel Bilgiler</h4>
|
||||||
Temel Bilgiler
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Müşteri Kodu:</span>
|
<span className="text-gray-600">Müşteri Kodu:</span>
|
||||||
|
|
@ -321,15 +304,14 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Sektör:</span>
|
<span className="text-gray-600">Sektör:</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{customer.industry || "Belirtilmemiş"}
|
{customer.industry || 'Belirtilmemiş'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Segment:</span>
|
<span className="text-gray-600">Segment:</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{getCustomerSegmentName(
|
{getCustomerSegmentName(
|
||||||
customer.customerSegment ||
|
customer.customerSegment || CustomerSegmentEnum.Startup,
|
||||||
CustomerSegmentEnum.Startup
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -337,23 +319,19 @@ const CustomerView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 mb-3">
|
<h4 className="font-medium text-gray-900 mb-3">Performans</h4>
|
||||||
Performans
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Son Sipariş:</span>
|
<span className="text-gray-600">Son Sipariş:</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{customer.lastOrderDate
|
{customer.lastOrderDate
|
||||||
? dayjs(customer.lastOrderDate).format("DD.MM.YYYY")
|
? dayjs(customer.lastOrderDate).format('DD.MM.YYYY')
|
||||||
: "Henüz sipariş yok"}
|
: 'Henüz sipariş yok'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Ödeme Koşulları:</span>
|
<span className="text-gray-600">Ödeme Koşulları:</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">{customer.paymentTerms}</span>
|
||||||
{customer.paymentTerms}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Para Birimi:</span>
|
<span className="text-gray-600">Para Birimi:</span>
|
||||||
|
|
@ -362,7 +340,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Kayıt Tarihi:</span>
|
<span className="text-gray-600">Kayıt Tarihi:</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{dayjs(customer.creationTime).format("DD.MM.YYYY")}
|
{dayjs(customer.creationTime).format('DD.MM.YYYY')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -393,9 +371,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="text-xl font-bold text-purple-600">
|
<div className="text-xl font-bold text-purple-600">
|
||||||
₺{((customer.averageOrderValue ?? 0) / 1000).toFixed(0)}K
|
₺{((customer.averageOrderValue ?? 0) / 1000).toFixed(0)}K
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">Ortalama Sipariş</div>
|
||||||
Ortalama Sipariş
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -405,12 +381,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="flex justify-between text-sm mb-1">
|
<div className="flex justify-between text-sm mb-1">
|
||||||
<span>Kredi Kullanımı</span>
|
<span>Kredi Kullanımı</span>
|
||||||
<span>
|
<span>
|
||||||
{Math.round(
|
{Math.round(((customer.totalRevenue ?? 0) / customer.creditLimit) * 100)}%
|
||||||
((customer.totalRevenue ?? 0) /
|
|
||||||
customer.creditLimit) *
|
|
||||||
100
|
|
||||||
)}
|
|
||||||
%
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-1.5">
|
<div className="w-full bg-gray-200 rounded-full h-1.5">
|
||||||
|
|
@ -418,10 +389,8 @@ const CustomerView: React.FC = () => {
|
||||||
className="bg-blue-600 h-1.5 rounded-full"
|
className="bg-blue-600 h-1.5 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.min(
|
width: `${Math.min(
|
||||||
((customer.totalRevenue ?? 0) /
|
((customer.totalRevenue ?? 0) / (customer.creditLimit ?? 1)) * 100,
|
||||||
(customer.creditLimit ?? 1)) *
|
|
||||||
100,
|
100,
|
||||||
100
|
|
||||||
)}%`,
|
)}%`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
|
@ -447,7 +416,7 @@ const CustomerView: React.FC = () => {
|
||||||
{customer.primaryContact?.fullName}
|
{customer.primaryContact?.fullName}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{customer.primaryContact?.title || "Pozisyon belirtilmemiş"}
|
{customer.primaryContact?.title || 'Pozisyon belirtilmemiş'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -477,9 +446,7 @@ const CustomerView: React.FC = () => {
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3">
|
<h3 className="text-base font-semibold text-gray-900 mb-3">Hızlı İşlemler</h3>
|
||||||
Hızlı İşlemler
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<button className="w-full flex items-center justify-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
<button className="w-full flex items-center justify-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
<FaShoppingCart className="w-4 h-4 mr-2" />
|
<FaShoppingCart className="w-4 h-4 mr-2" />
|
||||||
|
|
@ -506,11 +473,9 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div>
|
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm text-gray-900">
|
<p className="text-sm text-gray-900">Yeni sipariş oluşturuldu</p>
|
||||||
Yeni sipariş oluşturuldu
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{dayjs().subtract(2, "hour").format("HH:mm")}
|
{dayjs().subtract(2, 'hour').format('HH:mm')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -519,7 +484,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm text-gray-900">Teklif onaylandı</p>
|
<p className="text-sm text-gray-900">Teklif onaylandı</p>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{dayjs().subtract(1, "day").format("DD.MM.YYYY")}
|
{dayjs().subtract(1, 'day').format('DD.MM.YYYY')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -528,7 +493,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm text-gray-900">Görüşme yapıldı</p>
|
<p className="text-sm text-gray-900">Görüşme yapıldı</p>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{dayjs().subtract(3, "day").format("DD.MM.YYYY")}
|
{dayjs().subtract(3, 'day').format('DD.MM.YYYY')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -538,7 +503,7 @@ const CustomerView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "contact" && (
|
{activeTab === 'contact' && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
{/* Primary Contact */}
|
{/* Primary Contact */}
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
|
@ -557,8 +522,7 @@ const CustomerView: React.FC = () => {
|
||||||
{customer.primaryContact?.fullName}
|
{customer.primaryContact?.fullName}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
{customer.primaryContact?.title ||
|
{customer.primaryContact?.title || 'Pozisyon belirtilmemiş'}
|
||||||
"Pozisyon belirtilmemiş"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -605,16 +569,12 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="p-4 bg-gray-50 rounded-lg">
|
<div className="p-4 bg-gray-50 rounded-lg">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">{customer.address?.street}</p>
|
||||||
{customer.address?.street}
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-600">{customer.address?.city}</p>
|
<p className="text-gray-600">{customer.address?.city}</p>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
{customer.address?.state} {customer.address?.postalCode}
|
{customer.address?.state} {customer.address?.postalCode}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-600 font-medium">
|
<p className="text-gray-600 font-medium">{customer.address?.country}</p>
|
||||||
{customer.address?.country}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -647,20 +607,15 @@ const CustomerView: React.FC = () => {
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
{customer.contacts.map((contact, index) => (
|
{customer.contacts.map((contact, index) => (
|
||||||
<div
|
<div key={index} className="p-3 border border-gray-200 rounded-lg">
|
||||||
key={index}
|
|
||||||
className="p-3 border border-gray-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
<FaUser className="w-5 h-5 text-gray-500" />
|
<FaUser className="w-5 h-5 text-gray-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="font-medium text-gray-900">
|
<h4 className="font-medium text-gray-900">{contact.fullName}</h4>
|
||||||
{contact.fullName}
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-gray-600 mb-2">
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
{contact.title || "Pozisyon belirtilmemiş"}
|
{contact.title || 'Pozisyon belirtilmemiş'}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center text-sm text-gray-600">
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
|
|
@ -694,7 +649,7 @@ const CustomerView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "business" && (
|
{activeTab === 'business' && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
{/* Company Information */}
|
{/* Company Information */}
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
|
@ -721,12 +676,8 @@ const CustomerView: React.FC = () => {
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">Sektör</label>
|
||||||
Sektör
|
<p className="text-gray-900">{customer.industry || 'Belirtilmemiş'}</p>
|
||||||
</label>
|
|
||||||
<p className="text-gray-900">
|
|
||||||
{customer.industry || "Belirtilmemiş"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
|
@ -741,9 +692,7 @@ const CustomerView: React.FC = () => {
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
Vergi Numarası
|
Vergi Numarası
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900 font-mono">
|
<p className="text-gray-900 font-mono">{customer.taxNumber}</p>
|
||||||
{customer.taxNumber}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -752,9 +701,7 @@ const CustomerView: React.FC = () => {
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
Sicil Numarası
|
Sicil Numarası
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900 font-mono">
|
<p className="text-gray-900 font-mono">{customer.registrationNumber}</p>
|
||||||
{customer.registrationNumber}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -775,14 +722,14 @@ const CustomerView: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-sm font-medium border",
|
'inline-flex items-center px-2.5 py-0.5 rounded-full text-sm font-medium border',
|
||||||
getBusinessPartyStatusColor(
|
getBusinessPartyStatusColor(
|
||||||
customer.status || BusinessPartyStatusEnum.Active
|
customer.status || BusinessPartyStatusEnum.Active,
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getBusinessPartyStatusName(
|
{getBusinessPartyStatusName(
|
||||||
customer.status || BusinessPartyStatusEnum.Active
|
customer.status || BusinessPartyStatusEnum.Active,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -792,7 +739,7 @@ const CustomerView: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">
|
||||||
{getCustomerSegmentName(
|
{getCustomerSegmentName(
|
||||||
customer.customerSegment || CustomerSegmentEnum.Startup
|
customer.customerSegment || CustomerSegmentEnum.Startup,
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -813,7 +760,7 @@ const CustomerView: React.FC = () => {
|
||||||
Kayıt Tarihi
|
Kayıt Tarihi
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">
|
||||||
{dayjs(customer.creationTime).format("DD.MM.YYYY")}
|
{dayjs(customer.creationTime).format('DD.MM.YYYY')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -821,9 +768,7 @@ const CustomerView: React.FC = () => {
|
||||||
Son Güncelleme
|
Son Güncelleme
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">
|
||||||
{dayjs(customer.lastModificationTime).format(
|
{dayjs(customer.lastModificationTime).format('DD.MM.YYYY')}
|
||||||
"DD.MM.YYYY"
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -832,7 +777,7 @@ const CustomerView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "financial" && (
|
{activeTab === 'financial' && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
{/* Financial Overview */}
|
{/* Financial Overview */}
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
|
@ -845,9 +790,7 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
<div className="p-4 bg-green-50 rounded-lg">
|
<div className="p-4 bg-green-50 rounded-lg">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-sm font-medium text-green-800">
|
<span className="text-sm font-medium text-green-800">Toplam Ciro</span>
|
||||||
Toplam Ciro
|
|
||||||
</span>
|
|
||||||
<FaArrowUp className="w-4 h-4 text-green-600" />
|
<FaArrowUp className="w-4 h-4 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-green-900">
|
<div className="text-2xl font-bold text-green-900">
|
||||||
|
|
@ -857,9 +800,7 @@ const CustomerView: React.FC = () => {
|
||||||
|
|
||||||
<div className="p-4 bg-blue-50 rounded-lg">
|
<div className="p-4 bg-blue-50 rounded-lg">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-sm font-medium text-blue-800">
|
<span className="text-sm font-medium text-blue-800">Ortalama Sipariş</span>
|
||||||
Ortalama Sipariş
|
|
||||||
</span>
|
|
||||||
<FaShoppingCart className="w-4 h-4 text-blue-600" />
|
<FaShoppingCart className="w-4 h-4 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-blue-900">
|
<div className="text-2xl font-bold text-blue-900">
|
||||||
|
|
@ -882,38 +823,25 @@ const CustomerView: React.FC = () => {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<span className="text-sm font-medium text-gray-700">
|
<span className="text-sm font-medium text-gray-700">Kredi Kullanımı</span>
|
||||||
Kredi Kullanımı
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{Math.round(
|
{Math.round(((customer.totalRevenue ?? 0) / customer.creditLimit) * 100)}%
|
||||||
((customer.totalRevenue ?? 0) / customer.creditLimit) *
|
|
||||||
100
|
|
||||||
)}
|
|
||||||
%
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"h-3 rounded-full",
|
'h-3 rounded-full',
|
||||||
((customer.totalRevenue ?? 0) / customer.creditLimit) *
|
((customer.totalRevenue ?? 0) / customer.creditLimit) * 100 > 80
|
||||||
100 >
|
? 'bg-red-500'
|
||||||
80
|
: ((customer.totalRevenue ?? 0) / customer.creditLimit) * 100 > 60
|
||||||
? "bg-red-500"
|
? 'bg-yellow-500'
|
||||||
: ((customer.totalRevenue ?? 0) /
|
: 'bg-green-500',
|
||||||
customer.creditLimit) *
|
|
||||||
100 >
|
|
||||||
60
|
|
||||||
? "bg-yellow-500"
|
|
||||||
: "bg-green-500"
|
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.min(
|
width: `${Math.min(
|
||||||
((customer.totalRevenue ?? 0) /
|
((customer.totalRevenue ?? 0) / customer.creditLimit) * 100,
|
||||||
customer.creditLimit) *
|
|
||||||
100,
|
100,
|
||||||
100
|
|
||||||
)}%`,
|
)}%`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
|
@ -947,9 +875,7 @@ const CustomerView: React.FC = () => {
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
Para Birimi
|
Para Birimi
|
||||||
</label>
|
</label>
|
||||||
<p className="text-lg font-medium text-gray-900">
|
<p className="text-lg font-medium text-gray-900">{customer.currency}</p>
|
||||||
{customer.currency}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -966,7 +892,7 @@ const CustomerView: React.FC = () => {
|
||||||
Son Sipariş Tarihi
|
Son Sipariş Tarihi
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">
|
||||||
{dayjs(customer.lastOrderDate).format("DD.MM.YYYY")}
|
{dayjs(customer.lastOrderDate).format('DD.MM.YYYY')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -975,7 +901,7 @@ const CustomerView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "history" && (
|
{activeTab === 'history' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
<h3 className="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
||||||
|
|
@ -988,13 +914,9 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div>
|
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-medium text-gray-900">
|
<h4 className="font-medium text-gray-900">Müşteri Kaydı Oluşturuldu</h4>
|
||||||
Müşteri Kaydı Oluşturuldu
|
|
||||||
</h4>
|
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
{dayjs(customer.creationTime).format(
|
{dayjs(customer.creationTime).format('DD.MM.YYYY HH:mm')}
|
||||||
"DD.MM.YYYY HH:mm"
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1004,18 +926,12 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full mt-2"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full mt-2"></div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-medium text-gray-900">
|
<h4 className="font-medium text-gray-900">Son Güncelleme</h4>
|
||||||
Son Güncelleme
|
|
||||||
</h4>
|
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
{dayjs(customer.lastModificationTime).format(
|
{dayjs(customer.lastModificationTime).format('DD.MM.YYYY HH:mm')}
|
||||||
"DD.MM.YYYY HH:mm"
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">Müşteri bilgileri güncellendi.</p>
|
||||||
Müşteri bilgileri güncellendi.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1024,16 +940,13 @@ const CustomerView: React.FC = () => {
|
||||||
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2"></div>
|
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2"></div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-medium text-gray-900">
|
<h4 className="font-medium text-gray-900">Son Sipariş</h4>
|
||||||
Son Sipariş
|
|
||||||
</h4>
|
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
{dayjs(customer.lastOrderDate).format("DD.MM.YYYY")}
|
{dayjs(customer.lastOrderDate).format('DD.MM.YYYY')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
Son sipariş tarihi:{" "}
|
Son sipariş tarihi: {dayjs(customer.lastOrderDate).format('DD.MM.YYYY')}
|
||||||
{dayjs(customer.lastOrderDate).format("DD.MM.YYYY")}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1044,7 +957,8 @@ const CustomerView: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default CustomerView;
|
export default CustomerView
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import { FaEdit, FaTrash, FaEye, FaBullseye } from "react-icons/fa";
|
import { FaEdit, FaTrash, FaEye, FaBullseye } from 'react-icons/fa'
|
||||||
import { CrmLostReason, CrmOpportunity } from "../../../types/crm";
|
import { CrmLostReason, CrmOpportunity } from '../../../types/crm'
|
||||||
import {
|
import { getLostReasonCategoryColor, getLostReasonCategoryText } from '../../../utils/erp'
|
||||||
getLostReasonCategoryColor,
|
|
||||||
getLostReasonCategoryText,
|
|
||||||
} from "../../../utils/erp";
|
|
||||||
|
|
||||||
interface LossReasonCardViewProps {
|
interface LossReasonCardViewProps {
|
||||||
lossReasons: CrmLostReason[];
|
lossReasons: CrmLostReason[]
|
||||||
opportunities: CrmOpportunity[];
|
opportunities: CrmOpportunity[]
|
||||||
onEdit: (reason: CrmLostReason) => void;
|
onEdit: (reason: CrmLostReason) => void
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void
|
||||||
onView: (reason: CrmLostReason) => void;
|
onView: (reason: CrmLostReason) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
|
const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
|
||||||
|
|
@ -22,15 +19,14 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
|
||||||
onView,
|
onView,
|
||||||
}) => {
|
}) => {
|
||||||
const getUsageCount = (reasonId: string) => {
|
const getUsageCount = (reasonId: string) => {
|
||||||
return opportunities.filter((opp) => opp.lostReason?.id === reasonId)
|
return opportunities.filter((opp) => opp.lostReason?.id === reasonId).length
|
||||||
.length;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const getLostValue = (reasonId: string) => {
|
const getLostValue = (reasonId: string) => {
|
||||||
return opportunities
|
return opportunities
|
||||||
.filter((opp) => opp.lostReason?.id === reasonId)
|
.filter((opp) => opp.lostReason?.id === reasonId)
|
||||||
.reduce((sum, opp) => sum + opp.estimatedValue, 0);
|
.reduce((sum, opp) => sum + opp.estimatedValue, 0)
|
||||||
};
|
}
|
||||||
|
|
||||||
if (lossReasons.length === 0) {
|
if (lossReasons.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -38,21 +34,17 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
|
||||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
<FaBullseye className="w-8 h-8 text-gray-400" />
|
<FaBullseye className="w-8 h-8 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Kayıp nedeni bulunamadı</h3>
|
||||||
Kayıp nedeni bulunamadı
|
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500">
|
|
||||||
Arama kriterlerinizi değiştirmeyi deneyin.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||||
{lossReasons.map((reason) => {
|
{lossReasons.map((reason) => {
|
||||||
const usageCount = getUsageCount(reason.id);
|
const usageCount = getUsageCount(reason.id)
|
||||||
const lostValue = getLostValue(reason.id);
|
const lostValue = getLostValue(reason.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -63,21 +55,17 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
|
||||||
<div className="p-3 border-b">
|
<div className="p-3 border-b">
|
||||||
<div className="flex items-start justify-between mb-1">
|
<div className="flex items-start justify-between mb-1">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-semibold text-gray-900 text-sm">
|
<h3 className="font-semibold text-gray-900 text-sm">{reason.name}</h3>
|
||||||
{reason.name}
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-gray-500 font-mono">
|
<span className="text-xs text-gray-500 font-mono">{reason.code}</span>
|
||||||
{reason.code}
|
|
||||||
</span>
|
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||||
reason.isActive
|
reason.isActive
|
||||||
? "bg-green-100 text-green-800"
|
? 'bg-green-100 text-green-800'
|
||||||
: "bg-gray-100 text-gray-800"
|
: 'bg-gray-100 text-gray-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{reason.isActive ? "Aktif" : "Pasif"}
|
{reason.isActive ? 'Aktif' : 'Pasif'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -86,7 +74,7 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
|
||||||
{/* Category */}
|
{/* Category */}
|
||||||
<span
|
<span
|
||||||
className={`inline-block px-2 py-1 text-xs font-medium rounded-full ${getLostReasonCategoryColor(
|
className={`inline-block px-2 py-1 text-xs font-medium rounded-full ${getLostReasonCategoryColor(
|
||||||
reason.category
|
reason.category,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getLostReasonCategoryText(reason.category)}
|
{getLostReasonCategoryText(reason.category)}
|
||||||
|
|
@ -100,9 +88,9 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
|
||||||
<p
|
<p
|
||||||
className="text-xs text-gray-600 overflow-hidden flex-grow"
|
className="text-xs text-gray-600 overflow-hidden flex-grow"
|
||||||
style={{
|
style={{
|
||||||
display: "-webkit-box",
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 2,
|
||||||
WebkitBoxOrient: "vertical",
|
WebkitBoxOrient: 'vertical',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{reason.description}
|
{reason.description}
|
||||||
|
|
@ -116,9 +104,7 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
|
||||||
<FaBullseye className="w-3 h-3 text-gray-400" />
|
<FaBullseye className="w-3 h-3 text-gray-400" />
|
||||||
<span className="text-xs text-gray-500">Kullanım</span>
|
<span className="text-xs text-gray-500">Kullanım</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-gray-900">
|
<div className="text-sm font-bold text-gray-900">{usageCount}</div>
|
||||||
{usageCount}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-xs text-gray-500 mb-1">Kayıp Değer</div>
|
<div className="text-xs text-gray-500 mb-1">Kayıp Değer</div>
|
||||||
|
|
@ -154,10 +140,10 @@ const LossReasonCardView: React.FC<LossReasonCardViewProps> = ({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LossReasonCardView;
|
export default LossReasonCardView
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react'
|
||||||
import { FaTimes, FaSave } from "react-icons/fa";
|
import { FaTimes, FaSave } from 'react-icons/fa'
|
||||||
import { CrmLostReason, LostReasonCategoryEnum } from "../../../types/crm";
|
import { CrmLostReason, LostReasonCategoryEnum } from '../../../types/crm'
|
||||||
import { getLostReasonCategoryText } from "../../../utils/erp";
|
import { getLostReasonCategoryText } from '../../../utils/erp'
|
||||||
|
|
||||||
interface LossReasonModalProps {
|
interface LossReasonModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onSave: (reason: Partial<CrmLostReason>) => void;
|
onSave: (reason: Partial<CrmLostReason>) => void
|
||||||
editingReason?: CrmLostReason | null;
|
editingReason?: CrmLostReason | null
|
||||||
mode: "add" | "edit" | "view";
|
mode: 'add' | 'edit' | 'view'
|
||||||
}
|
}
|
||||||
|
|
||||||
const LossReasonModal: React.FC<LossReasonModalProps> = ({
|
const LossReasonModal: React.FC<LossReasonModalProps> = ({
|
||||||
|
|
@ -19,102 +19,97 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
|
||||||
mode,
|
mode,
|
||||||
}) => {
|
}) => {
|
||||||
const [formData, setFormData] = useState<Partial<CrmLostReason>>({
|
const [formData, setFormData] = useState<Partial<CrmLostReason>>({
|
||||||
code: "",
|
code: '',
|
||||||
name: "",
|
name: '',
|
||||||
description: "",
|
description: '',
|
||||||
category: LostReasonCategoryEnum.Price,
|
category: LostReasonCategoryEnum.Price,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
const [errors, setErrors] = useState<{ [key: string]: string }>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingReason) {
|
if (editingReason) {
|
||||||
setFormData(editingReason);
|
setFormData(editingReason)
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
code: "",
|
code: '',
|
||||||
name: "",
|
name: '',
|
||||||
description: "",
|
description: '',
|
||||||
category: LostReasonCategoryEnum.Price,
|
category: LostReasonCategoryEnum.Price,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
setErrors({});
|
setErrors({})
|
||||||
}, [editingReason, isOpen]);
|
}, [editingReason, isOpen])
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: { [key: string]: string } = {};
|
const newErrors: { [key: string]: string } = {}
|
||||||
|
|
||||||
if (!formData.code?.trim()) {
|
if (!formData.code?.trim()) {
|
||||||
newErrors.code = "Kod zorunludur";
|
newErrors.code = 'Kod zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.name?.trim()) {
|
if (!formData.name?.trim()) {
|
||||||
newErrors.name = "Kayıp nedeni adı zorunludur";
|
newErrors.name = 'Kayıp nedeni adı zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.category) {
|
if (!formData.category) {
|
||||||
newErrors.category = "Kategori seçimi zorunludur";
|
newErrors.category = 'Kategori seçimi zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors)
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
if (mode === "view") {
|
if (mode === 'view') {
|
||||||
onClose();
|
onClose()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validateForm()) {
|
if (validateForm()) {
|
||||||
onSave(formData);
|
onSave(formData)
|
||||||
onClose();
|
onClose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
field: keyof CrmLostReason,
|
field: keyof CrmLostReason,
|
||||||
value: string | boolean | LostReasonCategoryEnum
|
value: string | boolean | LostReasonCategoryEnum,
|
||||||
) => {
|
) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors((prev) => ({ ...prev, [field]: "" }));
|
setErrors((prev) => ({ ...prev, [field]: '' }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null
|
||||||
|
|
||||||
const getModalTitle = () => {
|
const getModalTitle = () => {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "add":
|
case 'add':
|
||||||
return "Yeni Kayıp Nedeni";
|
return 'Yeni Kayıp Nedeni'
|
||||||
case "edit":
|
case 'edit':
|
||||||
return "Kayıp Nedeni Düzenle";
|
return 'Kayıp Nedeni Düzenle'
|
||||||
case "view":
|
case 'view':
|
||||||
return "Kayıp Nedeni Detayları";
|
return 'Kayıp Nedeni Detayları'
|
||||||
default:
|
default:
|
||||||
return "";
|
return ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const isReadOnly = mode === "view";
|
const isReadOnly = mode === 'view'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-3 border-b">
|
<div className="flex items-center justify-between p-3 border-b">
|
||||||
<h3 className="text-sm font-semibold text-gray-900">
|
<h3 className="text-sm font-semibold text-gray-900">{getModalTitle()}</h3>
|
||||||
{getModalTitle()}
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5" />
|
<FaTimes className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -124,60 +119,47 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* Code */}
|
{/* Code */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Kod *</label>
|
||||||
Kod *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.code || ""}
|
value={formData.code || ''}
|
||||||
onChange={(e) => handleInputChange("code", e.target.value)}
|
onChange={(e) => handleInputChange('code', e.target.value)}
|
||||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.code ? "border-red-500" : "border-gray-300"
|
errors.code ? 'border-red-500' : 'border-gray-300'
|
||||||
} ${isReadOnly ? "bg-gray-100" : ""}`}
|
} ${isReadOnly ? 'bg-gray-100' : ''}`}
|
||||||
placeholder="Örn: LR001"
|
placeholder="Örn: LR001"
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
/>
|
/>
|
||||||
{errors.code && (
|
{errors.code && <p className="text-red-500 text-sm mt-1">{errors.code}</p>}
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.code}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Kayıp Nedeni *</label>
|
||||||
Kayıp Nedeni *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name || ""}
|
value={formData.name || ''}
|
||||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.name ? "border-red-500" : "border-gray-300"
|
errors.name ? 'border-red-500' : 'border-gray-300'
|
||||||
} ${isReadOnly ? "bg-gray-100" : ""}`}
|
} ${isReadOnly ? 'bg-gray-100' : ''}`}
|
||||||
placeholder="Kayıp nedeni adını girin"
|
placeholder="Kayıp nedeni adını girin"
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
/>
|
/>
|
||||||
{errors.name && (
|
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category */}
|
{/* Category */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Kategori *</label>
|
||||||
Kategori *
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={formData.category || ""}
|
value={formData.category || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('category', e.target.value as LostReasonCategoryEnum)
|
||||||
"category",
|
|
||||||
e.target.value as LostReasonCategoryEnum
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.category ? "border-red-500" : "border-gray-300"
|
errors.category ? 'border-red-500' : 'border-gray-300'
|
||||||
} ${isReadOnly ? "bg-gray-100" : ""}`}
|
} ${isReadOnly ? 'bg-gray-100' : ''}`}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
>
|
>
|
||||||
{Object.values(LostReasonCategoryEnum).map((category) => (
|
{Object.values(LostReasonCategoryEnum).map((category) => (
|
||||||
|
|
@ -186,24 +168,18 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{errors.category && (
|
{errors.category && <p className="text-red-500 text-sm mt-1">{errors.category}</p>}
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.category}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Açıklama</label>
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.description || ""}
|
value={formData.description || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||||
handleInputChange("description", e.target.value)
|
|
||||||
}
|
|
||||||
rows={2}
|
rows={2}
|
||||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
isReadOnly ? "bg-gray-100" : "border-gray-300"
|
isReadOnly ? 'bg-gray-100' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Kayıp nedeni hakkında detaylı açıklama"
|
placeholder="Kayıp nedeni hakkında detaylı açıklama"
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
|
|
@ -216,16 +192,11 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="isActive"
|
id="isActive"
|
||||||
checked={formData.isActive || false}
|
checked={formData.isActive || false}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('isActive', e.target.checked)}
|
||||||
handleInputChange("isActive", e.target.checked)
|
|
||||||
}
|
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor="isActive" className="text-sm font-medium text-gray-700">
|
||||||
htmlFor="isActive"
|
|
||||||
className="text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
Aktif
|
Aktif
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -238,9 +209,9 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||||
>
|
>
|
||||||
{mode === "view" ? "Kapat" : "İptal"}
|
{mode === 'view' ? 'Kapat' : 'İptal'}
|
||||||
</button>
|
</button>
|
||||||
{mode !== "view" && (
|
{mode !== 'view' && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
|
@ -253,7 +224,7 @@ const LossReasonModal: React.FC<LossReasonModalProps> = ({
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LossReasonModal;
|
export default LossReasonModal
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
FaPlus,
|
FaPlus,
|
||||||
FaEdit,
|
FaEdit,
|
||||||
|
|
@ -9,104 +9,86 @@ import {
|
||||||
FaTh,
|
FaTh,
|
||||||
FaList,
|
FaList,
|
||||||
FaTimes,
|
FaTimes,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import {
|
import { CrmLostReason, LostReasonCategoryEnum, CrmOpportunity } from '../../../types/crm'
|
||||||
CrmLostReason,
|
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||||
LostReasonCategoryEnum,
|
import LossReasonModal from './LossReasonModal'
|
||||||
CrmOpportunity,
|
import LossReasonCardView from './LossReasonCardView'
|
||||||
} from "../../../types/crm";
|
import { mockLossReasons } from '../../../mocks/mockLossReasons'
|
||||||
import DataTable, { Column } from "../../../components/common/DataTable";
|
import { mockOpportunities } from '../../../mocks/mockOpportunities'
|
||||||
import LossReasonModal from "./LossReasonModal";
|
import Widget from '../../../components/common/Widget'
|
||||||
import LossReasonCardView from "./LossReasonCardView";
|
import { getLostReasonCategoryColor, getLostReasonCategoryText } from '../../../utils/erp'
|
||||||
import { mockLossReasons } from "../../../mocks/mockLossReasons";
|
import { Container } from '@/components/shared'
|
||||||
import { mockOpportunities } from "../../../mocks/mockOpportunities";
|
|
||||||
import Widget from "../../../components/common/Widget";
|
|
||||||
import {
|
|
||||||
getLostReasonCategoryColor,
|
|
||||||
getLostReasonCategoryText,
|
|
||||||
} from "../../../utils/erp";
|
|
||||||
|
|
||||||
const LossReasons: React.FC = () => {
|
const LossReasons: React.FC = () => {
|
||||||
const [lossReasons, setLossReasons] =
|
const [lossReasons, setLossReasons] = useState<CrmLostReason[]>(mockLossReasons)
|
||||||
useState<CrmLostReason[]>(mockLossReasons);
|
const [opportunities] = useState<CrmOpportunity[]>(mockOpportunities)
|
||||||
const [opportunities] = useState<CrmOpportunity[]>(mockOpportunities);
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [selectedCategory, setSelectedCategory] = useState<LostReasonCategoryEnum | 'all'>('all')
|
||||||
const [selectedCategory, setSelectedCategory] = useState<
|
const [viewMode, setViewMode] = useState<'table' | 'card'>('table')
|
||||||
LostReasonCategoryEnum | "all"
|
|
||||||
>("all");
|
|
||||||
const [viewMode, setViewMode] = useState<"table" | "card">("table");
|
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [modalMode, setModalMode] = useState<"add" | "edit" | "view">("add");
|
const [modalMode, setModalMode] = useState<'add' | 'edit' | 'view'>('add')
|
||||||
const [editingReason, setEditingReason] = useState<CrmLostReason | null>(
|
const [editingReason, setEditingReason] = useState<CrmLostReason | null>(null)
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete modal states
|
// Delete modal states
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||||
const [reasonToDelete, setReasonToDelete] = useState<CrmLostReason | null>(
|
const [reasonToDelete, setReasonToDelete] = useState<CrmLostReason | null>(null)
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
setModalMode("add");
|
setModalMode('add')
|
||||||
setEditingReason(null);
|
setEditingReason(null)
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEdit = (reason: CrmLostReason) => {
|
const handleEdit = (reason: CrmLostReason) => {
|
||||||
setModalMode("edit");
|
setModalMode('edit')
|
||||||
setEditingReason(reason);
|
setEditingReason(reason)
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleView = (reason: CrmLostReason) => {
|
const handleView = (reason: CrmLostReason) => {
|
||||||
setModalMode("view");
|
setModalMode('view')
|
||||||
setEditingReason(reason);
|
setEditingReason(reason)
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
const reason = lossReasons.find((r) => r.id === id);
|
const reason = lossReasons.find((r) => r.id === id)
|
||||||
if (reason) {
|
if (reason) {
|
||||||
setReasonToDelete(reason);
|
setReasonToDelete(reason)
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = (reasonData: Partial<CrmLostReason>) => {
|
const handleSave = (reasonData: Partial<CrmLostReason>) => {
|
||||||
if (modalMode === "add") {
|
if (modalMode === 'add') {
|
||||||
const newReason: CrmLostReason = {
|
const newReason: CrmLostReason = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
code: reasonData.code || "",
|
code: reasonData.code || '',
|
||||||
name: reasonData.name || "",
|
name: reasonData.name || '',
|
||||||
description: reasonData.description || "",
|
description: reasonData.description || '',
|
||||||
category: reasonData.category || LostReasonCategoryEnum.Price,
|
category: reasonData.category || LostReasonCategoryEnum.Price,
|
||||||
isActive:
|
isActive: reasonData.isActive !== undefined ? reasonData.isActive : true,
|
||||||
reasonData.isActive !== undefined ? reasonData.isActive : true,
|
}
|
||||||
};
|
setLossReasons((prev) => [...prev, newReason])
|
||||||
setLossReasons((prev) => [...prev, newReason]);
|
} else if (modalMode === 'edit' && editingReason) {
|
||||||
} else if (modalMode === "edit" && editingReason) {
|
|
||||||
setLossReasons((prev) =>
|
setLossReasons((prev) =>
|
||||||
prev.map((reason) =>
|
prev.map((reason) =>
|
||||||
reason.id === editingReason.id
|
reason.id === editingReason.id ? ({ ...reason, ...reasonData } as CrmLostReason) : reason,
|
||||||
? ({ ...reason, ...reasonData } as CrmLostReason)
|
),
|
||||||
: reason
|
|
||||||
)
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (reasonToDelete) {
|
if (reasonToDelete) {
|
||||||
setLossReasons((prev) =>
|
setLossReasons((prev) => prev.filter((reason) => reason.id !== reasonToDelete.id))
|
||||||
prev.filter((reason) => reason.id !== reasonToDelete.id)
|
setIsDeleteModalOpen(false)
|
||||||
);
|
setReasonToDelete(null)
|
||||||
setIsDeleteModalOpen(false);
|
}
|
||||||
setReasonToDelete(null);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const filteredReasons = lossReasons.filter((reason) => {
|
const filteredReasons = lossReasons.filter((reason) => {
|
||||||
if (
|
if (
|
||||||
|
|
@ -114,26 +96,26 @@ const LossReasons: React.FC = () => {
|
||||||
!reason.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
!reason.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||||
!reason.code.toLowerCase().includes(searchTerm.toLowerCase())
|
!reason.code.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
) {
|
) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if (selectedCategory !== "all" && reason.category !== selectedCategory) {
|
if (selectedCategory !== 'all' && reason.category !== selectedCategory) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
return true;
|
return true
|
||||||
});
|
})
|
||||||
|
|
||||||
const columns: Column<CrmLostReason>[] = [
|
const columns: Column<CrmLostReason>[] = [
|
||||||
{
|
{
|
||||||
key: "code",
|
key: 'code',
|
||||||
header: "Kod",
|
header: 'Kod',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (reason: CrmLostReason) => (
|
render: (reason: CrmLostReason) => (
|
||||||
<div className="font-medium text-gray-900">{reason.code}</div>
|
<div className="font-medium text-gray-900">{reason.code}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "name",
|
key: 'name',
|
||||||
header: "Kayıp Nedeni",
|
header: 'Kayıp Nedeni',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (reason: CrmLostReason) => (
|
render: (reason: CrmLostReason) => (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -143,12 +125,12 @@ const LossReasons: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "category",
|
key: 'category',
|
||||||
header: "Kategori",
|
header: 'Kategori',
|
||||||
render: (reason: CrmLostReason) => (
|
render: (reason: CrmLostReason) => (
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getLostReasonCategoryColor(
|
className={`px-2 py-1 text-xs font-medium rounded-full ${getLostReasonCategoryColor(
|
||||||
reason.category
|
reason.category,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getLostReasonCategoryText(reason.category)}
|
{getLostReasonCategoryText(reason.category)}
|
||||||
|
|
@ -156,38 +138,34 @@ const LossReasons: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "usage",
|
key: 'usage',
|
||||||
header: "Kullanım",
|
header: 'Kullanım',
|
||||||
render: (reason: CrmLostReason) => {
|
render: (reason: CrmLostReason) => {
|
||||||
const usageCount = opportunities.filter(
|
const usageCount = opportunities.filter((opp) => opp.lostReason?.id === reason.id).length
|
||||||
(opp) => opp.lostReason?.id === reason.id
|
|
||||||
).length;
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FaBullseye className="w-4 h-4 text-gray-400" />
|
<FaBullseye className="w-4 h-4 text-gray-400" />
|
||||||
<span>{usageCount} Fırsat</span>
|
<span>{usageCount} Fırsat</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: 'status',
|
||||||
header: "Durum",
|
header: 'Durum',
|
||||||
render: (reason: CrmLostReason) => (
|
render: (reason: CrmLostReason) => (
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||||
reason.isActive
|
reason.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: "bg-gray-100 text-gray-800"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{reason.isActive ? "Aktif" : "Pasif"}
|
{reason.isActive ? 'Aktif' : 'Pasif'}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: 'actions',
|
||||||
header: "İşlemler",
|
header: 'İşlemler',
|
||||||
render: (reason: CrmLostReason) => (
|
render: (reason: CrmLostReason) => (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -214,48 +192,41 @@ const LossReasons: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
// Calculate statistics
|
// Calculate statistics
|
||||||
const totalReasons = lossReasons.length;
|
const totalReasons = lossReasons.length
|
||||||
const activeReasons = lossReasons.filter((r) => r.isActive).length;
|
const activeReasons = lossReasons.filter((r) => r.isActive).length
|
||||||
const categoryDistribution = Object.values(LostReasonCategoryEnum).map(
|
const categoryDistribution = Object.values(LostReasonCategoryEnum).map((category) => ({
|
||||||
(category) => ({
|
|
||||||
category,
|
category,
|
||||||
count: lossReasons.filter((r) => r.category === category).length,
|
count: lossReasons.filter((r) => r.category === category).length,
|
||||||
usage: opportunities.filter(
|
usage: opportunities.filter(
|
||||||
(opp) =>
|
(opp) =>
|
||||||
opp.lostReason &&
|
opp.lostReason &&
|
||||||
lossReasons.find(
|
lossReasons.find((r) => r.id === opp.lostReason?.id && r.category === category),
|
||||||
(r) => r.id === opp.lostReason?.id && r.category === category
|
|
||||||
)
|
|
||||||
).length,
|
).length,
|
||||||
})
|
}))
|
||||||
);
|
|
||||||
|
|
||||||
// Top loss reasons by usage
|
// Top loss reasons by usage
|
||||||
const topLossReasons = lossReasons
|
const topLossReasons = lossReasons
|
||||||
.map((reason) => ({
|
.map((reason) => ({
|
||||||
...reason,
|
...reason,
|
||||||
usageCount: opportunities.filter(
|
usageCount: opportunities.filter((opp) => opp.lostReason?.id === reason.id).length,
|
||||||
(opp) => opp.lostReason?.id === reason.id
|
|
||||||
).length,
|
|
||||||
lostValue: opportunities
|
lostValue: opportunities
|
||||||
.filter((opp) => opp.lostReason?.id === reason.id)
|
.filter((opp) => opp.lostReason?.id === reason.id)
|
||||||
.reduce((sum, opp) => sum + opp.estimatedValue, 0),
|
.reduce((sum, opp) => sum + opp.estimatedValue, 0),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.usageCount - a.usageCount)
|
.sort((a, b) => b.usageCount - a.usageCount)
|
||||||
.slice(0, 5);
|
.slice(0, 5)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold text-gray-900">Kayıp Nedenleri</h2>
|
<h2 className="text-lg font-bold text-gray-900">Kayıp Nedenleri</h2>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">Fırsat kaybı nedenlerini analiz edin</p>
|
||||||
Fırsat kaybı nedenlerini analiz edin
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
|
|
@ -277,12 +248,7 @@ const LossReasons: React.FC = () => {
|
||||||
icon="FaExclamationTriangle"
|
icon="FaExclamationTriangle"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Widget
|
<Widget title="Aktif Neden" value={activeReasons} color="green" icon="FaArrowDown" />
|
||||||
title="Aktif Neden"
|
|
||||||
value={activeReasons}
|
|
||||||
color="green"
|
|
||||||
icon="FaArrowDown"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget
|
||||||
title="Kayıp Fırsat"
|
title="Kayıp Fırsat"
|
||||||
|
|
@ -293,7 +259,7 @@ const LossReasons: React.FC = () => {
|
||||||
|
|
||||||
<Widget
|
<Widget
|
||||||
title="En Çok Kullanılan"
|
title="En Çok Kullanılan"
|
||||||
value={topLossReasons[0]?.name || "-"}
|
value={topLossReasons[0]?.name || '-'}
|
||||||
color="purple"
|
color="purple"
|
||||||
icon="FaChartBar"
|
icon="FaChartBar"
|
||||||
valueClassName="text-lg"
|
valueClassName="text-lg"
|
||||||
|
|
@ -302,22 +268,18 @@ const LossReasons: React.FC = () => {
|
||||||
|
|
||||||
{/* Category Distribution */}
|
{/* Category Distribution */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3">
|
<h3 className="text-base font-semibold text-gray-900 mb-3">Kategori Dağılımı</h3>
|
||||||
Kategori Dağılımı
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
|
||||||
{categoryDistribution.map(({ category, count, usage }) => (
|
{categoryDistribution.map(({ category, count, usage }) => (
|
||||||
<div key={category} className="text-center p-4 border rounded-lg">
|
<div key={category} className="text-center p-4 border rounded-lg">
|
||||||
<div
|
<div
|
||||||
className={`inline-block px-2 py-1 text-xs font-medium rounded-full mb-2 ${getLostReasonCategoryColor(
|
className={`inline-block px-2 py-1 text-xs font-medium rounded-full mb-2 ${getLostReasonCategoryColor(
|
||||||
category
|
category,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getLostReasonCategoryText(category)}
|
{getLostReasonCategoryText(category)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold text-gray-900 mb-1">
|
<div className="text-lg font-bold text-gray-900 mb-1">{count}</div>
|
||||||
{count}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500">{usage} Kullanım</div>
|
<div className="text-xs text-gray-500">{usage} Kullanım</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -342,8 +304,7 @@ const LossReasons: React.FC = () => {
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900">{reason.name}</h4>
|
<h4 className="font-medium text-gray-900">{reason.name}</h4>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{getLostReasonCategoryText(reason.category)} •{" "}
|
{getLostReasonCategoryText(reason.category)} • {reason.usageCount} fırsat
|
||||||
{reason.usageCount} fırsat
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -372,11 +333,7 @@ const LossReasons: React.FC = () => {
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={(e) =>
|
onChange={(e) => setSelectedCategory(e.target.value as LostReasonCategoryEnum | 'all')}
|
||||||
setSelectedCategory(
|
|
||||||
e.target.value as LostReasonCategoryEnum | "all"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">Tüm Kategoriler</option>
|
<option value="all">Tüm Kategoriler</option>
|
||||||
|
|
@ -390,21 +347,21 @@ const LossReasons: React.FC = () => {
|
||||||
{/* View Mode Toggle */}
|
{/* View Mode Toggle */}
|
||||||
<div className="flex bg-gray-100 rounded-lg p-1">
|
<div className="flex bg-gray-100 rounded-lg p-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("table")}
|
onClick={() => setViewMode('table')}
|
||||||
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
|
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
|
||||||
viewMode === "table"
|
viewMode === 'table'
|
||||||
? "bg-white text-gray-900 shadow-sm"
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaList className="w-4 h-4" />
|
<FaList className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("card")}
|
onClick={() => setViewMode('card')}
|
||||||
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
|
className={`flex items-center gap-2 px-2 py-1 rounded-md text-sm transition-colors ${
|
||||||
viewMode === "card"
|
viewMode === 'card'
|
||||||
? "bg-white text-gray-900 shadow-sm"
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaTh className="w-4 h-4" />
|
<FaTh className="w-4 h-4" />
|
||||||
|
|
@ -413,7 +370,7 @@ const LossReasons: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Display */}
|
{/* Data Display */}
|
||||||
{viewMode === "table" ? (
|
{viewMode === 'table' ? (
|
||||||
<div className="bg-white rounded-lg shadow-sm border">
|
<div className="bg-white rounded-lg shadow-sm border">
|
||||||
<DataTable data={filteredReasons} columns={columns} />
|
<DataTable data={filteredReasons} columns={columns} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -427,15 +384,11 @@ const LossReasons: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filteredReasons.length === 0 && viewMode === "table" && (
|
{filteredReasons.length === 0 && viewMode === 'table' && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaExclamationTriangle className="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
<FaExclamationTriangle className="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
||||||
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
<h3 className="text-sm font-medium text-gray-900 mb-2">Kayıp nedeni bulunamadı</h3>
|
||||||
Kayıp nedeni bulunamadı
|
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500">
|
|
||||||
Arama kriterlerinizi değiştirmeyi deneyin.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -443,8 +396,8 @@ const LossReasons: React.FC = () => {
|
||||||
<LossReasonModal
|
<LossReasonModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false)
|
||||||
setEditingReason(null);
|
setEditingReason(null)
|
||||||
}}
|
}}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
editingReason={editingReason}
|
editingReason={editingReason}
|
||||||
|
|
@ -459,14 +412,12 @@ const LossReasons: React.FC = () => {
|
||||||
<div className="flex items-center justify-between p-3 border-b">
|
<div className="flex items-center justify-between p-3 border-b">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<FaExclamationTriangle className="w-6 h-6 text-red-600" />
|
<FaExclamationTriangle className="w-6 h-6 text-red-600" />
|
||||||
<h3 className="text-sm font-semibold text-gray-900">
|
<h3 className="text-sm font-semibold text-gray-900">Silme Onayı</h3>
|
||||||
Silme Onayı
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false)
|
||||||
setReasonToDelete(null);
|
setReasonToDelete(null)
|
||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-gray-400 hover:text-gray-600"
|
||||||
>
|
>
|
||||||
|
|
@ -484,8 +435,8 @@ const LossReasons: React.FC = () => {
|
||||||
Kayıp nedeni silinsin mi?
|
Kayıp nedeni silinsin mi?
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500 mb-6">
|
<p className="text-sm text-gray-500 mb-6">
|
||||||
<span className="font-medium">"{reasonToDelete?.name}"</span>{" "}
|
<span className="font-medium">"{reasonToDelete?.name}"</span> kayıp nedenini
|
||||||
kayıp nedenini silmek üzeresiniz. Bu işlem geri alınamaz.
|
silmek üzeresiniz. Bu işlem geri alınamaz.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -493,8 +444,8 @@ const LossReasons: React.FC = () => {
|
||||||
<div className="flex justify-end gap-1.5">
|
<div className="flex justify-end gap-1.5">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false)
|
||||||
setReasonToDelete(null);
|
setReasonToDelete(null)
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||||
>
|
>
|
||||||
|
|
@ -513,7 +464,8 @@ const LossReasons: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default LossReasons;
|
export default LossReasons
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
FaTimes,
|
FaTimes,
|
||||||
FaBullseye,
|
FaBullseye,
|
||||||
|
|
@ -13,21 +13,21 @@ import {
|
||||||
FaClock,
|
FaClock,
|
||||||
FaPhone,
|
FaPhone,
|
||||||
FaEnvelope,
|
FaEnvelope,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import { CrmOpportunity, OpportunityStageEnum } from "../../../types/crm";
|
import { CrmOpportunity, OpportunityStageEnum } from '../../../types/crm'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import {
|
import {
|
||||||
getOpportunityLeadSourceText,
|
getOpportunityLeadSourceText,
|
||||||
getOpportunityProbabilityColor,
|
getOpportunityProbabilityColor,
|
||||||
getOpportunityStageColor,
|
getOpportunityStageColor,
|
||||||
getOpportunityStageText,
|
getOpportunityStageText,
|
||||||
} from "../../../utils/erp";
|
} from '../../../utils/erp'
|
||||||
|
|
||||||
interface OpportunityDetailsProps {
|
interface OpportunityDetailsProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onEdit: (opportunity: CrmOpportunity) => void;
|
onEdit: (opportunity: CrmOpportunity) => void
|
||||||
opportunity: CrmOpportunity | null;
|
opportunity: CrmOpportunity | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
|
|
@ -36,11 +36,9 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
onEdit,
|
onEdit,
|
||||||
opportunity,
|
opportunity,
|
||||||
}) => {
|
}) => {
|
||||||
if (!isOpen || !opportunity) return null;
|
if (!isOpen || !opportunity) return null
|
||||||
|
|
||||||
const customer = mockBusinessParties.find(
|
const customer = mockBusinessParties.find((c) => c.id === opportunity.customerId)
|
||||||
(c) => c.id === opportunity.customerId
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
|
@ -52,12 +50,8 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<FaBullseye className="w-6 h-6 text-blue-600" />
|
<FaBullseye className="w-6 h-6 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
<h2 className="text-lg font-semibold text-gray-900">{opportunity.title}</h2>
|
||||||
{opportunity.title}
|
<p className="text-sm text-gray-600">{opportunity.opportunityNumber}</p>
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{opportunity.opportunityNumber}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
@ -68,10 +62,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<FaEdit className="w-4 h-4" />
|
<FaEdit className="w-4 h-4" />
|
||||||
Düzenle
|
Düzenle
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-2">
|
||||||
onClick={onClose}
|
|
||||||
className="text-gray-400 hover:text-gray-600 p-2"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5" />
|
<FaTimes className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,7 +74,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div
|
||||||
className={`inline-flex px-3 py-1.5 rounded-full text-xs font-medium border ${getOpportunityStageColor(
|
className={`inline-flex px-3 py-1.5 rounded-full text-xs font-medium border ${getOpportunityStageColor(
|
||||||
opportunity.stage
|
opportunity.stage,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getOpportunityStageText(opportunity.stage)}
|
{getOpportunityStageText(opportunity.stage)}
|
||||||
|
|
@ -102,7 +93,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-center gap-2 text-xl font-bold ${getOpportunityProbabilityColor(
|
className={`flex items-center justify-center gap-2 text-xl font-bold ${getOpportunityProbabilityColor(
|
||||||
opportunity.probability
|
opportunity.probability,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
<FaPercentage className="w-5 h-5" />
|
<FaPercentage className="w-5 h-5" />
|
||||||
|
|
@ -114,9 +105,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="flex items-center justify-center gap-2 text-base font-semibold text-gray-900">
|
<div className="flex items-center justify-center gap-2 text-base font-semibold text-gray-900">
|
||||||
<FaCalendar className="w-4 h-4 text-gray-400" />
|
<FaCalendar className="w-4 h-4 text-gray-400" />
|
||||||
{new Date(opportunity.expectedCloseDate).toLocaleDateString(
|
{new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">Beklenen Kapanış</p>
|
<p className="text-xs text-gray-500 mt-1">Beklenen Kapanış</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -133,9 +122,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">Başlık</label>
|
||||||
Başlık
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-900">{opportunity.title}</p>
|
<p className="text-gray-900">{opportunity.title}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -144,16 +131,12 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
Açıklama
|
Açıklama
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900 whitespace-pre-wrap">
|
<p className="text-gray-900 whitespace-pre-wrap">{opportunity.description}</p>
|
||||||
{opportunity.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">Kaynak</label>
|
||||||
Kaynak
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaTags className="w-4 h-4 text-gray-400" />
|
<FaTags className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">
|
||||||
|
|
@ -209,16 +192,12 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaEnvelope className="w-4 h-4 text-gray-400" />
|
<FaEnvelope className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">{customer.primaryContact?.email}</span>
|
||||||
{customer.primaryContact?.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{customer.primaryContact?.phone && (
|
{customer.primaryContact?.phone && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaPhone className="w-4 h-4 text-gray-400" />
|
<FaPhone className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">{customer.primaryContact?.phone}</span>
|
||||||
{customer.primaryContact?.phone}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -238,14 +217,10 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className="block text-sm font-medium text-gray-600 mb-1">Sorumlu</label>
|
||||||
Sorumlu
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaUser className="w-4 h-4 text-gray-400" />
|
<FaUser className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">{opportunity.assigned?.fullName}</span>
|
||||||
{opportunity.assigned?.fullName}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -256,9 +231,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaCalendar className="w-4 h-4 text-gray-400" />
|
<FaCalendar className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">
|
||||||
{new Date(
|
{new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}
|
||||||
opportunity.expectedCloseDate
|
|
||||||
).toLocaleDateString("tr-TR")}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -271,9 +244,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaCalendar className="w-4 h-4 text-gray-400" />
|
<FaCalendar className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">
|
||||||
{new Date(
|
{new Date(opportunity.actualCloseDate).toLocaleDateString('tr-TR')}
|
||||||
opportunity.actualCloseDate
|
|
||||||
).toLocaleDateString("tr-TR")}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -291,14 +262,10 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<div className="flex items-start gap-3 border-l-2 border-blue-200 pl-4">
|
<div className="flex items-start gap-3 border-l-2 border-blue-200 pl-4">
|
||||||
<div className="w-3 h-3 bg-blue-600 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
|
<div className="w-3 h-3 bg-blue-600 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-900">
|
<p className="text-sm font-medium text-gray-900">Fırsat Oluşturuldu</p>
|
||||||
Fırsat Oluşturuldu
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||||
<FaClock className="w-3 h-3" />
|
<FaClock className="w-3 h-3" />
|
||||||
{new Date(opportunity.creationTime).toLocaleDateString(
|
{new Date(opportunity.creationTime).toLocaleDateString('tr-TR')}
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -306,14 +273,10 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
<div className="flex items-start gap-3 border-l-2 border-gray-200 pl-4">
|
<div className="flex items-start gap-3 border-l-2 border-gray-200 pl-4">
|
||||||
<div className="w-3 h-3 bg-gray-400 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
|
<div className="w-3 h-3 bg-gray-400 rounded-full mt-1.5 -ml-6 border-2 border-white"></div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-900">
|
<p className="text-sm font-medium text-gray-900">Son Güncelleme</p>
|
||||||
Son Güncelleme
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||||
<FaClock className="w-3 h-3" />
|
<FaClock className="w-3 h-3" />
|
||||||
{new Date(
|
{new Date(opportunity.lastModificationTime).toLocaleDateString('tr-TR')}
|
||||||
opportunity.lastModificationTime
|
|
||||||
).toLocaleDateString("tr-TR")}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -322,41 +285,37 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
|
|
||||||
{/* Progress Indicator */}
|
{/* Progress Indicator */}
|
||||||
<div className="bg-gray-50 rounded-lg p-4">
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3">
|
<h3 className="text-base font-semibold text-gray-900 mb-3">Aşama İlerlemesi</h3>
|
||||||
Aşama İlerlemesi
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.values(OpportunityStageEnum).map((stage, index) => {
|
{Object.values(OpportunityStageEnum).map((stage, index) => {
|
||||||
const isCurrentStage = stage === opportunity.stage;
|
const isCurrentStage = stage === opportunity.stage
|
||||||
const isPassedStage =
|
const isPassedStage =
|
||||||
Object.values(OpportunityStageEnum).indexOf(
|
Object.values(OpportunityStageEnum).indexOf(opportunity.stage) > index
|
||||||
opportunity.stage
|
|
||||||
) > index;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={stage} className="flex items-center gap-3">
|
<div key={stage} className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className={`w-4 h-4 rounded-full border-2 ${
|
className={`w-4 h-4 rounded-full border-2 ${
|
||||||
isCurrentStage
|
isCurrentStage
|
||||||
? "bg-blue-600 border-blue-600"
|
? 'bg-blue-600 border-blue-600'
|
||||||
: isPassedStage
|
: isPassedStage
|
||||||
? "bg-green-600 border-green-600"
|
? 'bg-green-600 border-green-600'
|
||||||
: "bg-white border-gray-300"
|
: 'bg-white border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
<span
|
<span
|
||||||
className={`text-sm ${
|
className={`text-sm ${
|
||||||
isCurrentStage
|
isCurrentStage
|
||||||
? "font-semibold text-blue-600"
|
? 'font-semibold text-blue-600'
|
||||||
: isPassedStage
|
: isPassedStage
|
||||||
? "text-green-600"
|
? 'text-green-600'
|
||||||
: "text-gray-500"
|
: 'text-gray-500'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{getOpportunityStageText(stage)}
|
{getOpportunityStageText(stage)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -365,7 +324,7 @@ const OpportunityDetails: React.FC<OpportunityDetailsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default OpportunityDetails;
|
export default OpportunityDetails
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react'
|
||||||
import { FaTimes, FaDollarSign, FaFileAlt } from "react-icons/fa";
|
import { FaTimes, FaDollarSign, FaFileAlt } from 'react-icons/fa'
|
||||||
import {
|
import {
|
||||||
CrmOpportunity,
|
CrmOpportunity,
|
||||||
OpportunityStageEnum,
|
OpportunityStageEnum,
|
||||||
LeadSourceEnum,
|
LeadSourceEnum,
|
||||||
OpportunityStatusEnum,
|
OpportunityStatusEnum,
|
||||||
} from "../../../types/crm";
|
} from '../../../types/crm'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import { mockEmployees } from "../../../mocks/mockEmployees";
|
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||||
import { BusinessParty } from "../../../types/common";
|
import { BusinessParty } from '../../../types/common'
|
||||||
|
import { getOpportunityLeadSourceText, getOpportunityStageText } from '@/utils/erp'
|
||||||
|
|
||||||
interface OpportunityFormProps {
|
interface OpportunityFormProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onSave: (opportunity: CrmOpportunity) => void;
|
onSave: (opportunity: CrmOpportunity) => void
|
||||||
opportunity?: CrmOpportunity | null;
|
opportunity?: CrmOpportunity | null
|
||||||
mode: "create" | "edit";
|
mode: 'create' | 'edit'
|
||||||
}
|
}
|
||||||
|
|
||||||
const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
|
|
@ -26,122 +27,118 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
mode,
|
mode,
|
||||||
}) => {
|
}) => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
id: "",
|
id: '',
|
||||||
opportunityNumber: "",
|
opportunityNumber: '',
|
||||||
title: "",
|
title: '',
|
||||||
description: "",
|
description: '',
|
||||||
customerId: "",
|
customerId: '',
|
||||||
contactId: "",
|
contactId: '',
|
||||||
stage: OpportunityStageEnum.Qualification,
|
stage: OpportunityStageEnum.Qualification,
|
||||||
probability: 10,
|
probability: 10,
|
||||||
estimatedValue: 0,
|
estimatedValue: 0,
|
||||||
currency: "TRY",
|
currency: 'TRY',
|
||||||
expectedCloseDate: "",
|
expectedCloseDate: '',
|
||||||
assignedTo: "",
|
assignedTo: '',
|
||||||
teamId: "",
|
teamId: '',
|
||||||
leadSource: LeadSourceEnum.Website,
|
leadSource: LeadSourceEnum.Website,
|
||||||
campaignId: "",
|
campaignId: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const [customers] = useState<BusinessParty[]>(mockBusinessParties);
|
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opportunity && mode === "edit") {
|
if (opportunity && mode === 'edit') {
|
||||||
setFormData({
|
setFormData({
|
||||||
id: opportunity.id,
|
id: opportunity.id,
|
||||||
opportunityNumber: opportunity.opportunityNumber,
|
opportunityNumber: opportunity.opportunityNumber,
|
||||||
title: opportunity.title,
|
title: opportunity.title,
|
||||||
description: opportunity.description || "",
|
description: opportunity.description || '',
|
||||||
customerId: opportunity.customerId,
|
customerId: opportunity.customerId,
|
||||||
contactId: opportunity.contactId || "",
|
contactId: opportunity.contactId || '',
|
||||||
stage: opportunity.stage,
|
stage: opportunity.stage,
|
||||||
probability: opportunity.probability,
|
probability: opportunity.probability,
|
||||||
estimatedValue: opportunity.estimatedValue,
|
estimatedValue: opportunity.estimatedValue,
|
||||||
currency: opportunity.currency,
|
currency: opportunity.currency,
|
||||||
expectedCloseDate: opportunity.expectedCloseDate
|
expectedCloseDate: opportunity.expectedCloseDate.toISOString().split('T')[0],
|
||||||
.toISOString()
|
|
||||||
.split("T")[0],
|
|
||||||
assignedTo: opportunity.assignedTo,
|
assignedTo: opportunity.assignedTo,
|
||||||
teamId: opportunity.teamId || "",
|
teamId: opportunity.teamId || '',
|
||||||
leadSource: opportunity.leadSource,
|
leadSource: opportunity.leadSource,
|
||||||
campaignId: opportunity.campaignId || "",
|
campaignId: opportunity.campaignId || '',
|
||||||
});
|
})
|
||||||
} else if (mode === "create") {
|
} else if (mode === 'create') {
|
||||||
// Generate new opportunity number
|
// Generate new opportunity number
|
||||||
const newNumber = `OPP-${Date.now().toString().slice(-6)}`;
|
const newNumber = `OPP-${Date.now().toString().slice(-6)}`
|
||||||
setFormData({
|
setFormData({
|
||||||
id: `opp_${Date.now()}`,
|
id: `opp_${Date.now()}`,
|
||||||
opportunityNumber: newNumber,
|
opportunityNumber: newNumber,
|
||||||
title: "",
|
title: '',
|
||||||
description: "",
|
description: '',
|
||||||
customerId: "",
|
customerId: '',
|
||||||
contactId: "",
|
contactId: '',
|
||||||
stage: OpportunityStageEnum.Qualification,
|
stage: OpportunityStageEnum.Qualification,
|
||||||
probability: 10,
|
probability: 10,
|
||||||
estimatedValue: 0,
|
estimatedValue: 0,
|
||||||
currency: "TRY",
|
currency: 'TRY',
|
||||||
expectedCloseDate: "",
|
expectedCloseDate: '',
|
||||||
assignedTo: "Mevcut Kullanıcı",
|
assignedTo: 'Mevcut Kullanıcı',
|
||||||
teamId: "",
|
teamId: '',
|
||||||
leadSource: LeadSourceEnum.Website,
|
leadSource: LeadSourceEnum.Website,
|
||||||
campaignId: "",
|
campaignId: '',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}, [opportunity, mode, isOpen]);
|
}, [opportunity, mode, isOpen])
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
e: React.ChangeEvent<
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
|
||||||
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
||||||
>
|
|
||||||
) => {
|
) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
// Clear error when user starts typing
|
// Clear error when user starts typing
|
||||||
if (errors[name]) {
|
if (errors[name]) {
|
||||||
setErrors((prev) => ({
|
setErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: "",
|
[name]: '',
|
||||||
}));
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {}
|
||||||
|
|
||||||
if (!formData.title.trim()) {
|
if (!formData.title.trim()) {
|
||||||
newErrors.title = "Fırsat başlığı zorunludur";
|
newErrors.title = 'Fırsat başlığı zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.customerId) {
|
if (!formData.customerId) {
|
||||||
newErrors.customerId = "Müşteri seçimi zorunludur";
|
newErrors.customerId = 'Müşteri seçimi zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.estimatedValue <= 0) {
|
if (formData.estimatedValue <= 0) {
|
||||||
newErrors.estimatedValue = "Tahmini değer 0'dan büyük olmalıdır";
|
newErrors.estimatedValue = "Tahmini değer 0'dan büyük olmalıdır"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.expectedCloseDate) {
|
if (!formData.expectedCloseDate) {
|
||||||
newErrors.expectedCloseDate = "Beklenen kapanış tarihi zorunludur";
|
newErrors.expectedCloseDate = 'Beklenen kapanış tarihi zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.probability < 0 || formData.probability > 100) {
|
if (formData.probability < 0 || formData.probability > 100) {
|
||||||
newErrors.probability = "Olasılık 0-100 arasında olmalıdır";
|
newErrors.probability = 'Olasılık 0-100 arasında olmalıdır'
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors)
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const opportunityData: CrmOpportunity = {
|
const opportunityData: CrmOpportunity = {
|
||||||
|
|
@ -171,38 +168,13 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
competitors: [],
|
competitors: [],
|
||||||
creationTime: opportunity?.creationTime || new Date(),
|
creationTime: opportunity?.creationTime || new Date(),
|
||||||
lastModificationTime: new Date(),
|
lastModificationTime: new Date(),
|
||||||
};
|
}
|
||||||
|
|
||||||
onSave(opportunityData);
|
onSave(opportunityData)
|
||||||
onClose();
|
onClose()
|
||||||
};
|
}
|
||||||
|
|
||||||
const getStageLabel = (stage: OpportunityStageEnum) => {
|
if (!isOpen) return null
|
||||||
const stageLabels = {
|
|
||||||
[OpportunityStageEnum.Qualification]: "Nitelendirme",
|
|
||||||
[OpportunityStageEnum.NeedsAnalysis]: "İhtiyaç Analizi",
|
|
||||||
[OpportunityStageEnum.Proposal]: "Teklif",
|
|
||||||
[OpportunityStageEnum.Negotiation]: "Müzakere",
|
|
||||||
[OpportunityStageEnum.ClosedWon]: "Kazanıldı",
|
|
||||||
[OpportunityStageEnum.ClosedLost]: "Kaybedildi",
|
|
||||||
};
|
|
||||||
return stageLabels[stage];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLeadSourceLabel = (source: LeadSourceEnum) => {
|
|
||||||
const sourceLabels: Record<LeadSourceEnum, string> = {
|
|
||||||
[LeadSourceEnum.Website]: "Web Sitesi",
|
|
||||||
[LeadSourceEnum.Referral]: "Referans",
|
|
||||||
[LeadSourceEnum.Campaign]: "Kampanya",
|
|
||||||
[LeadSourceEnum.Trade_Show]: "Fuar",
|
|
||||||
[LeadSourceEnum.Cold_Call]: "Soğuk Arama",
|
|
||||||
[LeadSourceEnum.Social_Media]: "Sosyal Medya",
|
|
||||||
[LeadSourceEnum.Partner]: "Partner",
|
|
||||||
};
|
|
||||||
return sourceLabels[source];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
|
@ -210,12 +182,9 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-4 border-b">
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
{mode === "create" ? "Yeni Fırsat Oluştur" : "Fırsat Düzenle"}
|
{mode === 'create' ? 'Yeni Fırsat Oluştur' : 'Fırsat Düzenle'}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||||
onClick={onClose}
|
|
||||||
className="text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5" />
|
<FaTimes className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -254,19 +223,15 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
value={formData.title}
|
value={formData.title}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.title ? "border-red-500" : "border-gray-300"
|
errors.title ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Fırsat başlığını girin"
|
placeholder="Fırsat başlığını girin"
|
||||||
/>
|
/>
|
||||||
{errors.title && (
|
{errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.title}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Açıklama</label>
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
name="description"
|
name="description"
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
|
|
@ -278,15 +243,13 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Müşteri *</label>
|
||||||
Müşteri *
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
name="customerId"
|
name="customerId"
|
||||||
value={formData.customerId}
|
value={formData.customerId}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.customerId ? "border-red-500" : "border-gray-300"
|
errors.customerId ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">Müşteri seçin</option>
|
<option value="">Müşteri seçin</option>
|
||||||
|
|
@ -297,18 +260,14 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{errors.customerId && (
|
{errors.customerId && (
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<p className="text-red-500 text-sm mt-1">{errors.customerId}</p>
|
||||||
{errors.customerId}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Sorumlu</label>
|
||||||
Sorumlu
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={formData.assignedTo || ""}
|
value={formData.assignedTo || ''}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
|
|
@ -330,9 +289,7 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Aşama</label>
|
||||||
Aşama
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
name="stage"
|
name="stage"
|
||||||
value={formData.stage}
|
value={formData.stage}
|
||||||
|
|
@ -341,7 +298,7 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
>
|
>
|
||||||
{Object.values(OpportunityStageEnum).map((stage) => (
|
{Object.values(OpportunityStageEnum).map((stage) => (
|
||||||
<option key={stage} value={stage}>
|
<option key={stage} value={stage}>
|
||||||
{getStageLabel(stage)}
|
{getOpportunityStageText(stage)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -359,13 +316,11 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.probability ? "border-red-500" : "border-gray-300"
|
errors.probability ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.probability && (
|
{errors.probability && (
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<p className="text-red-500 text-sm mt-1">{errors.probability}</p>
|
||||||
{errors.probability}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -381,21 +336,17 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.estimatedValue ? "border-red-500" : "border-gray-300"
|
errors.estimatedValue ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
/>
|
/>
|
||||||
{errors.estimatedValue && (
|
{errors.estimatedValue && (
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<p className="text-red-500 text-sm mt-1">{errors.estimatedValue}</p>
|
||||||
{errors.estimatedValue}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Para Birimi</label>
|
||||||
Para Birimi
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
name="currency"
|
name="currency"
|
||||||
value={formData.currency}
|
value={formData.currency}
|
||||||
|
|
@ -418,22 +369,16 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
value={formData.expectedCloseDate}
|
value={formData.expectedCloseDate}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.expectedCloseDate
|
errors.expectedCloseDate ? 'border-red-500' : 'border-gray-300'
|
||||||
? "border-red-500"
|
|
||||||
: "border-gray-300"
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.expectedCloseDate && (
|
{errors.expectedCloseDate && (
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<p className="text-red-500 text-sm mt-1">{errors.expectedCloseDate}</p>
|
||||||
{errors.expectedCloseDate}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Kaynak</label>
|
||||||
Kaynak
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
name="leadSource"
|
name="leadSource"
|
||||||
value={formData.leadSource}
|
value={formData.leadSource}
|
||||||
|
|
@ -442,7 +387,7 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
>
|
>
|
||||||
{Object.values(LeadSourceEnum).map((source) => (
|
{Object.values(LeadSourceEnum).map((source) => (
|
||||||
<option key={source} value={source}>
|
<option key={source} value={source}>
|
||||||
{getLeadSourceLabel(source)}
|
{getOpportunityLeadSourceText(source)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -463,13 +408,13 @@ const OpportunityForm: React.FC<OpportunityFormProps> = ({
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
{mode === "create" ? "Oluştur" : "Güncelle"}
|
{mode === 'create' ? 'Oluştur' : 'Güncelle'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default OpportunityForm;
|
export default OpportunityForm
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
FaBullseye,
|
FaBullseye,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
|
|
@ -9,86 +9,80 @@ import {
|
||||||
FaArrowUp,
|
FaArrowUp,
|
||||||
FaUser,
|
FaUser,
|
||||||
FaBuilding,
|
FaBuilding,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import { CrmOpportunity, OpportunityStageEnum } from "../../../types/crm";
|
import { CrmOpportunity, OpportunityStageEnum } from '../../../types/crm'
|
||||||
import DataTable, { Column } from "../../../components/common/DataTable";
|
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import { mockOpportunities } from "../../../mocks/mockOpportunities";
|
import { mockOpportunities } from '../../../mocks/mockOpportunities'
|
||||||
import OpportunityForm from "./OpportunityForm";
|
import OpportunityForm from './OpportunityForm'
|
||||||
import OpportunityDetails from "./OpportunityDetails";
|
import OpportunityDetails from './OpportunityDetails'
|
||||||
import { BusinessParty } from "../../../types/common";
|
import { BusinessParty } from '../../../types/common'
|
||||||
import Widget from "../../../components/common/Widget";
|
import Widget from '../../../components/common/Widget'
|
||||||
import {
|
import {
|
||||||
getOpportunityProbabilityColor,
|
getOpportunityProbabilityColor,
|
||||||
getOpportunityStageColor,
|
getOpportunityStageColor,
|
||||||
getOpportunityStageText,
|
getOpportunityStageText,
|
||||||
} from "../../../utils/erp";
|
} from '../../../utils/erp'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
// Mock data - replace with actual data fetching
|
|
||||||
const OpportunityManagement: React.FC = () => {
|
const OpportunityManagement: React.FC = () => {
|
||||||
const [opportunities, setOpportunities] =
|
const [opportunities, setOpportunities] = useState<CrmOpportunity[]>(mockOpportunities)
|
||||||
useState<CrmOpportunity[]>(mockOpportunities);
|
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
|
||||||
const [customers] = useState<BusinessParty[]>(mockBusinessParties);
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [selectedStage, setSelectedStage] = useState<OpportunityStageEnum | 'all'>('all')
|
||||||
const [selectedStage, setSelectedStage] = useState<
|
const [selectedCustomer, setSelectedCustomer] = useState<string>('all')
|
||||||
OpportunityStageEnum | "all"
|
|
||||||
>("all");
|
|
||||||
const [selectedCustomer, setSelectedCustomer] = useState<string>("all");
|
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
const [isFormOpen, setIsFormOpen] = useState(false)
|
||||||
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
const [isDetailsOpen, setIsDetailsOpen] = useState(false)
|
||||||
const [selectedOpportunity, setSelectedOpportunity] =
|
const [selectedOpportunity, setSelectedOpportunity] = useState<CrmOpportunity | null>(null)
|
||||||
useState<CrmOpportunity | null>(null);
|
const [formMode, setFormMode] = useState<'create' | 'edit'>('create')
|
||||||
const [formMode, setFormMode] = useState<"create" | "edit">("create");
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
setSelectedOpportunity(null);
|
setSelectedOpportunity(null)
|
||||||
setFormMode("create");
|
setFormMode('create')
|
||||||
setIsFormOpen(true);
|
setIsFormOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEdit = (opportunity: CrmOpportunity) => {
|
const handleEdit = (opportunity: CrmOpportunity) => {
|
||||||
setSelectedOpportunity(opportunity);
|
setSelectedOpportunity(opportunity)
|
||||||
setFormMode("edit");
|
setFormMode('edit')
|
||||||
setIsFormOpen(true);
|
setIsFormOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
if (confirm("Bu fırsatı silmek istediğinizden emin misiniz?")) {
|
if (confirm('Bu fırsatı silmek istediğinizden emin misiniz?')) {
|
||||||
setOpportunities((prev) => prev.filter((opp) => opp.id !== id));
|
setOpportunities((prev) => prev.filter((opp) => opp.id !== id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewDetails = (opportunity: CrmOpportunity) => {
|
const handleViewDetails = (opportunity: CrmOpportunity) => {
|
||||||
setSelectedOpportunity(opportunity);
|
setSelectedOpportunity(opportunity)
|
||||||
setIsDetailsOpen(true);
|
setIsDetailsOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSaveOpportunity = (opportunity: CrmOpportunity) => {
|
const handleSaveOpportunity = (opportunity: CrmOpportunity) => {
|
||||||
if (formMode === "create") {
|
if (formMode === 'create') {
|
||||||
setOpportunities((prev) => [...prev, opportunity]);
|
setOpportunities((prev) => [...prev, opportunity])
|
||||||
} else {
|
} else {
|
||||||
setOpportunities((prev) =>
|
setOpportunities((prev) => prev.map((opp) => (opp.id === opportunity.id ? opportunity : opp)))
|
||||||
prev.map((opp) => (opp.id === opportunity.id ? opportunity : opp))
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseForm = () => {
|
const handleCloseForm = () => {
|
||||||
setIsFormOpen(false);
|
setIsFormOpen(false)
|
||||||
setSelectedOpportunity(null);
|
setSelectedOpportunity(null)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleCloseDetails = () => {
|
const handleCloseDetails = () => {
|
||||||
setIsDetailsOpen(false);
|
setIsDetailsOpen(false)
|
||||||
setSelectedOpportunity(null);
|
setSelectedOpportunity(null)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEditFromDetails = (opportunity: CrmOpportunity) => {
|
const handleEditFromDetails = (opportunity: CrmOpportunity) => {
|
||||||
setIsDetailsOpen(false);
|
setIsDetailsOpen(false)
|
||||||
handleEdit(opportunity);
|
handleEdit(opportunity)
|
||||||
};
|
}
|
||||||
|
|
||||||
const filteredOpportunities = opportunities.filter((opportunity) => {
|
const filteredOpportunities = opportunities.filter((opportunity) => {
|
||||||
if (
|
if (
|
||||||
|
|
@ -96,56 +90,51 @@ const OpportunityManagement: React.FC = () => {
|
||||||
!opportunity.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
!opportunity.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||||
!opportunity.description?.toLowerCase().includes(searchTerm.toLowerCase())
|
!opportunity.description?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
) {
|
) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if (selectedStage !== "all" && opportunity.stage !== selectedStage) {
|
if (selectedStage !== 'all' && opportunity.stage !== selectedStage) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if (
|
if (selectedCustomer !== 'all' && opportunity.customerId !== selectedCustomer) {
|
||||||
selectedCustomer !== "all" &&
|
return false
|
||||||
opportunity.customerId !== selectedCustomer
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true
|
||||||
});
|
})
|
||||||
|
|
||||||
const columns: Column<CrmOpportunity>[] = [
|
const columns: Column<CrmOpportunity>[] = [
|
||||||
{
|
{
|
||||||
key: "title",
|
key: 'title',
|
||||||
header: "Fırsat Başlığı",
|
header: 'Fırsat Başlığı',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (opportunity: CrmOpportunity) => (
|
render: (opportunity: CrmOpportunity) => (
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900">{opportunity.title}</div>
|
<div className="font-medium text-gray-900">{opportunity.title}</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">{opportunity.opportunityNumber}</div>
|
||||||
{opportunity.opportunityNumber}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "customer",
|
key: 'customer',
|
||||||
header: "Müşteri",
|
header: 'Müşteri',
|
||||||
render: (opportunity: CrmOpportunity) => {
|
render: (opportunity: CrmOpportunity) => {
|
||||||
const customer = customers.find((c) => c.id === opportunity.customerId);
|
const customer = customers.find((c) => c.id === opportunity.customerId)
|
||||||
return customer ? (
|
return customer ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaBuilding className="w-4 h-4 text-gray-400" />
|
<FaBuilding className="w-4 h-4 text-gray-400" />
|
||||||
<span>{customer.name}</span>
|
<span>{customer.name}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
'-'
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "stage",
|
key: 'stage',
|
||||||
header: "Aşama",
|
header: 'Aşama',
|
||||||
render: (opportunity: CrmOpportunity) => (
|
render: (opportunity: CrmOpportunity) => (
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getOpportunityStageColor(
|
className={`px-2 py-1 text-xs font-medium rounded-full ${getOpportunityStageColor(
|
||||||
opportunity.stage
|
opportunity.stage,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getOpportunityStageText(opportunity.stage)}
|
{getOpportunityStageText(opportunity.stage)}
|
||||||
|
|
@ -153,27 +142,23 @@ const OpportunityManagement: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "estimatedValue",
|
key: 'estimatedValue',
|
||||||
header: "Tahmini Değer",
|
header: 'Tahmini Değer',
|
||||||
render: (opportunity: CrmOpportunity) => (
|
render: (opportunity: CrmOpportunity) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FaDollarSign className="w-4 h-4 text-gray-400" />
|
<FaDollarSign className="w-4 h-4 text-gray-400" />
|
||||||
<span className="font-medium">
|
<span className="font-medium">₺{opportunity.estimatedValue.toLocaleString()}</span>
|
||||||
₺{opportunity.estimatedValue.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "probability",
|
key: 'probability',
|
||||||
header: "Olasılık",
|
header: 'Olasılık',
|
||||||
render: (opportunity: CrmOpportunity) => (
|
render: (opportunity: CrmOpportunity) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FaArrowUp className="w-4 h-4 text-gray-400" />
|
<FaArrowUp className="w-4 h-4 text-gray-400" />
|
||||||
<span
|
<span
|
||||||
className={`font-medium ${getOpportunityProbabilityColor(
|
className={`font-medium ${getOpportunityProbabilityColor(opportunity.probability)}`}
|
||||||
opportunity.probability
|
|
||||||
)}`}
|
|
||||||
>
|
>
|
||||||
%{opportunity.probability}
|
%{opportunity.probability}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -181,22 +166,18 @@ const OpportunityManagement: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "expectedCloseDate",
|
key: 'expectedCloseDate',
|
||||||
header: "Beklenen Kapanış",
|
header: 'Beklenen Kapanış',
|
||||||
render: (opportunity: CrmOpportunity) => (
|
render: (opportunity: CrmOpportunity) => (
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="flex items-center gap-1 text-sm">
|
||||||
<FaCalendar className="w-3 h-3 text-gray-400" />
|
<FaCalendar className="w-3 h-3 text-gray-400" />
|
||||||
<span>
|
<span>{new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}</span>
|
||||||
{new Date(opportunity.expectedCloseDate).toLocaleDateString(
|
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "assignedTo",
|
key: 'assignedTo',
|
||||||
header: "Sorumlu",
|
header: 'Sorumlu',
|
||||||
render: (opportunity: CrmOpportunity) => (
|
render: (opportunity: CrmOpportunity) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FaUser className="w-4 h-4 text-gray-400" />
|
<FaUser className="w-4 h-4 text-gray-400" />
|
||||||
|
|
@ -205,8 +186,8 @@ const OpportunityManagement: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: 'actions',
|
||||||
header: "İşlemler",
|
header: 'İşlemler',
|
||||||
render: (opportunity: CrmOpportunity) => (
|
render: (opportunity: CrmOpportunity) => (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -233,45 +214,35 @@ const OpportunityManagement: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
// Calculate statistics
|
// Calculate statistics
|
||||||
const totalOpportunities = opportunities.length;
|
const totalOpportunities = opportunities.length
|
||||||
const activeOpportunities = opportunities.filter(
|
const activeOpportunities = opportunities.filter(
|
||||||
(o) =>
|
(o) =>
|
||||||
o.stage !== OpportunityStageEnum.ClosedWon &&
|
o.stage !== OpportunityStageEnum.ClosedWon && o.stage !== OpportunityStageEnum.ClosedLost,
|
||||||
o.stage !== OpportunityStageEnum.ClosedLost
|
).length
|
||||||
).length;
|
const totalValue = opportunities.reduce((sum, opp) => sum + opp.estimatedValue, 0)
|
||||||
const totalValue = opportunities.reduce(
|
|
||||||
(sum, opp) => sum + opp.estimatedValue,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const averageProbability =
|
const averageProbability =
|
||||||
opportunities.reduce((sum, opp) => sum + opp.probability, 0) /
|
opportunities.reduce((sum, opp) => sum + opp.probability, 0) / opportunities.length || 0
|
||||||
opportunities.length || 0;
|
|
||||||
|
|
||||||
// Stage distribution
|
// Stage distribution
|
||||||
const stageDistribution = Object.values(OpportunityStageEnum).map(
|
const stageDistribution = Object.values(OpportunityStageEnum).map((stage) => ({
|
||||||
(stage) => ({
|
|
||||||
stage,
|
stage,
|
||||||
count: opportunities.filter((o) => o.stage === stage).length,
|
count: opportunities.filter((o) => o.stage === stage).length,
|
||||||
value: opportunities
|
value: opportunities
|
||||||
.filter((o) => o.stage === stage)
|
.filter((o) => o.stage === stage)
|
||||||
.reduce((sum, o) => sum + o.estimatedValue, 0),
|
.reduce((sum, o) => sum + o.estimatedValue, 0),
|
||||||
})
|
}))
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
<h2 className="text-xl font-bold text-gray-900">Teklif & Fırsat Yönetimi</h2>
|
||||||
Teklif & Fırsat Yönetimi
|
<p className="text-sm text-gray-600">Satış fırsatları ve teklif takibi</p>
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Satış fırsatları ve teklif takibi
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
|
|
@ -284,19 +255,9 @@ const OpportunityManagement: React.FC = () => {
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||||
<Widget
|
<Widget title="Toplam Fırsat" value={totalOpportunities} color="blue" icon="FaBullseye" />
|
||||||
title="Toplam Fırsat"
|
|
||||||
value={totalOpportunities}
|
|
||||||
color="blue"
|
|
||||||
icon="FaBullseye"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget title="Aktif Fırsat" value={activeOpportunities} color="green" icon="FaArrowUp" />
|
||||||
title="Aktif Fırsat"
|
|
||||||
value={activeOpportunities}
|
|
||||||
color="green"
|
|
||||||
icon="FaArrowUp"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget
|
||||||
title="Toplam Değer"
|
title="Toplam Değer"
|
||||||
|
|
@ -315,25 +276,19 @@ const OpportunityManagement: React.FC = () => {
|
||||||
|
|
||||||
{/* Stage Pipeline */}
|
{/* Stage Pipeline */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-3">
|
<div className="bg-white rounded-lg shadow-sm border p-3">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">
|
<h3 className="text-sm font-semibold text-gray-900 mb-3">Satış Aşamaları</h3>
|
||||||
Satış Aşamaları
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-6 gap-2">
|
||||||
{stageDistribution.map(({ stage, count, value }) => (
|
{stageDistribution.map(({ stage, count, value }) => (
|
||||||
<div key={stage} className="text-center p-2 border rounded-lg">
|
<div key={stage} className="text-center p-2 border rounded-lg">
|
||||||
<div
|
<div
|
||||||
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getOpportunityStageColor(
|
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getOpportunityStageColor(
|
||||||
stage
|
stage,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getOpportunityStageText(stage)}
|
{getOpportunityStageText(stage)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold text-gray-900 mb-1">
|
<div className="text-lg font-bold text-gray-900 mb-1">{count}</div>
|
||||||
{count}
|
<div className="text-xs text-gray-500">₺{value.toLocaleString()}</div>
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
₺{value.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -341,28 +296,18 @@ const OpportunityManagement: React.FC = () => {
|
||||||
|
|
||||||
{/* Win Rate Analysis */}
|
{/* Win Rate Analysis */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-3">
|
<div className="bg-white rounded-lg shadow-sm border p-3">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">
|
<h3 className="text-sm font-semibold text-gray-900 mb-3">Başarı Oranı Analizi</h3>
|
||||||
Başarı Oranı Analizi
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-green-600 mb-1">
|
<div className="text-2xl font-bold text-green-600 mb-1">
|
||||||
{
|
{opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedWon).length}
|
||||||
opportunities.filter(
|
|
||||||
(o) => o.stage === OpportunityStageEnum.ClosedWon
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">Kazanılan Fırsat</p>
|
<p className="text-sm text-gray-600">Kazanılan Fırsat</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-red-600 mb-1">
|
<div className="text-2xl font-bold text-red-600 mb-1">
|
||||||
{
|
{opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedLost).length}
|
||||||
opportunities.filter(
|
|
||||||
(o) => o.stage === OpportunityStageEnum.ClosedLost
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">Kaybedilen Fırsat</p>
|
<p className="text-sm text-gray-600">Kaybedilen Fırsat</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -370,18 +315,16 @@ const OpportunityManagement: React.FC = () => {
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-blue-600 mb-1">
|
<div className="text-2xl font-bold text-blue-600 mb-1">
|
||||||
{Math.round(
|
{Math.round(
|
||||||
(opportunities.filter(
|
(opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedWon).length /
|
||||||
(o) => o.stage === OpportunityStageEnum.ClosedWon
|
|
||||||
).length /
|
|
||||||
Math.max(
|
Math.max(
|
||||||
opportunities.filter(
|
opportunities.filter(
|
||||||
(o) =>
|
(o) =>
|
||||||
o.stage === OpportunityStageEnum.ClosedWon ||
|
o.stage === OpportunityStageEnum.ClosedWon ||
|
||||||
o.stage === OpportunityStageEnum.ClosedLost
|
o.stage === OpportunityStageEnum.ClosedLost,
|
||||||
).length,
|
).length,
|
||||||
1
|
1,
|
||||||
)) *
|
)) *
|
||||||
100
|
100,
|
||||||
)}
|
)}
|
||||||
%
|
%
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -404,9 +347,7 @@ const OpportunityManagement: React.FC = () => {
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={selectedStage}
|
value={selectedStage}
|
||||||
onChange={(e) =>
|
onChange={(e) => setSelectedStage(e.target.value as OpportunityStageEnum | 'all')}
|
||||||
setSelectedStage(e.target.value as OpportunityStageEnum | "all")
|
|
||||||
}
|
|
||||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">Tüm Aşamalar</option>
|
<option value="all">Tüm Aşamalar</option>
|
||||||
|
|
@ -439,12 +380,8 @@ const OpportunityManagement: React.FC = () => {
|
||||||
{filteredOpportunities.length === 0 && (
|
{filteredOpportunities.length === 0 && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaBullseye className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
<FaBullseye className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||||
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
<h3 className="text-sm font-medium text-gray-900 mb-2">Fırsat bulunamadı</h3>
|
||||||
Fırsat bulunamadı
|
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500">
|
|
||||||
Arama kriterlerinizi değiştirmeyi deneyin.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -464,7 +401,8 @@ const OpportunityManagement: React.FC = () => {
|
||||||
opportunity={selectedOpportunity}
|
opportunity={selectedOpportunity}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default OpportunityManagement;
|
export default OpportunityManagement
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
FaSave,
|
FaSave,
|
||||||
FaTimes,
|
FaTimes,
|
||||||
|
|
@ -11,31 +11,32 @@ import {
|
||||||
FaBuilding,
|
FaBuilding,
|
||||||
FaClipboardList,
|
FaClipboardList,
|
||||||
FaNotesMedical,
|
FaNotesMedical,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import {
|
import {
|
||||||
SaleOrderStatusEnum,
|
SaleOrderStatusEnum,
|
||||||
CrmSalesOrder,
|
CrmSalesOrder,
|
||||||
CrmSalesOrderItem,
|
CrmSalesOrderItem,
|
||||||
SaleOrderItemStatusEnum,
|
SaleOrderItemStatusEnum,
|
||||||
} from "../../../types/crm";
|
} from '../../../types/crm'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import { mockMaterials } from "../../../mocks/mockMaterials";
|
import { mockMaterials } from '../../../mocks/mockMaterials'
|
||||||
import { mockSalesOrders } from "../../../mocks/mockSalesOrders";
|
import { mockSalesOrders } from '../../../mocks/mockSalesOrders'
|
||||||
import { BusinessParty, PaymentTerms } from "../../../types/common";
|
import { BusinessParty, PaymentTerms } from '../../../types/common'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const SalesOrderForm: React.FC = () => {
|
const SalesOrderForm: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { id } = useParams();
|
const { id } = useParams()
|
||||||
const isEdit = Boolean(id);
|
const isEdit = Boolean(id)
|
||||||
|
|
||||||
const [customers] = useState<BusinessParty[]>(mockBusinessParties);
|
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
|
||||||
const [materials] = useState(mockMaterials);
|
const [materials] = useState(mockMaterials)
|
||||||
|
|
||||||
// Form data state with extended fields for form handling
|
// Form data state with extended fields for form handling
|
||||||
const [formData, setFormData] = useState<CrmSalesOrder>({
|
const [formData, setFormData] = useState<CrmSalesOrder>({
|
||||||
id: "",
|
id: '',
|
||||||
orderNumber: "",
|
orderNumber: '',
|
||||||
customerId: "",
|
customerId: '',
|
||||||
customer: undefined,
|
customer: undefined,
|
||||||
orderDate: new Date(),
|
orderDate: new Date(),
|
||||||
requestedDeliveryDate: new Date(),
|
requestedDeliveryDate: new Date(),
|
||||||
|
|
@ -45,76 +46,73 @@ const SalesOrderForm: React.FC = () => {
|
||||||
taxAmount: 0,
|
taxAmount: 0,
|
||||||
discountAmount: 0,
|
discountAmount: 0,
|
||||||
totalAmount: 0,
|
totalAmount: 0,
|
||||||
currency: "TRY",
|
currency: 'TRY',
|
||||||
paymentTerms: PaymentTerms.Net30,
|
paymentTerms: PaymentTerms.Net30,
|
||||||
deliveryAddress: {
|
deliveryAddress: {
|
||||||
street: "",
|
street: '',
|
||||||
city: "",
|
city: '',
|
||||||
state: "",
|
state: '',
|
||||||
postalCode: "",
|
postalCode: '',
|
||||||
country: "Türkiye",
|
country: 'Türkiye',
|
||||||
},
|
},
|
||||||
billingAddress: {
|
billingAddress: {
|
||||||
street: "",
|
street: '',
|
||||||
city: "",
|
city: '',
|
||||||
state: "",
|
state: '',
|
||||||
postalCode: "",
|
postalCode: '',
|
||||||
country: "Türkiye",
|
country: 'Türkiye',
|
||||||
},
|
},
|
||||||
items: [],
|
items: [],
|
||||||
deliveries: [],
|
deliveries: [],
|
||||||
creationTime: new Date(),
|
creationTime: new Date(),
|
||||||
lastModificationTime: new Date(),
|
lastModificationTime: new Date(),
|
||||||
// Additional form fields
|
// Additional form fields
|
||||||
deliveryTerms: "",
|
deliveryTerms: '',
|
||||||
notes: "",
|
notes: '',
|
||||||
exchangeRate: 1,
|
exchangeRate: 1,
|
||||||
discountRate: 0,
|
discountRate: 0,
|
||||||
taxRate: 18,
|
taxRate: 18,
|
||||||
specialInstructions: "",
|
specialInstructions: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const [newItem, setNewItem] = useState<Partial<CrmSalesOrderItem>>({
|
const [newItem, setNewItem] = useState<Partial<CrmSalesOrderItem>>({
|
||||||
materialId: "",
|
materialId: '',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unitPrice: 0,
|
unitPrice: 0,
|
||||||
discountRate: 0,
|
discountRate: 0,
|
||||||
taxRate: 18,
|
taxRate: 18,
|
||||||
notes: "",
|
notes: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const [showItemForm, setShowItemForm] = useState(false);
|
const [showItemForm, setShowItemForm] = useState(false)
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEdit && id) {
|
if (isEdit && id) {
|
||||||
const existingOrder = mockSalesOrders.find((order) => order.id === id);
|
const existingOrder = mockSalesOrders.find((order) => order.id === id)
|
||||||
if (existingOrder) {
|
if (existingOrder) {
|
||||||
setFormData(existingOrder);
|
setFormData(existingOrder)
|
||||||
}
|
}
|
||||||
// Load existing sales order data
|
// Load existing sales order data
|
||||||
// This would typically come from an API call
|
// This would typically come from an API call
|
||||||
console.log("Loading sales order:", id);
|
console.log('Loading sales order:', id)
|
||||||
}
|
}
|
||||||
}, [id, isEdit]);
|
}, [id, isEdit])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const calculateTotals = () => {
|
const calculateTotals = () => {
|
||||||
if (!formData.items) return;
|
if (!formData.items) return
|
||||||
|
|
||||||
const subtotal = formData.items.reduce(
|
const subtotal = formData.items.reduce((sum: number, item: CrmSalesOrderItem) => {
|
||||||
(sum: number, item: CrmSalesOrderItem) => {
|
const itemTotal = item.quantity * item.unitPrice
|
||||||
const itemTotal = item.quantity * item.unitPrice;
|
const itemDiscount = itemTotal * ((item.discountRate || 0) / 100)
|
||||||
const itemDiscount = itemTotal * ((item.discountRate || 0) / 100);
|
return sum + (itemTotal - itemDiscount)
|
||||||
return sum + (itemTotal - itemDiscount);
|
}, 0)
|
||||||
},
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
const discountAmount = subtotal * ((formData.discountRate || 0) / 100);
|
const discountAmount = subtotal * ((formData.discountRate || 0) / 100)
|
||||||
const taxableAmount = subtotal - discountAmount;
|
const taxableAmount = subtotal - discountAmount
|
||||||
const taxAmount = taxableAmount * ((formData.taxRate || 18) / 100);
|
const taxAmount = taxableAmount * ((formData.taxRate || 18) / 100)
|
||||||
const totalAmount = taxableAmount + taxAmount;
|
const totalAmount = taxableAmount + taxAmount
|
||||||
|
|
||||||
setFormData((prev: CrmSalesOrder) => ({
|
setFormData((prev: CrmSalesOrder) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -122,53 +120,47 @@ const SalesOrderForm: React.FC = () => {
|
||||||
discountAmount,
|
discountAmount,
|
||||||
taxAmount,
|
taxAmount,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
}));
|
}))
|
||||||
};
|
}
|
||||||
|
|
||||||
calculateTotals();
|
calculateTotals()
|
||||||
}, [formData.items, formData.discountRate, formData.taxRate]);
|
}, [formData.items, formData.discountRate, formData.taxRate])
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
field: string,
|
field: string,
|
||||||
value:
|
value: string | number | SaleOrderStatusEnum | PaymentTerms | Date | undefined,
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| SaleOrderStatusEnum
|
|
||||||
| PaymentTerms
|
|
||||||
| Date
|
|
||||||
| undefined
|
|
||||||
) => {
|
) => {
|
||||||
setFormData((prev: CrmSalesOrder) => ({
|
setFormData((prev: CrmSalesOrder) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors((prev: Record<string, string>) => ({
|
setErrors((prev: Record<string, string>) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: "",
|
[field]: '',
|
||||||
}));
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddItem = () => {
|
const handleAddItem = () => {
|
||||||
if (!newItem.materialId || !newItem.quantity || !newItem.unitPrice) {
|
if (!newItem.materialId || !newItem.quantity || !newItem.unitPrice) {
|
||||||
alert("Lütfen tüm item bilgilerini doldurun!");
|
alert('Lütfen tüm item bilgilerini doldurun!')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const material = materials.find((m) => m.id === newItem.materialId);
|
const material = materials.find((m) => m.id === newItem.materialId)
|
||||||
if (!material) return;
|
if (!material) return
|
||||||
|
|
||||||
const itemTotal = (newItem.quantity || 0) * (newItem.unitPrice || 0);
|
const itemTotal = (newItem.quantity || 0) * (newItem.unitPrice || 0)
|
||||||
const itemDiscount = itemTotal * ((newItem.discountRate || 0) / 100);
|
const itemDiscount = itemTotal * ((newItem.discountRate || 0) / 100)
|
||||||
const itemTaxableAmount = itemTotal - itemDiscount;
|
const itemTaxableAmount = itemTotal - itemDiscount
|
||||||
const itemTaxAmount = itemTaxableAmount * ((newItem.taxRate || 0) / 100);
|
const itemTaxAmount = itemTaxableAmount * ((newItem.taxRate || 0) / 100)
|
||||||
const itemTotalAmount = itemTaxableAmount + itemTaxAmount;
|
const itemTotalAmount = itemTaxableAmount + itemTaxAmount
|
||||||
|
|
||||||
const item: CrmSalesOrderItem = {
|
const item: CrmSalesOrderItem = {
|
||||||
id: `item_${Date.now()}`,
|
id: `item_${Date.now()}`,
|
||||||
orderId: formData.id || "",
|
orderId: formData.id || '',
|
||||||
materialId: newItem.materialId!,
|
materialId: newItem.materialId!,
|
||||||
material: material,
|
material: material,
|
||||||
description: material.name,
|
description: material.name,
|
||||||
|
|
@ -185,89 +177,88 @@ const SalesOrderForm: React.FC = () => {
|
||||||
requestedDate: formData.requestedDeliveryDate,
|
requestedDate: formData.requestedDeliveryDate,
|
||||||
confirmedDate: undefined,
|
confirmedDate: undefined,
|
||||||
status: SaleOrderItemStatusEnum.Pending,
|
status: SaleOrderItemStatusEnum.Pending,
|
||||||
notes: newItem.notes || "",
|
notes: newItem.notes || '',
|
||||||
};
|
}
|
||||||
|
|
||||||
setFormData((prev: CrmSalesOrder) => ({
|
setFormData((prev: CrmSalesOrder) => ({
|
||||||
...prev,
|
...prev,
|
||||||
items: [...prev.items, item],
|
items: [...prev.items, item],
|
||||||
}));
|
}))
|
||||||
|
|
||||||
setNewItem({
|
setNewItem({
|
||||||
materialId: "",
|
materialId: '',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unitPrice: 0,
|
unitPrice: 0,
|
||||||
discountRate: 0,
|
discountRate: 0,
|
||||||
taxRate: 18,
|
taxRate: 18,
|
||||||
notes: "",
|
notes: '',
|
||||||
});
|
})
|
||||||
setShowItemForm(false);
|
setShowItemForm(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleRemoveItem = (itemId: string) => {
|
const handleRemoveItem = (itemId: string) => {
|
||||||
setFormData((prev: CrmSalesOrder) => ({
|
setFormData((prev: CrmSalesOrder) => ({
|
||||||
...prev,
|
...prev,
|
||||||
items: prev.items.filter((item: CrmSalesOrderItem) => item.id !== itemId),
|
items: prev.items.filter((item: CrmSalesOrderItem) => item.id !== itemId),
|
||||||
}));
|
}))
|
||||||
};
|
}
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {}
|
||||||
|
|
||||||
if (!formData.orderNumber) {
|
if (!formData.orderNumber) {
|
||||||
newErrors.orderNumber = "Sipariş numarası gerekli";
|
newErrors.orderNumber = 'Sipariş numarası gerekli'
|
||||||
}
|
}
|
||||||
if (!formData.customerId) {
|
if (!formData.customerId) {
|
||||||
newErrors.customerId = "Müşteri seçimi gerekli";
|
newErrors.customerId = 'Müşteri seçimi gerekli'
|
||||||
}
|
}
|
||||||
if (!formData.orderDate) {
|
if (!formData.orderDate) {
|
||||||
newErrors.orderDate = "Sipariş tarihi gerekli";
|
newErrors.orderDate = 'Sipariş tarihi gerekli'
|
||||||
}
|
}
|
||||||
if (!formData.items || formData.items.length === 0) {
|
if (!formData.items || formData.items.length === 0) {
|
||||||
newErrors.items = "En az bir ürün eklemelisiniz";
|
newErrors.items = 'En az bir ürün eklemelisiniz'
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors)
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Here you would make an API call to save the sales order
|
// Here you would make an API call to save the sales order
|
||||||
console.log("Saving sales order:", formData);
|
console.log('Saving sales order:', formData)
|
||||||
|
|
||||||
// Navigate back to sales orders list
|
// Navigate back to sales orders list
|
||||||
navigate("/admin/crm/sales-orders");
|
navigate('/admin/crm/sales-orders')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving sales order:", error);
|
console.error('Error saving sales order:', error)
|
||||||
alert("Sipariş kaydedilirken bir hata oluştu!");
|
alert('Sipariş kaydedilirken bir hata oluştu!')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const selectedCustomer = customers.find((c) => c.id === formData.customerId);
|
const selectedCustomer = customers.find((c) => c.id === formData.customerId)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-6 pt-2">
|
<div className="space-y-6 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
<h2 className="text-xl font-bold text-gray-900">
|
||||||
{isEdit ? "Satış Siparişi Düzenle" : "Yeni Satış Siparişi"}
|
{isEdit ? 'Satış Siparişi Düzenle' : 'Yeni Satış Siparişi'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">Müşteri siparişi oluştur ve yönet</p>
|
||||||
Müşteri siparişi oluştur ve yönet
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate("/admin/crm/sales-orders")}
|
onClick={() => navigate('/admin/crm/sales-orders')}
|
||||||
className="flex items-center gap-2 px-3 py-1.5 text-sm border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors"
|
className="flex items-center gap-2 px-3 py-1.5 text-sm border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<FaTimes className="w-4 h-4" />
|
<FaTimes className="w-4 h-4" />
|
||||||
|
|
@ -288,9 +279,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<FaShoppingCart className="w-5 h-5 text-blue-600" />
|
<FaShoppingCart className="w-5 h-5 text-blue-600" />
|
||||||
<h3 className="text-base font-semibold text-gray-900">
|
<h3 className="text-base font-semibold text-gray-900">Sipariş Bilgileri</h3>
|
||||||
Sipariş Bilgileri
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
|
@ -301,18 +290,14 @@ const SalesOrderForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.orderNumber}
|
value={formData.orderNumber}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('orderNumber', e.target.value)}
|
||||||
handleInputChange("orderNumber", e.target.value)
|
|
||||||
}
|
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.orderNumber ? "border-red-500" : "border-gray-300"
|
errors.orderNumber ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="ORD-2024-001"
|
placeholder="ORD-2024-001"
|
||||||
/>
|
/>
|
||||||
{errors.orderNumber && (
|
{errors.orderNumber && (
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<p className="text-red-500 text-sm mt-1">{errors.orderNumber}</p>
|
||||||
{errors.orderNumber}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -322,12 +307,10 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.orderDate.toISOString().split("T")[0]}
|
value={formData.orderDate.toISOString().split('T')[0]}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('orderDate', new Date(e.target.value))}
|
||||||
handleInputChange("orderDate", new Date(e.target.value))
|
|
||||||
}
|
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.orderDate ? "border-red-500" : "border-gray-300"
|
errors.orderDate ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.orderDate && (
|
{errors.orderDate && (
|
||||||
|
|
@ -336,32 +319,21 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
|
||||||
Durum
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={formData.status}
|
value={formData.status}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('status', e.target.value as SaleOrderStatusEnum)
|
||||||
"status",
|
|
||||||
e.target.value as SaleOrderStatusEnum
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value={SaleOrderStatusEnum.Draft}>Taslak</option>
|
<option value={SaleOrderStatusEnum.Draft}>Taslak</option>
|
||||||
<option value={SaleOrderStatusEnum.Confirmed}>Onaylandı</option>
|
<option value={SaleOrderStatusEnum.Confirmed}>Onaylandı</option>
|
||||||
<option value={SaleOrderStatusEnum.InProduction}>
|
<option value={SaleOrderStatusEnum.InProduction}>Üretimde</option>
|
||||||
Üretimde
|
|
||||||
</option>
|
|
||||||
<option value={SaleOrderStatusEnum.Ready}>Hazır</option>
|
<option value={SaleOrderStatusEnum.Ready}>Hazır</option>
|
||||||
<option value={SaleOrderStatusEnum.Shipped}>Kargoda</option>
|
<option value={SaleOrderStatusEnum.Shipped}>Kargoda</option>
|
||||||
<option value={SaleOrderStatusEnum.Delivered}>
|
<option value={SaleOrderStatusEnum.Delivered}>Teslim Edildi</option>
|
||||||
Teslim Edildi
|
<option value={SaleOrderStatusEnum.Cancelled}>İptal Edildi</option>
|
||||||
</option>
|
|
||||||
<option value={SaleOrderStatusEnum.Cancelled}>
|
|
||||||
İptal Edildi
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -371,23 +343,17 @@ const SalesOrderForm: React.FC = () => {
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<FaBuilding className="w-5 h-5 text-blue-600" />
|
<FaBuilding className="w-5 h-5 text-blue-600" />
|
||||||
<h3 className="text-base font-semibold text-gray-900">
|
<h3 className="text-base font-semibold text-gray-900">Müşteri Bilgileri</h3>
|
||||||
Müşteri Bilgileri
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Müşteri *</label>
|
||||||
Müşteri *
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={formData.customerId}
|
value={formData.customerId}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('customerId', e.target.value)}
|
||||||
handleInputChange("customerId", e.target.value)
|
|
||||||
}
|
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||||
errors.customerId ? "border-red-500" : "border-gray-300"
|
errors.customerId ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">Müşteri seçin...</option>
|
<option value="">Müşteri seçin...</option>
|
||||||
|
|
@ -416,20 +382,16 @@ const SalesOrderForm: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Telefon</label>
|
||||||
Telefon
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={selectedCustomer.primaryContact?.phone || ""}
|
value={selectedCustomer.primaryContact?.phone || ''}
|
||||||
disabled
|
disabled
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md bg-gray-50"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md bg-gray-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Adres</label>
|
||||||
Adres
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
value={`${selectedCustomer.address?.street}, ${selectedCustomer.address?.city}, ${selectedCustomer.address?.country}`}
|
value={`${selectedCustomer.address?.street}, ${selectedCustomer.address?.city}, ${selectedCustomer.address?.country}`}
|
||||||
disabled
|
disabled
|
||||||
|
|
@ -446,9 +408,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<FaCalendar className="w-5 h-5 text-blue-600" />
|
<FaCalendar className="w-5 h-5 text-blue-600" />
|
||||||
<h3 className="text-base font-semibold text-gray-900">
|
<h3 className="text-base font-semibold text-gray-900">Teslimat Bilgileri</h3>
|
||||||
Teslimat Bilgileri
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
|
@ -458,14 +418,9 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={
|
value={formData.requestedDeliveryDate.toISOString().split('T')[0]}
|
||||||
formData.requestedDeliveryDate.toISOString().split("T")[0]
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('requestedDeliveryDate', new Date(e.target.value))
|
||||||
"requestedDeliveryDate",
|
|
||||||
new Date(e.target.value)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
|
|
@ -477,14 +432,11 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={
|
value={formData.confirmedDeliveryDate?.toISOString().split('T')[0] || ''}
|
||||||
formData.confirmedDeliveryDate?.toISOString().split("T")[0] ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange(
|
||||||
"confirmedDeliveryDate",
|
'confirmedDeliveryDate',
|
||||||
e.target.value ? new Date(e.target.value) : undefined
|
e.target.value ? new Date(e.target.value) : undefined,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
|
@ -498,9 +450,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.deliveryTerms}
|
value={formData.deliveryTerms}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('deliveryTerms', e.target.value)}
|
||||||
handleInputChange("deliveryTerms", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
placeholder="EXW, FOB, CIF, vb."
|
placeholder="EXW, FOB, CIF, vb."
|
||||||
/>
|
/>
|
||||||
|
|
@ -513,9 +463,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaClipboardList className="w-5 h-5 text-blue-600" />
|
<FaClipboardList className="w-5 h-5 text-blue-600" />
|
||||||
<h3 className="text-base font-semibold text-gray-900">
|
<h3 className="text-base font-semibold text-gray-900">Sipariş Kalemleri</h3>
|
||||||
Sipariş Kalemleri
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -527,20 +475,16 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errors.items && (
|
{errors.items && <p className="text-red-500 text-sm mb-4">{errors.items}</p>}
|
||||||
<p className="text-red-500 text-sm mb-4">{errors.items}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Add Item Form */}
|
{/* Add Item Form */}
|
||||||
{showItemForm && (
|
{showItemForm && (
|
||||||
<div className="bg-gray-50 p-3 rounded-lg mb-3">
|
<div className="bg-gray-50 p-3 rounded-lg mb-3">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-6 gap-3">
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Malzeme</label>
|
||||||
Malzeme
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={newItem.materialId || ""}
|
value={newItem.materialId || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
|
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -559,14 +503,12 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Miktar</label>
|
||||||
Miktar
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={newItem.quantity || ""}
|
value={newItem.quantity || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
|
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -585,7 +527,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={newItem.unitPrice || ""}
|
value={newItem.unitPrice || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
|
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -605,7 +547,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={newItem.discountRate || ""}
|
value={newItem.discountRate || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
|
setNewItem((prev: Partial<CrmSalesOrderItem>) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -681,7 +623,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
||||||
{item.quantity} {item.unit?.name || ""}
|
{item.quantity} {item.unit?.name || ''}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
||||||
₺{item.unitPrice.toLocaleString()}
|
₺{item.unitPrice.toLocaleString()}
|
||||||
|
|
@ -717,8 +659,8 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
Henüz sipariş kalemi eklenmedi. Yukarıdaki "Kalem Ekle" butonunu
|
Henüz sipariş kalemi eklenmedi. Yukarıdaki "Kalem Ekle" butonunu kullanarak ürün
|
||||||
kullanarak ürün ekleyebilirsiniz.
|
ekleyebilirsiniz.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -727,9 +669,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<FaCalculator className="w-5 h-5 text-blue-600" />
|
<FaCalculator className="w-5 h-5 text-blue-600" />
|
||||||
<h3 className="text-base font-semibold text-gray-900">
|
<h3 className="text-base font-semibold text-gray-900">Mali Bilgiler</h3>
|
||||||
Mali Bilgiler
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
@ -740,9 +680,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.currency}
|
value={formData.currency}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('currency', e.target.value)}
|
||||||
handleInputChange("currency", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="TRY">TRY - Türk Lirası</option>
|
<option value="TRY">TRY - Türk Lirası</option>
|
||||||
|
|
@ -752,19 +690,14 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Döviz Kuru</label>
|
||||||
Döviz Kuru
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={formData.exchangeRate}
|
value={formData.exchangeRate}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('exchangeRate', parseFloat(e.target.value) || 1)
|
||||||
"exchangeRate",
|
|
||||||
parseFloat(e.target.value) || 1
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
|
|
@ -781,10 +714,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={formData.discountRate}
|
value={formData.discountRate}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(
|
handleInputChange('discountRate', parseFloat(e.target.value) || 0)
|
||||||
"discountRate",
|
|
||||||
parseFloat(e.target.value) || 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
|
|
@ -800,12 +730,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
max="100"
|
max="100"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={formData.taxRate}
|
value={formData.taxRate}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('taxRate', parseFloat(e.target.value) || 18)}
|
||||||
handleInputChange(
|
|
||||||
"taxRate",
|
|
||||||
parseFloat(e.target.value) || 18
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -816,9 +741,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.paymentTerms}
|
value={formData.paymentTerms}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('paymentTerms', e.target.value)}
|
||||||
handleInputChange("paymentTerms", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Seçin...</option>
|
<option value="">Seçin...</option>
|
||||||
|
|
@ -832,9 +755,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 p-3 rounded-lg">
|
<div className="bg-gray-50 p-3 rounded-lg">
|
||||||
<h4 className="font-medium text-sm text-gray-900 mb-2">
|
<h4 className="font-medium text-sm text-gray-900 mb-2">Sipariş Özeti</h4>
|
||||||
Sipariş Özeti
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-sm text-gray-600">Ara Toplam:</span>
|
<span className="text-sm text-gray-600">Ara Toplam:</span>
|
||||||
|
|
@ -856,9 +777,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t pt-2 mt-2">
|
<div className="border-t pt-2 mt-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-lg font-semibold text-gray-900">
|
<span className="text-lg font-semibold text-gray-900">Genel Toplam:</span>
|
||||||
Genel Toplam:
|
|
||||||
</span>
|
|
||||||
<span className="text-lg font-bold text-blue-600">
|
<span className="text-lg font-bold text-blue-600">
|
||||||
₺{formData.totalAmount.toLocaleString()}
|
₺{formData.totalAmount.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -873,19 +792,15 @@ const SalesOrderForm: React.FC = () => {
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<FaNotesMedical className="w-5 h-5 text-blue-600" />
|
<FaNotesMedical className="w-5 h-5 text-blue-600" />
|
||||||
<h3 className="text-base font-semibold text-gray-900">
|
<h3 className="text-base font-semibold text-gray-900">Notlar ve Talimatlar</h3>
|
||||||
Notlar ve Talimatlar
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Genel Notlar</label>
|
||||||
Genel Notlar
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.notes}
|
value={formData.notes}
|
||||||
onChange={(e) => handleInputChange("notes", e.target.value)}
|
onChange={(e) => handleInputChange('notes', e.target.value)}
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
placeholder="Sipariş ile ilgili genel notlar..."
|
placeholder="Sipariş ile ilgili genel notlar..."
|
||||||
|
|
@ -898,9 +813,7 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.specialInstructions}
|
value={formData.specialInstructions}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('specialInstructions', e.target.value)}
|
||||||
handleInputChange("specialInstructions", e.target.value)
|
|
||||||
}
|
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
placeholder="Özel üretim talimatları, ambalaj notları vb..."
|
placeholder="Özel üretim talimatları, ambalaj notları vb..."
|
||||||
|
|
@ -910,7 +823,8 @@ const SalesOrderForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default SalesOrderForm;
|
export default SalesOrderForm
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
FaEdit,
|
FaEdit,
|
||||||
FaArrowLeft,
|
FaArrowLeft,
|
||||||
|
|
@ -17,73 +17,69 @@ import {
|
||||||
FaTruck,
|
FaTruck,
|
||||||
FaFileInvoiceDollar,
|
FaFileInvoiceDollar,
|
||||||
FaExclamationCircle,
|
FaExclamationCircle,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import {
|
import { CrmSalesOrder, CrmSalesOrderItem, SaleOrderItemStatusEnum } from '../../../types/crm'
|
||||||
CrmSalesOrder,
|
import { mockSalesOrders } from '../../../mocks/mockSalesOrders'
|
||||||
CrmSalesOrderItem,
|
|
||||||
SaleOrderItemStatusEnum,
|
|
||||||
} from "../../../types/crm";
|
|
||||||
import { mockSalesOrders } from "../../../mocks/mockSalesOrders";
|
|
||||||
import {
|
import {
|
||||||
getSaleOrderItemStatusnfo,
|
getSaleOrderItemStatusnfo,
|
||||||
getSaleOrderStatusColor,
|
getSaleOrderStatusColor,
|
||||||
getSaleOrderStatusText,
|
getSaleOrderStatusText,
|
||||||
} from "../../../utils/erp";
|
} from '../../../utils/erp'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const SalesOrderView: React.FC = () => {
|
const SalesOrderView: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { id } = useParams();
|
const { id } = useParams()
|
||||||
|
|
||||||
const [order, setOrder] = useState<CrmSalesOrder | null>(null);
|
const [order, setOrder] = useState<CrmSalesOrder | null>(null)
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
// Find the order from mock data
|
// Find the order from mock data
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const foundOrder = mockSalesOrders.find((order) => order.id === id);
|
const foundOrder = mockSalesOrders.find((order) => order.id === id)
|
||||||
if (foundOrder) {
|
if (foundOrder) {
|
||||||
setOrder(foundOrder);
|
setOrder(foundOrder)
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}, 1000);
|
}, 1000)
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-64">
|
<div className="flex flex-col items-center justify-center h-64">
|
||||||
<FaExclamationCircle className="text-4xl text-red-500 mb-4" />
|
<FaExclamationCircle className="text-4xl text-red-500 mb-4" />
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">Sipariş Bulunamadı</h2>
|
||||||
Sipariş Bulunamadı
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 mb-4">Belirtilen sipariş mevcut değil.</p>
|
<p className="text-gray-600 mb-4">Belirtilen sipariş mevcut değil.</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/sales-orders")}
|
onClick={() => navigate('/admin/crm/sales-orders')}
|
||||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Sipariş Listesine Dön
|
Sipariş Listesine Dön
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/sales-orders")}
|
onClick={() => navigate('/admin/crm/sales-orders')}
|
||||||
className="flex items-center text-gray-600 hover:text-gray-800 transition-colors"
|
className="flex items-center text-gray-600 hover:text-gray-800 transition-colors"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="mr-2" />
|
<FaArrowLeft className="mr-2" />
|
||||||
|
|
@ -95,24 +91,20 @@ const SalesOrderView: React.FC = () => {
|
||||||
<FaShoppingCart className="mr-3 text-blue-600" />
|
<FaShoppingCart className="mr-3 text-blue-600" />
|
||||||
{order.orderNumber}
|
{order.orderNumber}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">Satış Siparişi Detayları</p>
|
||||||
Satış Siparişi Detayları
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${getSaleOrderStatusColor(
|
className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${getSaleOrderStatusColor(
|
||||||
order.status
|
order.status,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getSaleOrderStatusText(order.status)}
|
{getSaleOrderStatusText(order.status)}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() => navigate(`/admin/crm/sales-orders/edit/${order.id}`)}
|
||||||
navigate(`/admin/crm/sales-orders/edit/${order.id}`)
|
|
||||||
}
|
|
||||||
className="bg-blue-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-blue-700 transition-colors flex items-center"
|
className="bg-blue-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-blue-700 transition-colors flex items-center"
|
||||||
>
|
>
|
||||||
<FaEdit className="mr-2" />
|
<FaEdit className="mr-2" />
|
||||||
|
|
@ -134,11 +126,9 @@ const SalesOrderView: React.FC = () => {
|
||||||
<div className="bg-gradient-to-r from-blue-50 to-blue-100 p-3 rounded-lg border border-blue-200">
|
<div className="bg-gradient-to-r from-blue-50 to-blue-100 p-3 rounded-lg border border-blue-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-blue-800">
|
<p className="text-sm font-medium text-blue-800">Sipariş Tarihi</p>
|
||||||
Sipariş Tarihi
|
|
||||||
</p>
|
|
||||||
<p className="text-base font-bold text-blue-900">
|
<p className="text-base font-bold text-blue-900">
|
||||||
{new Date(order.orderDate).toLocaleDateString("tr-TR")}
|
{new Date(order.orderDate).toLocaleDateString('tr-TR')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FaCalendar className="text-2xl text-blue-600" />
|
<FaCalendar className="text-2xl text-blue-600" />
|
||||||
|
|
@ -148,13 +138,9 @@ const SalesOrderView: React.FC = () => {
|
||||||
<div className="bg-gradient-to-r from-green-50 to-green-100 p-3 rounded-lg border border-green-200">
|
<div className="bg-gradient-to-r from-green-50 to-green-100 p-3 rounded-lg border border-green-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-green-800">
|
<p className="text-sm font-medium text-green-800">Talep Edilen Tarih</p>
|
||||||
Talep Edilen Tarih
|
|
||||||
</p>
|
|
||||||
<p className="text-base font-bold text-green-900">
|
<p className="text-base font-bold text-green-900">
|
||||||
{new Date(order.requestedDeliveryDate).toLocaleDateString(
|
{new Date(order.requestedDeliveryDate).toLocaleDateString('tr-TR')}
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FaCalendar className="text-2xl text-green-600" />
|
<FaCalendar className="text-2xl text-green-600" />
|
||||||
|
|
@ -164,15 +150,11 @@ const SalesOrderView: React.FC = () => {
|
||||||
<div className="bg-gradient-to-r from-purple-50 to-purple-100 p-3 rounded-lg border border-purple-200">
|
<div className="bg-gradient-to-r from-purple-50 to-purple-100 p-3 rounded-lg border border-purple-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-purple-800">
|
<p className="text-sm font-medium text-purple-800">Onaylanan Tarih</p>
|
||||||
Onaylanan Tarih
|
|
||||||
</p>
|
|
||||||
<p className="text-base font-bold text-purple-900">
|
<p className="text-base font-bold text-purple-900">
|
||||||
{order.confirmedDeliveryDate
|
{order.confirmedDeliveryDate
|
||||||
? new Date(order.confirmedDeliveryDate).toLocaleDateString(
|
? new Date(order.confirmedDeliveryDate).toLocaleDateString('tr-TR')
|
||||||
"tr-TR"
|
: 'Belirtilmemiş'}
|
||||||
)
|
|
||||||
: "Belirtilmemiş"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FaCalendar className="text-2xl text-purple-600" />
|
<FaCalendar className="text-2xl text-purple-600" />
|
||||||
|
|
@ -182,11 +164,9 @@ const SalesOrderView: React.FC = () => {
|
||||||
<div className="bg-gradient-to-r from-amber-50 to-amber-100 p-3 rounded-lg border border-amber-200">
|
<div className="bg-gradient-to-r from-amber-50 to-amber-100 p-3 rounded-lg border border-amber-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-amber-800">
|
<p className="text-sm font-medium text-amber-800">Toplam Tutar</p>
|
||||||
Toplam Tutar
|
|
||||||
</p>
|
|
||||||
<p className="text-base font-bold text-amber-900">
|
<p className="text-base font-bold text-amber-900">
|
||||||
{order.totalAmount.toLocaleString("tr-TR")} {order.currency}
|
{order.totalAmount.toLocaleString('tr-TR')} {order.currency}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FaCalculator className="text-2xl text-amber-600" />
|
<FaCalculator className="text-2xl text-amber-600" />
|
||||||
|
|
@ -211,8 +191,8 @@ const SalesOrderView: React.FC = () => {
|
||||||
<span className="font-medium">Son Güncelleme:</span>
|
<span className="font-medium">Son Güncelleme:</span>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
{order.lastModificationTime
|
{order.lastModificationTime
|
||||||
? new Date(order.lastModificationTime).toLocaleString("tr-TR")
|
? new Date(order.lastModificationTime).toLocaleString('tr-TR')
|
||||||
: "Güncelleme yok"}
|
: 'Güncelleme yok'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -233,32 +213,24 @@ const SalesOrderView: React.FC = () => {
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-sm font-medium text-gray-600">
|
<span className="text-sm font-medium text-gray-600">Firma Adı:</span>
|
||||||
Firma Adı:
|
|
||||||
</span>
|
|
||||||
<p className="text-sm font-medium text-gray-900">
|
<p className="text-sm font-medium text-gray-900">
|
||||||
{order.customer?.name || "Müşteri bilgisi yok"}
|
{order.customer?.name || 'Müşteri bilgisi yok'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-sm font-medium text-gray-600">
|
<span className="text-sm font-medium text-gray-600">İletişim Kişisi:</span>
|
||||||
İletişim Kişisi:
|
|
||||||
</span>
|
|
||||||
<p className="text-sm text-gray-900">
|
<p className="text-sm text-gray-900">
|
||||||
{order.customer?.primaryContact?.fullName || "Belirtilmemiş"}
|
{order.customer?.primaryContact?.fullName || 'Belirtilmemiş'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-sm text-gray-600">
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
<FaPhone className="mr-2" />
|
<FaPhone className="mr-2" />
|
||||||
<span>
|
<span>{order.customer?.primaryContact?.phone || 'Belirtilmemiş'}</span>
|
||||||
{order.customer?.primaryContact?.phone || "Belirtilmemiş"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-sm text-gray-600">
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
<FaEnvelope className="mr-2" />
|
<FaEnvelope className="mr-2" />
|
||||||
<span>
|
<span>{order.customer?.primaryContact?.email || 'Belirtilmemiş'}</span>
|
||||||
{order.customer?.primaryContact?.email || "Belirtilmemiş"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -313,7 +285,7 @@ const SalesOrderView: React.FC = () => {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{order.items.map((item: CrmSalesOrderItem) => {
|
{order.items.map((item: CrmSalesOrderItem) => {
|
||||||
const itemStatus = getSaleOrderItemStatusnfo(item.status);
|
const itemStatus = getSaleOrderItemStatusnfo(item.status)
|
||||||
return (
|
return (
|
||||||
<tr key={item.id} className="hover:bg-gray-50">
|
<tr key={item.id} className="hover:bg-gray-50">
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
|
|
@ -327,40 +299,32 @@ const SalesOrderView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
|
||||||
{item.quantity} {item.material?.baseUnit?.name || "Adet"}
|
{item.quantity} {item.material?.baseUnit?.name || 'Adet'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{item.deliveredQuantity}{" "}
|
{item.deliveredQuantity} {item.material?.baseUnit?.name || 'Adet'}
|
||||||
{item.material?.baseUnit?.name || "Adet"}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
{(
|
{((item.deliveredQuantity / item.quantity) * 100).toFixed(1)}% tamamlandı
|
||||||
(item.deliveredQuantity / item.quantity) *
|
|
||||||
100
|
|
||||||
).toFixed(1)}
|
|
||||||
% tamamlandı
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
|
||||||
{item.unitPrice.toLocaleString("tr-TR")} {order.currency}
|
{item.unitPrice.toLocaleString('tr-TR')} {order.currency}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap text-sm font-medium text-gray-900">
|
<td className="px-4 py-2 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
{item.totalAmount.toLocaleString("tr-TR")}{" "}
|
{item.totalAmount.toLocaleString('tr-TR')} {order.currency}
|
||||||
{order.currency}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${itemStatus.color}`}
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${itemStatus.color}`}
|
||||||
>
|
>
|
||||||
<itemStatus.icon
|
<itemStatus.icon className={`mr-1 ${itemStatus.iconColor}`} />
|
||||||
className={`mr-1 ${itemStatus.iconColor}`}
|
|
||||||
/>
|
|
||||||
{itemStatus.label}
|
{itemStatus.label}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -377,33 +341,27 @@ const SalesOrderView: React.FC = () => {
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between py-1.5 border-b border-gray-100">
|
<div className="flex justify-between py-1.5 border-b border-gray-100">
|
||||||
<span className="text-sm font-medium text-gray-600">
|
<span className="text-sm font-medium text-gray-600">Ara Toplam:</span>
|
||||||
Ara Toplam:
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-gray-900">
|
<span className="text-sm text-gray-900">
|
||||||
{order.subtotal.toLocaleString("tr-TR")} {order.currency}
|
{order.subtotal.toLocaleString('tr-TR')} {order.currency}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between py-1.5 border-b border-gray-100">
|
<div className="flex justify-between py-1.5 border-b border-gray-100">
|
||||||
<span className="text-sm font-medium text-gray-600">
|
<span className="text-sm font-medium text-gray-600">İndirim:</span>
|
||||||
İndirim:
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-gray-900">
|
<span className="text-sm text-gray-900">
|
||||||
-{order.discountAmount.toLocaleString("tr-TR")} {order.currency}
|
-{order.discountAmount.toLocaleString('tr-TR')} {order.currency}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between py-1.5 border-b border-gray-100">
|
<div className="flex justify-between py-1.5 border-b border-gray-100">
|
||||||
<span className="text-sm font-medium text-gray-600">KDV:</span>
|
<span className="text-sm font-medium text-gray-600">KDV:</span>
|
||||||
<span className="text-sm text-gray-900">
|
<span className="text-sm text-gray-900">
|
||||||
{order.taxAmount.toLocaleString("tr-TR")} {order.currency}
|
{order.taxAmount.toLocaleString('tr-TR')} {order.currency}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between py-2 border-t-2 border-gray-200">
|
<div className="flex justify-between py-2 border-t-2 border-gray-200">
|
||||||
|
<span className="text-base font-bold text-gray-900">Genel Toplam:</span>
|
||||||
<span className="text-base font-bold text-gray-900">
|
<span className="text-base font-bold text-gray-900">
|
||||||
Genel Toplam:
|
{order.totalAmount.toLocaleString('tr-TR')} {order.currency}
|
||||||
</span>
|
|
||||||
<span className="text-base font-bold text-gray-900">
|
|
||||||
{order.totalAmount.toLocaleString("tr-TR")} {order.currency}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -417,20 +375,18 @@ const SalesOrderView: React.FC = () => {
|
||||||
<div className="text-sm text-blue-900">
|
<div className="text-sm text-blue-900">
|
||||||
<p>Toplam Kalem: {order.items.length}</p>
|
<p>Toplam Kalem: {order.items.length}</p>
|
||||||
<p>
|
<p>
|
||||||
Teslim Edilenler:{" "}
|
Teslim Edilenler:{' '}
|
||||||
{
|
{
|
||||||
order.items.filter(
|
order.items.filter(
|
||||||
(item) =>
|
(item) => item.status === SaleOrderItemStatusEnum.Delivered,
|
||||||
item.status === SaleOrderItemStatusEnum.Delivered
|
|
||||||
).length
|
).length
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Bekleyenler:{" "}
|
Bekleyenler:{' '}
|
||||||
{
|
{
|
||||||
order.items.filter(
|
order.items.filter(
|
||||||
(item) =>
|
(item) => item.status !== SaleOrderItemStatusEnum.Delivered,
|
||||||
item.status !== SaleOrderItemStatusEnum.Delivered
|
|
||||||
).length
|
).length
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -450,7 +406,8 @@ const SalesOrderView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default SalesOrderView;
|
export default SalesOrderView
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
FaShoppingCart,
|
FaShoppingCart,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
|
|
@ -9,146 +9,128 @@ import {
|
||||||
FaCalendar,
|
FaCalendar,
|
||||||
FaBox,
|
FaBox,
|
||||||
FaBuilding,
|
FaBuilding,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import { CrmSalesOrder, SaleOrderStatusEnum } from "../../../types/crm";
|
import { CrmSalesOrder, SaleOrderStatusEnum } from '../../../types/crm'
|
||||||
import DataTable, { Column } from "../../../components/common/DataTable";
|
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import { mockSalesOrders } from "../../../mocks/mockSalesOrders";
|
import { mockSalesOrders } from '../../../mocks/mockSalesOrders'
|
||||||
import { BusinessParty } from "../../../types/common";
|
import { BusinessParty } from '../../../types/common'
|
||||||
import Widget from "../../../components/common/Widget";
|
import Widget from '../../../components/common/Widget'
|
||||||
import {
|
import { getSaleOrderStatusColor, getSaleOrderStatusText } from '../../../utils/erp'
|
||||||
getSaleOrderStatusColor,
|
import { Container } from '@/components/shared'
|
||||||
getSaleOrderStatusText,
|
|
||||||
} from "../../../utils/erp";
|
|
||||||
|
|
||||||
const SalesOrders: React.FC = () => {
|
const SalesOrders: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const [salesOrders] = useState<CrmSalesOrder[]>(mockSalesOrders);
|
const [salesOrders] = useState<CrmSalesOrder[]>(mockSalesOrders)
|
||||||
const [customers] = useState<BusinessParty[]>(mockBusinessParties);
|
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [selectedStatus, setSelectedStatus] = useState<
|
const [selectedStatus, setSelectedStatus] = useState<SaleOrderStatusEnum | 'all'>('all')
|
||||||
SaleOrderStatusEnum | "all"
|
const [selectedCustomer, setSelectedCustomer] = useState<string>('all')
|
||||||
>("all");
|
|
||||||
const [selectedCustomer, setSelectedCustomer] = useState<string>("all");
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
navigate("/admin/crm/sales-orders/new");
|
navigate('/admin/crm/sales-orders/new')
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEdit = (order: CrmSalesOrder) => {
|
const handleEdit = (order: CrmSalesOrder) => {
|
||||||
navigate(`/admin/crm/sales-orders/edit/${order.id}`);
|
navigate(`/admin/crm/sales-orders/edit/${order.id}`)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
console.log("Delete sales order:", id);
|
console.log('Delete sales order:', id)
|
||||||
// Implement delete functionality
|
// Implement delete functionality
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleViewDetails = (order: CrmSalesOrder) => {
|
const handleViewDetails = (order: CrmSalesOrder) => {
|
||||||
navigate(`/admin/crm/sales-orders/${order.id}`);
|
navigate(`/admin/crm/sales-orders/${order.id}`)
|
||||||
};
|
}
|
||||||
|
|
||||||
const filteredOrders = salesOrders.filter((order) => {
|
const filteredOrders = salesOrders.filter((order) => {
|
||||||
if (
|
if (searchTerm && !order.orderNumber.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||||
searchTerm &&
|
return false
|
||||||
!order.orderNumber.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if (selectedStatus !== "all" && order.status !== selectedStatus) {
|
if (selectedStatus !== 'all' && order.status !== selectedStatus) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if (selectedCustomer !== "all" && order.customerId !== selectedCustomer) {
|
if (selectedCustomer !== 'all' && order.customerId !== selectedCustomer) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
return true;
|
return true
|
||||||
});
|
})
|
||||||
|
|
||||||
const columns: Column<CrmSalesOrder>[] = [
|
const columns: Column<CrmSalesOrder>[] = [
|
||||||
{
|
{
|
||||||
key: "orderNumber",
|
key: 'orderNumber',
|
||||||
header: "Sipariş No",
|
header: 'Sipariş No',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (order: CrmSalesOrder) => (
|
render: (order: CrmSalesOrder) => (
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900">{order.orderNumber}</div>
|
<div className="font-medium text-gray-900">{order.orderNumber}</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">Sipariş #{order.id.substring(0, 8)}</div>
|
||||||
Sipariş #{order.id.substring(0, 8)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "customer",
|
key: 'customer',
|
||||||
header: "Müşteri",
|
header: 'Müşteri',
|
||||||
render: (order: CrmSalesOrder) => {
|
render: (order: CrmSalesOrder) => {
|
||||||
const customer = customers.find((c) => c.id === order.customerId);
|
const customer = customers.find((c) => c.id === order.customerId)
|
||||||
return customer ? (
|
return customer ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaBuilding className="w-4 h-4 text-gray-400" />
|
<FaBuilding className="w-4 h-4 text-gray-400" />
|
||||||
<span>{customer.name}</span>
|
<span>{customer.name}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
'-'
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "orderDate",
|
key: 'orderDate',
|
||||||
header: "Sipariş Tarihi",
|
header: 'Sipariş Tarihi',
|
||||||
render: (order: CrmSalesOrder) => (
|
render: (order: CrmSalesOrder) => (
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="flex items-center gap-1 text-sm">
|
||||||
<FaCalendar className="w-3 h-3 text-gray-400" />
|
<FaCalendar className="w-3 h-3 text-gray-400" />
|
||||||
<span>{new Date(order.orderDate).toLocaleDateString("tr-TR")}</span>
|
<span>{new Date(order.orderDate).toLocaleDateString('tr-TR')}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "deliveryDate",
|
key: 'deliveryDate',
|
||||||
header: "Teslimat Tarihi",
|
header: 'Teslimat Tarihi',
|
||||||
render: (order: CrmSalesOrder) =>
|
render: (order: CrmSalesOrder) =>
|
||||||
order.confirmedDeliveryDate ? (
|
order.confirmedDeliveryDate ? (
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="flex items-center gap-1 text-sm">
|
||||||
<FaBox className="w-3 h-3 text-gray-400" />
|
<FaBox className="w-3 h-3 text-gray-400" />
|
||||||
<span>
|
<span>{new Date(order.confirmedDeliveryDate).toLocaleDateString('tr-TR')}</span>
|
||||||
{new Date(order.confirmedDeliveryDate).toLocaleDateString(
|
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : order.requestedDeliveryDate ? (
|
) : order.requestedDeliveryDate ? (
|
||||||
<div className="flex items-center gap-1 text-sm text-gray-500">
|
<div className="flex items-center gap-1 text-sm text-gray-500">
|
||||||
<FaBox className="w-3 h-3 text-gray-400" />
|
<FaBox className="w-3 h-3 text-gray-400" />
|
||||||
<span>
|
<span>
|
||||||
{new Date(order.requestedDeliveryDate).toLocaleDateString(
|
{new Date(order.requestedDeliveryDate).toLocaleDateString('tr-TR')} (İstenen)
|
||||||
"tr-TR"
|
|
||||||
)}{" "}
|
|
||||||
(İstenen)
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
'-'
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "totalAmount",
|
key: 'totalAmount',
|
||||||
header: "Toplam Tutar",
|
header: 'Toplam Tutar',
|
||||||
render: (order: CrmSalesOrder) => (
|
render: (order: CrmSalesOrder) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FaDollarSign className="w-4 h-4 text-gray-400" />
|
<FaDollarSign className="w-4 h-4 text-gray-400" />
|
||||||
<span className="font-medium">
|
<span className="font-medium">₺{order.totalAmount.toLocaleString()}</span>
|
||||||
₺{order.totalAmount.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: 'status',
|
||||||
header: "Durum",
|
header: 'Durum',
|
||||||
render: (order: CrmSalesOrder) => (
|
render: (order: CrmSalesOrder) => (
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getSaleOrderStatusColor(
|
className={`px-2 py-1 text-xs font-medium rounded-full ${getSaleOrderStatusColor(
|
||||||
order.status
|
order.status,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getSaleOrderStatusText(order.status)}
|
{getSaleOrderStatusText(order.status)}
|
||||||
|
|
@ -156,8 +138,8 @@ const SalesOrders: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: 'actions',
|
||||||
header: "İşlemler",
|
header: 'İşlemler',
|
||||||
render: (order: CrmSalesOrder) => (
|
render: (order: CrmSalesOrder) => (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -184,61 +166,49 @@ const SalesOrders: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
// Calculate statistics
|
// Calculate statistics
|
||||||
const totalOrders = salesOrders.length;
|
const totalOrders = salesOrders.length
|
||||||
const activeOrders = salesOrders.filter(
|
const activeOrders = salesOrders.filter(
|
||||||
(o) =>
|
(o) =>
|
||||||
o.status === SaleOrderStatusEnum.Confirmed ||
|
o.status === SaleOrderStatusEnum.Confirmed || o.status === SaleOrderStatusEnum.InProduction,
|
||||||
o.status === SaleOrderStatusEnum.InProduction
|
).length
|
||||||
).length;
|
|
||||||
const totalRevenue = salesOrders
|
const totalRevenue = salesOrders
|
||||||
.filter((o) => o.status === SaleOrderStatusEnum.Delivered)
|
.filter((o) => o.status === SaleOrderStatusEnum.Delivered)
|
||||||
.reduce((sum, order) => sum + order.totalAmount, 0);
|
.reduce((sum, order) => sum + order.totalAmount, 0)
|
||||||
const averageOrderValue =
|
const averageOrderValue =
|
||||||
salesOrders.reduce((sum, order) => sum + order.totalAmount, 0) /
|
salesOrders.reduce((sum, order) => sum + order.totalAmount, 0) / salesOrders.length || 0
|
||||||
salesOrders.length || 0;
|
|
||||||
|
|
||||||
// Status distribution
|
// Status distribution
|
||||||
const statusDistribution = Object.values(SaleOrderStatusEnum).map(
|
const statusDistribution = Object.values(SaleOrderStatusEnum).map((status) => ({
|
||||||
(status) => ({
|
|
||||||
status,
|
status,
|
||||||
count: salesOrders.filter((o) => o.status === status).length,
|
count: salesOrders.filter((o) => o.status === status).length,
|
||||||
value: salesOrders
|
value: salesOrders
|
||||||
.filter((o) => o.status === status)
|
.filter((o) => o.status === status)
|
||||||
.reduce((sum, o) => sum + o.totalAmount, 0),
|
.reduce((sum, o) => sum + o.totalAmount, 0),
|
||||||
})
|
}))
|
||||||
);
|
|
||||||
|
|
||||||
// Monthly trend
|
// Monthly trend
|
||||||
const currentMonth = new Date();
|
const currentMonth = new Date()
|
||||||
const lastMonth = new Date(
|
const lastMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)
|
||||||
currentMonth.getFullYear(),
|
const currentMonthOrders = salesOrders.filter((o) => new Date(o.orderDate) >= lastMonth).length
|
||||||
currentMonth.getMonth() - 1,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
const currentMonthOrders = salesOrders.filter(
|
|
||||||
(o) => new Date(o.orderDate) >= lastMonth
|
|
||||||
).length;
|
|
||||||
const previousMonthOrders = salesOrders.filter((o) => {
|
const previousMonthOrders = salesOrders.filter((o) => {
|
||||||
const orderDate = new Date(o.orderDate);
|
const orderDate = new Date(o.orderDate)
|
||||||
return (
|
return (
|
||||||
orderDate >=
|
orderDate >= new Date(lastMonth.getFullYear(), lastMonth.getMonth() - 1, 1) &&
|
||||||
new Date(lastMonth.getFullYear(), lastMonth.getMonth() - 1, 1) &&
|
|
||||||
orderDate < lastMonth
|
orderDate < lastMonth
|
||||||
);
|
)
|
||||||
}).length;
|
}).length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">Satış Siparişleri</h2>
|
<h2 className="text-xl font-bold text-gray-900">Satış Siparişleri</h2>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">Müşteri siparişleri ve teslimat takibi</p>
|
||||||
Müşteri siparişleri ve teslimat takibi
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
|
|
@ -251,19 +221,9 @@ const SalesOrders: React.FC = () => {
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||||
<Widget
|
<Widget title="Toplam Sipariş" value={totalOrders} color="blue" icon="FaShoppingCart" />
|
||||||
title="Toplam Sipariş"
|
|
||||||
value={totalOrders}
|
|
||||||
color="blue"
|
|
||||||
icon="FaShoppingCart"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget title="Aktif Sipariş" value={activeOrders} color="orange" icon="FaBox" />
|
||||||
title="Aktif Sipariş"
|
|
||||||
value={activeOrders}
|
|
||||||
color="orange"
|
|
||||||
icon="FaBox"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget
|
||||||
title="Toplam Gelir"
|
title="Toplam Gelir"
|
||||||
|
|
@ -282,28 +242,19 @@ const SalesOrders: React.FC = () => {
|
||||||
|
|
||||||
{/* Order Status Pipeline */}
|
{/* Order Status Pipeline */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-3">
|
<div className="bg-white rounded-lg shadow-sm border p-3">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">
|
<h3 className="text-sm font-semibold text-gray-900 mb-3">Sipariş Durumu Dağılımı</h3>
|
||||||
Sipariş Durumu Dağılımı
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-7 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-7 gap-2">
|
||||||
{statusDistribution.map(({ status, count, value }) => (
|
{statusDistribution.map(({ status, count, value }) => (
|
||||||
<div
|
<div key={status as string} className="text-center p-2 border rounded-lg">
|
||||||
key={status as string}
|
|
||||||
className="text-center p-2 border rounded-lg"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getSaleOrderStatusColor(
|
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getSaleOrderStatusColor(
|
||||||
status as SaleOrderStatusEnum
|
status as SaleOrderStatusEnum,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getSaleOrderStatusText(status as SaleOrderStatusEnum)}
|
{getSaleOrderStatusText(status as SaleOrderStatusEnum)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold text-gray-900 mb-1">
|
<div className="text-xl font-bold text-gray-900 mb-1">{count}</div>
|
||||||
{count}
|
<div className="text-xs text-gray-500">₺{value.toLocaleString()}</div>
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
₺{value.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -311,39 +262,29 @@ const SalesOrders: React.FC = () => {
|
||||||
|
|
||||||
{/* Monthly Performance */}
|
{/* Monthly Performance */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-3">
|
<div className="bg-white rounded-lg shadow-sm border p-3">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">
|
<h3 className="text-sm font-semibold text-gray-900 mb-3">Aylık Performans</h3>
|
||||||
Aylık Performans
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-blue-600 mb-1">
|
<div className="text-2xl font-bold text-blue-600 mb-1">{currentMonthOrders}</div>
|
||||||
{currentMonthOrders}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-600">Bu Ay</p>
|
<p className="text-sm text-gray-600">Bu Ay</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-gray-600 mb-1">
|
<div className="text-2xl font-bold text-gray-600 mb-1">{previousMonthOrders}</div>
|
||||||
{previousMonthOrders}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-600">Geçen Ay</p>
|
<p className="text-sm text-gray-600">Geçen Ay</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div
|
||||||
className={`text-2xl font-bold mb-1 ${
|
className={`text-2xl font-bold mb-1 ${
|
||||||
currentMonthOrders >= previousMonthOrders
|
currentMonthOrders >= previousMonthOrders ? 'text-green-600' : 'text-red-600'
|
||||||
? "text-green-600"
|
|
||||||
: "text-red-600"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{previousMonthOrders > 0
|
{previousMonthOrders > 0
|
||||||
? `${Math.round(
|
? `${Math.round(
|
||||||
((currentMonthOrders - previousMonthOrders) /
|
((currentMonthOrders - previousMonthOrders) / previousMonthOrders) * 100,
|
||||||
previousMonthOrders) *
|
|
||||||
100
|
|
||||||
)}%`
|
)}%`
|
||||||
: "-%"}
|
: '-%'}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">Değişim</p>
|
<p className="text-sm text-gray-600">Değişim</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -364,9 +305,7 @@ const SalesOrders: React.FC = () => {
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={selectedStatus}
|
value={selectedStatus}
|
||||||
onChange={(e) =>
|
onChange={(e) => setSelectedStatus(e.target.value as SaleOrderStatusEnum | 'all')}
|
||||||
setSelectedStatus(e.target.value as SaleOrderStatusEnum | "all")
|
|
||||||
}
|
|
||||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">Tüm Durumlar</option>
|
<option value="all">Tüm Durumlar</option>
|
||||||
|
|
@ -399,16 +338,13 @@ const SalesOrders: React.FC = () => {
|
||||||
{filteredOrders.length === 0 && (
|
{filteredOrders.length === 0 && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaShoppingCart className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
<FaShoppingCart className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||||
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
<h3 className="text-sm font-medium text-gray-900 mb-2">Sipariş bulunamadı</h3>
|
||||||
Sipariş bulunamadı
|
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500">
|
|
||||||
Arama kriterlerinizi değiştirmeyi deneyin.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default SalesOrders;
|
export default SalesOrders
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
FaUsers,
|
FaUsers,
|
||||||
FaArrowLeft,
|
FaArrowLeft,
|
||||||
|
|
@ -7,158 +7,144 @@ import {
|
||||||
FaPlus,
|
FaPlus,
|
||||||
FaTrash,
|
FaTrash,
|
||||||
FaMapMarkerAlt,
|
FaMapMarkerAlt,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { CrmTerritory } from "../../../types/crm";
|
import { CrmTerritory } from '../../../types/crm'
|
||||||
import { mockEmployees } from "../../../mocks/mockEmployees";
|
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||||
import { Team, TeamMember, TeamRoleEnum } from "../../../types/common";
|
import { Team, TeamMember, TeamRoleEnum } from '../../../types/common'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const SalesTeamCreate: React.FC = () => {
|
const SalesTeamCreate: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
teamCode: "",
|
teamCode: '',
|
||||||
name: "",
|
name: '',
|
||||||
description: "",
|
description: '',
|
||||||
managerId: "",
|
managerId: '',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
const [members, setMembers] = useState<Partial<TeamMember>[]>([]);
|
const [members, setMembers] = useState<Partial<TeamMember>[]>([])
|
||||||
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([]);
|
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([])
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string | boolean) => {
|
const handleInputChange = (field: string, value: string | boolean) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors((prev) => ({ ...prev, [field]: "" }));
|
setErrors((prev) => ({ ...prev, [field]: '' }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const addMember = () => {
|
const addMember = () => {
|
||||||
setMembers((prev) => [
|
setMembers((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
employeeId: "",
|
employeeId: '',
|
||||||
role: TeamRoleEnum.Member,
|
role: TeamRoleEnum.Member,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
};
|
}
|
||||||
|
|
||||||
const removeMember = (index: number) => {
|
const removeMember = (index: number) => {
|
||||||
setMembers((prev) => prev.filter((_, i) => i !== index));
|
setMembers((prev) => prev.filter((_, i) => i !== index))
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateMember = (
|
const updateMember = (index: number, field: string, value: string | TeamRoleEnum | boolean) => {
|
||||||
index: number,
|
|
||||||
field: string,
|
|
||||||
value: string | TeamRoleEnum | boolean
|
|
||||||
) => {
|
|
||||||
setMembers((prev) =>
|
setMembers((prev) =>
|
||||||
prev.map((member, i) =>
|
prev.map((member, i) => (i === index ? { ...member, [field]: value } : member)),
|
||||||
i === index ? { ...member, [field]: value } : member
|
|
||||||
)
|
)
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const addTerritory = () => {
|
const addTerritory = () => {
|
||||||
setTerritories((prev) => [
|
setTerritories((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
territoryCode: "",
|
territoryCode: '',
|
||||||
name: "",
|
name: '',
|
||||||
description: "",
|
description: '',
|
||||||
region: "",
|
region: '',
|
||||||
countries: [],
|
countries: [],
|
||||||
cities: [],
|
cities: [],
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
};
|
}
|
||||||
|
|
||||||
const removeTerritory = (index: number) => {
|
const removeTerritory = (index: number) => {
|
||||||
setTerritories((prev) => prev.filter((_, i) => i !== index));
|
setTerritories((prev) => prev.filter((_, i) => i !== index))
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateTerritory = (
|
const updateTerritory = (index: number, field: string, value: string | string[] | boolean) => {
|
||||||
index: number,
|
|
||||||
field: string,
|
|
||||||
value: string | string[] | boolean
|
|
||||||
) => {
|
|
||||||
setTerritories((prev) =>
|
setTerritories((prev) =>
|
||||||
prev.map((territory, i) =>
|
prev.map((territory, i) => (i === index ? { ...territory, [field]: value } : territory)),
|
||||||
i === index ? { ...territory, [field]: value } : territory
|
|
||||||
)
|
)
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {}
|
||||||
|
|
||||||
if (!formData.teamCode.trim()) {
|
if (!formData.teamCode.trim()) {
|
||||||
newErrors.teamCode = "Takım kodu zorunludur";
|
newErrors.teamCode = 'Takım kodu zorunludur'
|
||||||
}
|
}
|
||||||
if (!formData.name.trim()) {
|
if (!formData.name.trim()) {
|
||||||
newErrors.name = "Takım adı zorunludur";
|
newErrors.name = 'Takım adı zorunludur'
|
||||||
}
|
}
|
||||||
if (!formData.managerId.trim()) {
|
if (!formData.managerId.trim()) {
|
||||||
newErrors.managerId = "Takım lideri seçimi zorunludur";
|
newErrors.managerId = 'Takım lideri seçimi zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors)
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return
|
||||||
|
|
||||||
const newTeam: Partial<Team> = {
|
const newTeam: Partial<Team> = {
|
||||||
...formData,
|
...formData,
|
||||||
members: members.map((member, index) => ({
|
members: members.map((member, index) => ({
|
||||||
...member,
|
...member,
|
||||||
id: `member-new-${index}`,
|
id: `member-new-${index}`,
|
||||||
teamId: "new-team",
|
teamId: 'new-team',
|
||||||
joinDate: new Date(),
|
joinDate: new Date(),
|
||||||
})) as TeamMember[],
|
})) as TeamMember[],
|
||||||
territories: territories.map((territory, index) => ({
|
territories: territories.map((territory, index) => ({
|
||||||
...territory,
|
...territory,
|
||||||
id: `territory-new-${index}`,
|
id: `territory-new-${index}`,
|
||||||
assignedTeamId: "new-team",
|
assignedTeamId: 'new-team',
|
||||||
})) as CrmTerritory[],
|
})) as CrmTerritory[],
|
||||||
targets: [],
|
targets: [],
|
||||||
creationTime: new Date(),
|
creationTime: new Date(),
|
||||||
lastModificationTime: new Date(),
|
lastModificationTime: new Date(),
|
||||||
};
|
}
|
||||||
|
|
||||||
console.log("Creating new sales team:", newTeam);
|
console.log('Creating new sales team:', newTeam)
|
||||||
alert("Satış ekibi başarıyla oluşturuldu!");
|
alert('Satış ekibi başarıyla oluşturuldu!')
|
||||||
navigate("/admin/crm/sales-teams");
|
navigate('/admin/crm/sales-teams')
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (
|
if (confirm('Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?')) {
|
||||||
confirm("Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?")
|
navigate('/admin/crm/sales-teams')
|
||||||
) {
|
}
|
||||||
navigate("/admin/crm/sales-teams");
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-4 pt-2">
|
<div className="space-y-4 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/sales-teams")}
|
onClick={() => navigate('/admin/crm/sales-teams')}
|
||||||
className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-5 h-5" />
|
<FaArrowLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
<h2 className="text-xl font-bold text-gray-900">Yeni Satış Ekibi</h2>
|
||||||
Yeni Satış Ekibi
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
Yeni satış ekibi oluşturun ve ekip üyelerini atayın
|
Yeni satış ekibi oluşturun ve ekip üyelerini atayın
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -201,11 +187,9 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.teamCode}
|
value={formData.teamCode}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('teamCode', e.target.value)}
|
||||||
handleInputChange("teamCode", e.target.value)
|
|
||||||
}
|
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
errors.teamCode ? "border-red-500" : "border-gray-300"
|
errors.teamCode ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="ST-001"
|
placeholder="ST-001"
|
||||||
/>
|
/>
|
||||||
|
|
@ -221,11 +205,9 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={formData.managerId}
|
value={formData.managerId}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('managerId', e.target.value)}
|
||||||
handleInputChange("managerId", e.target.value)
|
|
||||||
}
|
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
errors.managerId ? "border-red-500" : "border-gray-300"
|
errors.managerId ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">Çalışan seçin</option>
|
<option value="">Çalışan seçin</option>
|
||||||
|
|
@ -237,40 +219,30 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{errors.managerId && (
|
{errors.managerId && (
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<p className="text-red-500 text-sm mt-1">{errors.managerId}</p>
|
||||||
{errors.managerId}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Takım Adı *</label>
|
||||||
Takım Adı *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
errors.name ? "border-red-500" : "border-gray-300"
|
errors.name ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Kurumsal Satış Ekibi"
|
placeholder="Kurumsal Satış Ekibi"
|
||||||
/>
|
/>
|
||||||
{errors.name && (
|
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Açıklama</label>
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||||
handleInputChange("description", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Ekip açıklaması..."
|
placeholder="Ekip açıklaması..."
|
||||||
|
|
@ -282,9 +254,7 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.isActive}
|
checked={formData.isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('isActive', e.target.checked)}
|
||||||
handleInputChange("isActive", e.target.checked)
|
|
||||||
}
|
|
||||||
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span className="ml-2 text-sm text-gray-700">Aktif</span>
|
<span className="ml-2 text-sm text-gray-700">Aktif</span>
|
||||||
|
|
@ -320,10 +290,8 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
Çalışan
|
Çalışan
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={member.employeeId || ""}
|
value={member.employeeId || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateMember(index, 'employeeId', e.target.value)}
|
||||||
updateMember(index, "employeeId", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value="">Çalışan seçin</option>
|
<option value="">Çalışan seçin</option>
|
||||||
|
|
@ -335,14 +303,10 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Rol</label>
|
||||||
Rol
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={member.role || TeamRoleEnum.Member}
|
value={member.role || TeamRoleEnum.Member}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateMember(index, 'role', e.target.value)}
|
||||||
updateMember(index, "role", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value={TeamRoleEnum.Member}>Üye</option>
|
<option value={TeamRoleEnum.Member}>Üye</option>
|
||||||
|
|
@ -364,9 +328,7 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
||||||
<p>Henüz ekip üyesi eklenmedi</p>
|
<p>Henüz ekip üyesi eklenmedi</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">Üye eklemek için yukarıdaki butona tıklayın</p>
|
||||||
Üye eklemek için yukarıdaki butona tıklayın
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -392,14 +354,9 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{territories.map((territory, index) => (
|
{territories.map((territory, index) => (
|
||||||
<div
|
<div key={index} className="p-3 border border-gray-200 rounded-lg">
|
||||||
key={index}
|
|
||||||
className="p-3 border border-gray-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start mb-3">
|
<div className="flex justify-between items-start mb-3">
|
||||||
<h4 className="font-medium text-gray-900">
|
<h4 className="font-medium text-gray-900">Bölge {index + 1}</h4>
|
||||||
Bölge {index + 1}
|
|
||||||
</h4>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => removeTerritory(index)}
|
onClick={() => removeTerritory(index)}
|
||||||
className="p-1.5 text-red-600 hover:bg-red-50 rounded"
|
className="p-1.5 text-red-600 hover:bg-red-50 rounded"
|
||||||
|
|
@ -415,14 +372,8 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={territory.territoryCode || ""}
|
value={territory.territoryCode || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateTerritory(index, 'territoryCode', e.target.value)}
|
||||||
updateTerritory(
|
|
||||||
index,
|
|
||||||
"territoryCode",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="TR-IST"
|
placeholder="TR-IST"
|
||||||
/>
|
/>
|
||||||
|
|
@ -434,10 +385,8 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={territory.name || ""}
|
value={territory.name || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateTerritory(index, 'name', e.target.value)}
|
||||||
updateTerritory(index, "name", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="İstanbul Anadolu"
|
placeholder="İstanbul Anadolu"
|
||||||
/>
|
/>
|
||||||
|
|
@ -449,10 +398,8 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={territory.region || ""}
|
value={territory.region || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateTerritory(index, 'region', e.target.value)}
|
||||||
updateTerritory(index, "region", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="Marmara"
|
placeholder="Marmara"
|
||||||
/>
|
/>
|
||||||
|
|
@ -465,9 +412,7 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
||||||
<p>Henüz bölge eklenmedi</p>
|
<p>Henüz bölge eklenmedi</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">Bölge eklemek için yukarıdaki butona tıklayın</p>
|
||||||
Bölge eklemek için yukarıdaki butona tıklayın
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -475,7 +420,8 @@ const SalesTeamCreate: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default SalesTeamCreate;
|
export default SalesTeamCreate
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
FaUsers,
|
FaUsers,
|
||||||
FaArrowLeft,
|
FaArrowLeft,
|
||||||
|
|
@ -7,131 +7,120 @@ import {
|
||||||
FaPlus,
|
FaPlus,
|
||||||
FaTrash,
|
FaTrash,
|
||||||
FaMapMarkerAlt,
|
FaMapMarkerAlt,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { CrmTerritory } from "../../../types/crm";
|
import { CrmTerritory } from '../../../types/crm'
|
||||||
import mockSalesTeams from "../../../mocks/mockSalesTeams";
|
import mockSalesTeams from '../../../mocks/mockSalesTeams'
|
||||||
import { mockEmployees } from "../../../mocks/mockEmployees";
|
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||||
import { Team, TeamMember, TeamRoleEnum } from "../../../types/common";
|
import { Team, TeamMember, TeamRoleEnum } from '../../../types/common'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const SalesTeamEdit: React.FC = () => {
|
const SalesTeamEdit: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>()
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
teamCode: "",
|
teamCode: '',
|
||||||
name: "",
|
name: '',
|
||||||
description: "",
|
description: '',
|
||||||
managerId: "",
|
managerId: '',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
const [members, setMembers] = useState<Partial<TeamMember>[]>([]);
|
const [members, setMembers] = useState<Partial<TeamMember>[]>([])
|
||||||
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([]);
|
const [territories, setTerritories] = useState<Partial<CrmTerritory>[]>([])
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
// Load team data on component mount
|
// Load team data on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const team = mockSalesTeams.find((t) => t.id === id);
|
const team = mockSalesTeams.find((t) => t.id === id)
|
||||||
if (team) {
|
if (team) {
|
||||||
setFormData({
|
setFormData({
|
||||||
teamCode: team.code,
|
teamCode: team.code,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
description: team.description || "",
|
description: team.description || '',
|
||||||
managerId: team.managerId,
|
managerId: team.managerId,
|
||||||
isActive: team.isActive,
|
isActive: team.isActive,
|
||||||
});
|
})
|
||||||
setMembers(team.members || []);
|
setMembers(team.members || [])
|
||||||
setTerritories(team.territories || []);
|
setTerritories(team.territories || [])
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}, [id]);
|
}, [id])
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string | boolean) => {
|
const handleInputChange = (field: string, value: string | boolean) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors((prev) => ({ ...prev, [field]: "" }));
|
setErrors((prev) => ({ ...prev, [field]: '' }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const addMember = () => {
|
const addMember = () => {
|
||||||
setMembers((prev) => [
|
setMembers((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
employeeId: "",
|
employeeId: '',
|
||||||
role: TeamRoleEnum.Member,
|
role: TeamRoleEnum.Member,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
};
|
}
|
||||||
|
|
||||||
const removeMember = (index: number) => {
|
const removeMember = (index: number) => {
|
||||||
setMembers((prev) => prev.filter((_, i) => i !== index));
|
setMembers((prev) => prev.filter((_, i) => i !== index))
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateMember = (
|
const updateMember = (index: number, field: string, value: string | TeamRoleEnum | boolean) => {
|
||||||
index: number,
|
|
||||||
field: string,
|
|
||||||
value: string | TeamRoleEnum | boolean
|
|
||||||
) => {
|
|
||||||
setMembers((prev) =>
|
setMembers((prev) =>
|
||||||
prev.map((member, i) =>
|
prev.map((member, i) => (i === index ? { ...member, [field]: value } : member)),
|
||||||
i === index ? { ...member, [field]: value } : member
|
|
||||||
)
|
)
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const addTerritory = () => {
|
const addTerritory = () => {
|
||||||
setTerritories((prev) => [
|
setTerritories((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
territoryCode: "",
|
territoryCode: '',
|
||||||
name: "",
|
name: '',
|
||||||
description: "",
|
description: '',
|
||||||
region: "",
|
region: '',
|
||||||
countries: [],
|
countries: [],
|
||||||
cities: [],
|
cities: [],
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
};
|
}
|
||||||
|
|
||||||
const removeTerritory = (index: number) => {
|
const removeTerritory = (index: number) => {
|
||||||
setTerritories((prev) => prev.filter((_, i) => i !== index));
|
setTerritories((prev) => prev.filter((_, i) => i !== index))
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateTerritory = (
|
const updateTerritory = (index: number, field: string, value: string | string[] | boolean) => {
|
||||||
index: number,
|
|
||||||
field: string,
|
|
||||||
value: string | string[] | boolean
|
|
||||||
) => {
|
|
||||||
setTerritories((prev) =>
|
setTerritories((prev) =>
|
||||||
prev.map((territory, i) =>
|
prev.map((territory, i) => (i === index ? { ...territory, [field]: value } : territory)),
|
||||||
i === index ? { ...territory, [field]: value } : territory
|
|
||||||
)
|
)
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {}
|
||||||
|
|
||||||
if (!formData.teamCode.trim()) {
|
if (!formData.teamCode.trim()) {
|
||||||
newErrors.teamCode = "Takım kodu zorunludur";
|
newErrors.teamCode = 'Takım kodu zorunludur'
|
||||||
}
|
}
|
||||||
if (!formData.name.trim()) {
|
if (!formData.name.trim()) {
|
||||||
newErrors.name = "Takım adı zorunludur";
|
newErrors.name = 'Takım adı zorunludur'
|
||||||
}
|
}
|
||||||
if (!formData.managerId.trim()) {
|
if (!formData.managerId.trim()) {
|
||||||
newErrors.managerId = "Takım lideri seçimi zorunludur";
|
newErrors.managerId = 'Takım lideri seçimi zorunludur'
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors)
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return
|
||||||
|
|
||||||
const updatedTeam: Partial<Team> = {
|
const updatedTeam: Partial<Team> = {
|
||||||
id,
|
id,
|
||||||
|
|
@ -139,29 +128,27 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
members: members.map((member, index) => ({
|
members: members.map((member, index) => ({
|
||||||
...member,
|
...member,
|
||||||
id: member.id || `member-updated-${index}`,
|
id: member.id || `member-updated-${index}`,
|
||||||
teamId: id || "",
|
teamId: id || '',
|
||||||
joinDate: member.joinDate || new Date(),
|
joinDate: member.joinDate || new Date(),
|
||||||
})) as TeamMember[],
|
})) as TeamMember[],
|
||||||
territories: territories.map((territory, index) => ({
|
territories: territories.map((territory, index) => ({
|
||||||
...territory,
|
...territory,
|
||||||
id: territory.id || `territory-updated-${index}`,
|
id: territory.id || `territory-updated-${index}`,
|
||||||
assignedTeamId: id || "",
|
assignedTeamId: id || '',
|
||||||
})) as CrmTerritory[],
|
})) as CrmTerritory[],
|
||||||
lastModificationTime: new Date(),
|
lastModificationTime: new Date(),
|
||||||
};
|
}
|
||||||
|
|
||||||
console.log("Updating sales team:", updatedTeam);
|
console.log('Updating sales team:', updatedTeam)
|
||||||
alert("Satış ekibi başarıyla güncellendi!");
|
alert('Satış ekibi başarıyla güncellendi!')
|
||||||
navigate("/admin/crm/sales-teams");
|
navigate('/admin/crm/sales-teams')
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (
|
if (confirm('Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?')) {
|
||||||
confirm("Değişiklikler kaydedilmedi. Çıkmak istediğinizden emin misiniz?")
|
navigate('/admin/crm/sales-teams')
|
||||||
) {
|
}
|
||||||
navigate("/admin/crm/sales-teams");
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -171,47 +158,42 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
<p className="mt-4 text-gray-600">Yükleniyor...</p>
|
<p className="mt-4 text-gray-600">Yükleniyor...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const team = mockSalesTeams.find((t) => t.id === id);
|
const team = mockSalesTeams.find((t) => t.id === id)
|
||||||
if (!team) {
|
if (!team) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Satış ekibi bulunamadı</h3>
|
||||||
Satış ekibi bulunamadı
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500 mb-4">
|
<p className="text-gray-500 mb-4">
|
||||||
Aradığınız satış ekibi mevcut değil veya silinmiş olabilir.
|
Aradığınız satış ekibi mevcut değil veya silinmiş olabilir.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/sales-teams")}
|
onClick={() => navigate('/admin/crm/sales-teams')}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Geri Dön
|
Geri Dön
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-4 pt-2">
|
<div className="space-y-4 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/sales-teams")}
|
onClick={() => navigate('/admin/crm/sales-teams')}
|
||||||
className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-5 h-5" />
|
<FaArrowLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
<h2 className="text-xl font-bold text-gray-900">Satış Ekibi Düzenle</h2>
|
||||||
Satış Ekibi Düzenle
|
<p className="text-sm text-gray-600 mt-1">{team.name} ekibini düzenleyin</p>
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
|
||||||
{team.name} ekibini düzenleyin
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -251,11 +233,9 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.teamCode}
|
value={formData.teamCode}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('teamCode', e.target.value)}
|
||||||
handleInputChange("teamCode", e.target.value)
|
|
||||||
}
|
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
errors.teamCode ? "border-red-500" : "border-gray-300"
|
errors.teamCode ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="ST-001"
|
placeholder="ST-001"
|
||||||
/>
|
/>
|
||||||
|
|
@ -271,11 +251,9 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={formData.managerId}
|
value={formData.managerId}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('managerId', e.target.value)}
|
||||||
handleInputChange("managerId", e.target.value)
|
|
||||||
}
|
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
errors.managerId ? "border-red-500" : "border-gray-300"
|
errors.managerId ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">Lider seçin</option>
|
<option value="">Lider seçin</option>
|
||||||
|
|
@ -286,40 +264,30 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{errors.managerId && (
|
{errors.managerId && (
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<p className="text-red-500 text-sm mt-1">{errors.managerId}</p>
|
||||||
{errors.managerId}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Takım Adı *</label>
|
||||||
Takım Adı *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
className={`w-full px-3 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
errors.name ? "border-red-500" : "border-gray-300"
|
errors.name ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Kurumsal Satış Ekibi"
|
placeholder="Kurumsal Satış Ekibi"
|
||||||
/>
|
/>
|
||||||
{errors.name && (
|
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Açıklama</label>
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||||
handleInputChange("description", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Ekip açıklaması..."
|
placeholder="Ekip açıklaması..."
|
||||||
|
|
@ -331,9 +299,7 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.isActive}
|
checked={formData.isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleInputChange('isActive', e.target.checked)}
|
||||||
handleInputChange("isActive", e.target.checked)
|
|
||||||
}
|
|
||||||
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span className="ml-2 text-sm text-gray-700">Aktif</span>
|
<span className="ml-2 text-sm text-gray-700">Aktif</span>
|
||||||
|
|
@ -370,10 +336,8 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={member.employeeId || ""}
|
value={member.employeeId || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateMember(index, 'employeeId', e.target.value)}
|
||||||
updateMember(index, "employeeId", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value="">Çalışan seçin</option>
|
<option value="">Çalışan seçin</option>
|
||||||
|
|
@ -385,17 +349,11 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">Rol</label>
|
||||||
Rol
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={member.role || TeamRoleEnum.Member}
|
value={member.role || TeamRoleEnum.Member}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateMember(
|
updateMember(index, 'role', e.target.value as TeamRoleEnum)
|
||||||
index,
|
|
||||||
"role",
|
|
||||||
e.target.value as TeamRoleEnum
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
|
|
@ -418,9 +376,7 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
<FaUsers className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
||||||
<p>Henüz ekip üyesi yok</p>
|
<p>Henüz ekip üyesi yok</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">Üye eklemek için yukarıdaki butona tıklayın</p>
|
||||||
Üye eklemek için yukarıdaki butona tıklayın
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -446,10 +402,7 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{territories.map((territory, index) => (
|
{territories.map((territory, index) => (
|
||||||
<div
|
<div key={index} className="p-3 border border-gray-200 rounded-lg">
|
||||||
key={index}
|
|
||||||
className="p-3 border border-gray-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start mb-3">
|
<div className="flex justify-between items-start mb-3">
|
||||||
<h4 className="font-medium text-gray-900">
|
<h4 className="font-medium text-gray-900">
|
||||||
{territory.name || `Bölge ${index + 1}`}
|
{territory.name || `Bölge ${index + 1}`}
|
||||||
|
|
@ -469,14 +422,8 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={territory.territoryCode || ""}
|
value={territory.territoryCode || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateTerritory(index, 'territoryCode', e.target.value)}
|
||||||
updateTerritory(
|
|
||||||
index,
|
|
||||||
"territoryCode",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="TR-IST"
|
placeholder="TR-IST"
|
||||||
/>
|
/>
|
||||||
|
|
@ -488,10 +435,8 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={territory.name || ""}
|
value={territory.name || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateTerritory(index, 'name', e.target.value)}
|
||||||
updateTerritory(index, "name", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="İstanbul Anadolu"
|
placeholder="İstanbul Anadolu"
|
||||||
/>
|
/>
|
||||||
|
|
@ -503,10 +448,8 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={territory.region || ""}
|
value={territory.region || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateTerritory(index, 'region', e.target.value)}
|
||||||
updateTerritory(index, "region", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
placeholder="Marmara"
|
placeholder="Marmara"
|
||||||
/>
|
/>
|
||||||
|
|
@ -519,9 +462,7 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
<FaMapMarkerAlt className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
||||||
<p>Henüz bölge yok</p>
|
<p>Henüz bölge yok</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">Bölge eklemek için yukarıdaki butona tıklayın</p>
|
||||||
Bölge eklemek için yukarıdaki butona tıklayın
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -529,7 +470,8 @@ const SalesTeamEdit: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default SalesTeamEdit;
|
export default SalesTeamEdit
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
FaUsers,
|
FaUsers,
|
||||||
FaArrowLeft,
|
FaArrowLeft,
|
||||||
|
|
@ -11,82 +11,80 @@ import {
|
||||||
FaAward,
|
FaAward,
|
||||||
FaTrophy,
|
FaTrophy,
|
||||||
FaChartLine,
|
FaChartLine,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import mockSalesTeams from "../../../mocks/mockSalesTeams";
|
import mockSalesTeams from '../../../mocks/mockSalesTeams'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { Team, TeamRoleEnum } from "../../../types/common";
|
import { Team, TeamRoleEnum } from '../../../types/common'
|
||||||
import { getTeamRoleText } from "../../../utils/erp";
|
import { getTeamRoleText } from '../../../utils/erp'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const SalesTeamView: React.FC = () => {
|
const SalesTeamView: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>()
|
||||||
|
|
||||||
const team = mockSalesTeams.find((t) => t.id === id);
|
const team = mockSalesTeams.find((t) => t.id === id)
|
||||||
|
|
||||||
const calculateTeamPerformance = (team: Team): number => {
|
const calculateTeamPerformance = (team: Team): number => {
|
||||||
if (!team.targets || team.targets.length === 0) return 0;
|
if (!team.targets || team.targets.length === 0) return 0
|
||||||
const activeTarget = team.targets.find((t) => t.status === "ACTIVE");
|
const activeTarget = team.targets.find((t) => t.status === 'ACTIVE')
|
||||||
if (!activeTarget || activeTarget.targetValue === 0) return 0;
|
if (!activeTarget || activeTarget.targetValue === 0) return 0
|
||||||
return (activeTarget.actualValue / activeTarget.targetValue) * 100;
|
return (activeTarget.actualValue / activeTarget.targetValue) * 100
|
||||||
};
|
}
|
||||||
|
|
||||||
const calculateTeamRevenue = (team: Team): number => {
|
const calculateTeamRevenue = (team: Team): number => {
|
||||||
if (!team.targets || team.targets.length === 0) return 0;
|
if (!team.targets || team.targets.length === 0) return 0
|
||||||
return team.targets.reduce((sum, target) => sum + target.actualValue, 0);
|
return team.targets.reduce((sum, target) => sum + target.actualValue, 0)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
navigate(`/admin/crm/sales-teams/edit/${id}`);
|
navigate(`/admin/crm/sales-teams/edit/${id}`)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (confirm(`${team?.name} ekibini silmek istediğinizden emin misiniz?`)) {
|
if (confirm(`${team?.name} ekibini silmek istediğinizden emin misiniz?`)) {
|
||||||
console.log("Delete sales team:", id);
|
console.log('Delete sales team:', id)
|
||||||
alert("Ekip silindi (mock)");
|
alert('Ekip silindi (mock)')
|
||||||
navigate("/admin/crm/sales-teams");
|
navigate('/admin/crm/sales-teams')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (!team) {
|
if (!team) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Satış ekibi bulunamadı</h3>
|
||||||
Satış ekibi bulunamadı
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500 mb-4">
|
<p className="text-gray-500 mb-4">
|
||||||
Aradığınız satış ekibi mevcut değil veya silinmiş olabilir.
|
Aradığınız satış ekibi mevcut değil veya silinmiş olabilir.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/sales-teams")}
|
onClick={() => navigate('/admin/crm/sales-teams')}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Geri Dön
|
Geri Dön
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const performance = calculateTeamPerformance(team);
|
const performance = calculateTeamPerformance(team)
|
||||||
const revenue = calculateTeamRevenue(team);
|
const revenue = calculateTeamRevenue(team)
|
||||||
const activeTarget = team.targets?.find((t) => t.status === "ACTIVE");
|
const activeTarget = team.targets?.find((t) => t.status === 'ACTIVE')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-6 pt-2">
|
<div className="space-y-6 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/admin/crm/sales-teams")}
|
onClick={() => navigate('/admin/crm/sales-teams')}
|
||||||
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-5 h-5" />
|
<FaArrowLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">{team.name}</h2>
|
<h2 className="text-xl font-bold text-gray-900">{team.name}</h2>
|
||||||
<p className="text-gray-600 mt-1">
|
<p className="text-gray-600 mt-1">{team.code} • Satış ekibi detayları</p>
|
||||||
{team.code} • Satış ekibi detayları
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -115,9 +113,7 @@ const SalesTeamView: React.FC = () => {
|
||||||
<FaUsers className="w-6 h-6 text-blue-500" />
|
<FaUsers className="w-6 h-6 text-blue-500" />
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<p className="text-sm font-medium text-gray-600">Üye Sayısı</p>
|
<p className="text-sm font-medium text-gray-600">Üye Sayısı</p>
|
||||||
<p className="text-xl font-bold text-gray-900">
|
<p className="text-xl font-bold text-gray-900">{team.members?.length || 0}</p>
|
||||||
{team.members?.length || 0}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,9 +123,7 @@ const SalesTeamView: React.FC = () => {
|
||||||
<FaMapMarkerAlt className="w-6 h-6 text-green-500" />
|
<FaMapMarkerAlt className="w-6 h-6 text-green-500" />
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<p className="text-sm font-medium text-gray-600">Bölge Sayısı</p>
|
<p className="text-sm font-medium text-gray-600">Bölge Sayısı</p>
|
||||||
<p className="text-xl font-bold text-gray-900">
|
<p className="text-xl font-bold text-gray-900">{team.territories?.length || 0}</p>
|
||||||
{team.territories?.length || 0}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -139,10 +133,10 @@ const SalesTeamView: React.FC = () => {
|
||||||
<FaArrowUp
|
<FaArrowUp
|
||||||
className={`w-6 h-6 ${
|
className={`w-6 h-6 ${
|
||||||
performance >= 100
|
performance >= 100
|
||||||
? "text-green-600"
|
? 'text-green-600'
|
||||||
: performance >= 80
|
: performance >= 80
|
||||||
? "text-yellow-600"
|
? 'text-yellow-600'
|
||||||
: "text-red-600"
|
: 'text-red-600'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
|
|
@ -150,10 +144,10 @@ const SalesTeamView: React.FC = () => {
|
||||||
<p
|
<p
|
||||||
className={`text-xl font-bold ${
|
className={`text-xl font-bold ${
|
||||||
performance >= 100
|
performance >= 100
|
||||||
? "text-green-600"
|
? 'text-green-600'
|
||||||
: performance >= 80
|
: performance >= 80
|
||||||
? "text-yellow-600"
|
? 'text-yellow-600'
|
||||||
: "text-red-600"
|
: 'text-red-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
%{performance.toFixed(1)}
|
%{performance.toFixed(1)}
|
||||||
|
|
@ -167,9 +161,7 @@ const SalesTeamView: React.FC = () => {
|
||||||
<FaDollarSign className="w-6 h-6 text-purple-500" />
|
<FaDollarSign className="w-6 h-6 text-purple-500" />
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<p className="text-sm font-medium text-gray-600">Toplam Gelir</p>
|
<p className="text-sm font-medium text-gray-600">Toplam Gelir</p>
|
||||||
<p className="text-lg font-bold text-gray-900">
|
<p className="text-lg font-bold text-gray-900">₺{revenue.toLocaleString()}</p>
|
||||||
₺{revenue.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -187,42 +179,30 @@ const SalesTeamView: React.FC = () => {
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
<label className="block text-xs font-medium text-gray-600 mb-1">Ekip Kodu</label>
|
||||||
Ekip Kodu
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-900">{team.code}</p>
|
<p className="text-gray-900">{team.code}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
<label className="block text-xs font-medium text-gray-600 mb-1">Durum</label>
|
||||||
Durum
|
|
||||||
</label>
|
|
||||||
<span
|
<span
|
||||||
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
||||||
team.isActive
|
team.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: "bg-red-100 text-red-800"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{team.isActive ? "Aktif" : "Pasif"}
|
{team.isActive ? 'Aktif' : 'Pasif'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
<label className="block text-xs font-medium text-gray-600 mb-1">Açıklama</label>
|
||||||
Açıklama
|
<p className="text-gray-900">{team.description || 'Açıklama girilmemiş'}</p>
|
||||||
</label>
|
|
||||||
<p className="text-gray-900">
|
|
||||||
{team.description || "Açıklama girilmemiş"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
<label className="block text-xs font-medium text-gray-600 mb-1">Yönetici</label>
|
||||||
Yönetici
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">
|
||||||
{team.manager?.fullName || "Yönetici bilgisi girilmemiş"}
|
{team.manager?.fullName || 'Yönetici bilgisi girilmemiş'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -230,9 +210,7 @@ const SalesTeamView: React.FC = () => {
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||||
Oluşturulma Tarihi
|
Oluşturulma Tarihi
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">{dayjs(team.creationTime).format('DD.MM.YYYY')}</p>
|
||||||
{dayjs(team.creationTime).format("DD.MM.YYYY")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -240,7 +218,7 @@ const SalesTeamView: React.FC = () => {
|
||||||
Son Güncelleme
|
Son Güncelleme
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">
|
||||||
{dayjs(team.lastModificationTime).format("DD.MM.YYYY")}
|
{dayjs(team.lastModificationTime).format('DD.MM.YYYY')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -265,11 +243,9 @@ const SalesTeamView: React.FC = () => {
|
||||||
<FaUser className="w-5 h-5 text-blue-600" />
|
<FaUser className="w-5 h-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">{member.employee?.fullName}</p>
|
||||||
{member.employee?.fullName}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Katılım: {dayjs(member.joinDate).format("DD.MM.YYYY")}
|
Katılım: {dayjs(member.joinDate).format('DD.MM.YYYY')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -277,16 +253,16 @@ const SalesTeamView: React.FC = () => {
|
||||||
<span
|
<span
|
||||||
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
||||||
member.role === TeamRoleEnum.Manager
|
member.role === TeamRoleEnum.Manager
|
||||||
? "bg-purple-100 text-purple-800"
|
? 'bg-purple-100 text-purple-800'
|
||||||
: member.role === TeamRoleEnum.Lead
|
: member.role === TeamRoleEnum.Lead
|
||||||
? "bg-blue-100 text-blue-800"
|
? 'bg-blue-100 text-blue-800'
|
||||||
: "bg-gray-100 text-gray-800"
|
: 'bg-gray-100 text-gray-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{getTeamRoleText(member.role)}
|
{getTeamRoleText(member.role)}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
{member.isActive ? "Aktif" : "Pasif"}
|
{member.isActive ? 'Aktif' : 'Pasif'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -315,21 +291,15 @@ const SalesTeamView: React.FC = () => {
|
||||||
className="p-3 border border-gray-200 rounded-lg"
|
className="p-3 border border-gray-200 rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<h4 className="font-medium text-gray-900">
|
<h4 className="font-medium text-gray-900">{territory.name}</h4>
|
||||||
{territory.name}
|
<span className="text-sm text-gray-500">{territory.territoryCode}</span>
|
||||||
</h4>
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
{territory.territoryCode}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 mb-2">
|
<p className="text-sm text-gray-600 mb-2">{territory.description}</p>
|
||||||
{territory.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-4 text-sm text-gray-500">
|
<div className="flex gap-4 text-sm text-gray-500">
|
||||||
<span>Bölge: {territory.region}</span>
|
<span>Bölge: {territory.region}</span>
|
||||||
<span>Ülke: {territory.countries?.join(", ")}</span>
|
<span>Ülke: {territory.countries?.join(', ')}</span>
|
||||||
{territory.cities && territory.cities.length > 0 && (
|
{territory.cities && territory.cities.length > 0 && (
|
||||||
<span>Şehir: {territory.cities.join(", ")}</span>
|
<span>Şehir: {territory.cities.join(', ')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -358,10 +328,10 @@ const SalesTeamView: React.FC = () => {
|
||||||
<div
|
<div
|
||||||
className={`text-3xl font-bold mb-2 ${
|
className={`text-3xl font-bold mb-2 ${
|
||||||
performance >= 100
|
performance >= 100
|
||||||
? "text-green-600"
|
? 'text-green-600'
|
||||||
: performance >= 80
|
: performance >= 80
|
||||||
? "text-yellow-600"
|
? 'text-yellow-600'
|
||||||
: "text-red-600"
|
: 'text-red-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
%{performance.toFixed(1)}
|
%{performance.toFixed(1)}
|
||||||
|
|
@ -373,10 +343,10 @@ const SalesTeamView: React.FC = () => {
|
||||||
<div
|
<div
|
||||||
className={`h-2.5 rounded-full ${
|
className={`h-2.5 rounded-full ${
|
||||||
performance >= 100
|
performance >= 100
|
||||||
? "bg-green-500"
|
? 'bg-green-500'
|
||||||
: performance >= 80
|
: performance >= 80
|
||||||
? "bg-yellow-500"
|
? 'bg-yellow-500'
|
||||||
: "bg-red-500"
|
: 'bg-red-500'
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${Math.min(performance, 100)}%` }}
|
style={{ width: `${Math.min(performance, 100)}%` }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -428,8 +398,8 @@ const SalesTeamView: React.FC = () => {
|
||||||
Başlangıç - Bitiş
|
Başlangıç - Bitiş
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900">
|
<p className="text-sm text-gray-900">
|
||||||
{dayjs(activeTarget.startDate).format("DD.MM.YYYY")} -{" "}
|
{dayjs(activeTarget.startDate).format('DD.MM.YYYY')} -{' '}
|
||||||
{dayjs(activeTarget.endDate).format("DD.MM.YYYY")}
|
{dayjs(activeTarget.endDate).format('DD.MM.YYYY')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -441,17 +411,13 @@ const SalesTeamView: React.FC = () => {
|
||||||
<div className="bg-gradient-to-r from-yellow-400 to-orange-500 rounded-lg p-4 text-center text-white">
|
<div className="bg-gradient-to-r from-yellow-400 to-orange-500 rounded-lg p-4 text-center text-white">
|
||||||
<FaAward className="w-10 h-10 mx-auto mb-2" />
|
<FaAward className="w-10 h-10 mx-auto mb-2" />
|
||||||
<h3 className="text-base font-bold mb-1">Tebrikler!</h3>
|
<h3 className="text-base font-bold mb-1">Tebrikler!</h3>
|
||||||
<p className="text-sm">
|
<p className="text-sm">Bu ekip hedefini aştı ve başarı ödülünü hak etti!</p>
|
||||||
Bu ekip hedefini aştı ve başarı ödülünü hak etti!
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Quick Stats */}
|
{/* Quick Stats */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3">
|
<h3 className="text-base font-semibold text-gray-900 mb-3">Hızlı İstatistikler</h3>
|
||||||
Hızlı İstatistikler
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
|
@ -467,10 +433,7 @@ const SalesTeamView: React.FC = () => {
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Kapsam Alanı:</span>
|
<span className="text-gray-600">Kapsam Alanı:</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{team.territories?.reduce(
|
{team.territories?.reduce((acc, t) => acc + (t.cities?.length || 0), 0) || 0}{' '}
|
||||||
(acc, t) => acc + (t.cities?.length || 0),
|
|
||||||
0
|
|
||||||
) || 0}{" "}
|
|
||||||
şehir
|
şehir
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -479,7 +442,8 @@ const SalesTeamView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default SalesTeamView;
|
export default SalesTeamView
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
FaUsers,
|
FaUsers,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
|
|
@ -12,96 +12,93 @@ import {
|
||||||
FaList,
|
FaList,
|
||||||
FaSearch,
|
FaSearch,
|
||||||
FaFilter,
|
FaFilter,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom'
|
||||||
import DataTable, { Column } from "../../../components/common/DataTable";
|
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||||
import mockSalesTeams from "../../../mocks/mockSalesTeams";
|
import mockSalesTeams from '../../../mocks/mockSalesTeams'
|
||||||
import Widget from "../../../components/common/Widget";
|
import Widget from '../../../components/common/Widget'
|
||||||
import { Team } from "../../../types/common";
|
import { Team } from '../../../types/common'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const SalesTeams: React.FC = () => {
|
const SalesTeams: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const [teams] = useState<Team[]>(mockSalesTeams);
|
const [teams] = useState<Team[]>(mockSalesTeams)
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [viewMode, setViewMode] = useState<"cards" | "list">("cards");
|
const [viewMode, setViewMode] = useState<'cards' | 'list'>('cards')
|
||||||
const [filterStatus, setFilterStatus] = useState<
|
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all')
|
||||||
"all" | "active" | "inactive"
|
|
||||||
>("all");
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
navigate("/admin/crm/sales-teams/new");
|
navigate('/admin/crm/sales-teams/new')
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEdit = (team: Team) => {
|
const handleEdit = (team: Team) => {
|
||||||
navigate(`/admin/crm/sales-teams/edit/${team.id}`);
|
navigate(`/admin/crm/sales-teams/edit/${team.id}`)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
const team = teams.find((t) => t.id === id);
|
const team = teams.find((t) => t.id === id)
|
||||||
if (confirm(`${team?.name} ekibini silmek istediğinizden emin misiniz?`)) {
|
if (confirm(`${team?.name} ekibini silmek istediğinizden emin misiniz?`)) {
|
||||||
console.log("Delete sales team:", id);
|
console.log('Delete sales team:', id)
|
||||||
// Implement delete logic here
|
// Implement delete logic here
|
||||||
alert("Ekip silindi (mock)");
|
alert('Ekip silindi (mock)')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewDetails = (team: Team) => {
|
const handleViewDetails = (team: Team) => {
|
||||||
navigate(`/admin/crm/sales-teams/${team.id}`);
|
navigate(`/admin/crm/sales-teams/${team.id}`)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Calculate team performance based on targets
|
// Calculate team performance based on targets
|
||||||
const calculateTeamPerformance = (team: Team): number => {
|
const calculateTeamPerformance = (team: Team): number => {
|
||||||
if (!team.targets || team.targets.length === 0) return 0;
|
if (!team.targets || team.targets.length === 0) return 0
|
||||||
const activeTarget = team.targets.find((t) => t.status === "ACTIVE");
|
const activeTarget = team.targets.find((t) => t.status === 'ACTIVE')
|
||||||
if (!activeTarget || activeTarget.targetValue === 0) return 0;
|
if (!activeTarget || activeTarget.targetValue === 0) return 0
|
||||||
return (activeTarget.actualValue / activeTarget.targetValue) * 100;
|
return (activeTarget.actualValue / activeTarget.targetValue) * 100
|
||||||
};
|
}
|
||||||
|
|
||||||
// Calculate team revenue
|
// Calculate team revenue
|
||||||
const calculateTeamRevenue = (team: Team): number => {
|
const calculateTeamRevenue = (team: Team): number => {
|
||||||
if (!team.targets || team.targets.length === 0) return 0;
|
if (!team.targets || team.targets.length === 0) return 0
|
||||||
return team.targets.reduce((sum, target) => sum + target.actualValue, 0);
|
return team.targets.reduce((sum, target) => sum + target.actualValue, 0)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Filter teams based on search and status
|
// Filter teams based on search and status
|
||||||
const filteredTeams = teams.filter((team) => {
|
const filteredTeams = teams.filter((team) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
searchTerm === "" ||
|
searchTerm === '' ||
|
||||||
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
team.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
team.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
team.description?.toLowerCase().includes(searchTerm.toLowerCase());
|
team.description?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
|
||||||
const matchesStatus =
|
const matchesStatus =
|
||||||
filterStatus === "all" ||
|
filterStatus === 'all' ||
|
||||||
(filterStatus === "active" && team.isActive) ||
|
(filterStatus === 'active' && team.isActive) ||
|
||||||
(filterStatus === "inactive" && !team.isActive);
|
(filterStatus === 'inactive' && !team.isActive)
|
||||||
|
|
||||||
return matchesSearch && matchesStatus;
|
return matchesSearch && matchesStatus
|
||||||
});
|
})
|
||||||
|
|
||||||
// Table columns for list view
|
// Table columns for list view
|
||||||
const columns: Column<Team>[] = [
|
const columns: Column<Team>[] = [
|
||||||
{
|
{
|
||||||
key: "teamCode",
|
key: 'teamCode',
|
||||||
header: "Takım Kodu",
|
header: 'Takım Kodu',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "name",
|
key: 'name',
|
||||||
header: "Takım Adı",
|
header: 'Takım Adı',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (team: Team) => (
|
render: (team: Team) => (
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900">{team.name}</div>
|
<div className="font-medium text-gray-900">{team.name}</div>
|
||||||
<div className="text-sm text-gray-500 truncate max-w-xs">
|
<div className="text-sm text-gray-500 truncate max-w-xs">{team.description}</div>
|
||||||
{team.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "memberCount",
|
key: 'memberCount',
|
||||||
header: "Üye Sayısı",
|
header: 'Üye Sayısı',
|
||||||
render: (team: Team) => (
|
render: (team: Team) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FaUsers className="w-4 h-4 text-gray-500" />
|
<FaUsers className="w-4 h-4 text-gray-500" />
|
||||||
|
|
@ -110,8 +107,8 @@ const SalesTeams: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "territories",
|
key: 'territories',
|
||||||
header: "Bölge Sayısı",
|
header: 'Bölge Sayısı',
|
||||||
render: (team: Team) => (
|
render: (team: Team) => (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FaMapMarkerAlt className="w-4 h-4 text-gray-500" />
|
<FaMapMarkerAlt className="w-4 h-4 text-gray-500" />
|
||||||
|
|
@ -120,55 +117,53 @@ const SalesTeams: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "performance",
|
key: 'performance',
|
||||||
header: "Performans",
|
header: 'Performans',
|
||||||
render: (team: Team) => {
|
render: (team: Team) => {
|
||||||
const performance = calculateTeamPerformance(team);
|
const performance = calculateTeamPerformance(team)
|
||||||
const color =
|
const color =
|
||||||
performance >= 100
|
performance >= 100
|
||||||
? "text-green-600"
|
? 'text-green-600'
|
||||||
: performance >= 80
|
: performance >= 80
|
||||||
? "text-yellow-600"
|
? 'text-yellow-600'
|
||||||
: "text-red-600";
|
: 'text-red-600'
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center gap-1 ${color}`}>
|
<div className={`flex items-center gap-1 ${color}`}>
|
||||||
<FaArrowUp className="w-4 h-4" />
|
<FaArrowUp className="w-4 h-4" />
|
||||||
<span className="font-medium">%{performance.toFixed(1)}</span>
|
<span className="font-medium">%{performance.toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "totalRevenue",
|
key: 'totalRevenue',
|
||||||
header: "Toplam Gelir",
|
header: 'Toplam Gelir',
|
||||||
render: (team: Team) => {
|
render: (team: Team) => {
|
||||||
const revenue = calculateTeamRevenue(team);
|
const revenue = calculateTeamRevenue(team)
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FaDollarSign className="w-4 h-4 text-gray-500" />
|
<FaDollarSign className="w-4 h-4 text-gray-500" />
|
||||||
<span>₺{revenue.toLocaleString()}</span>
|
<span>₺{revenue.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: 'status',
|
||||||
header: "Durum",
|
header: 'Durum',
|
||||||
render: (team: Team) => (
|
render: (team: Team) => (
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||||
team.isActive
|
team.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: "bg-red-100 text-red-800"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{team.isActive ? "Aktif" : "Pasif"}
|
{team.isActive ? 'Aktif' : 'Pasif'}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: 'actions',
|
||||||
header: "İşlemler",
|
header: 'İşlemler',
|
||||||
render: (team: Team) => (
|
render: (team: Team) => (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -195,33 +190,23 @@ const SalesTeams: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
// Calculate overall statistics
|
// Calculate overall statistics
|
||||||
const totalMembers = teams.reduce(
|
const totalMembers = teams.reduce((sum, team) => sum + (team.members?.length || 0), 0)
|
||||||
(sum, team) => sum + (team.members?.length || 0),
|
const totalRevenue = teams.reduce((sum, team) => sum + calculateTeamRevenue(team), 0)
|
||||||
0
|
|
||||||
);
|
|
||||||
const totalRevenue = teams.reduce(
|
|
||||||
(sum, team) => sum + calculateTeamRevenue(team),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const averagePerformance =
|
const averagePerformance =
|
||||||
teams.reduce((sum, team) => sum + calculateTeamPerformance(team), 0) /
|
teams.reduce((sum, team) => sum + calculateTeamPerformance(team), 0) / teams.length || 0
|
||||||
teams.length || 0;
|
const topPerformingTeams = teams.filter((team) => calculateTeamPerformance(team) >= 100).length
|
||||||
const topPerformingTeams = teams.filter(
|
|
||||||
(team) => calculateTeamPerformance(team) >= 100
|
|
||||||
).length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className="space-y-4 pt-2">
|
<div className="space-y-4 pt-2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">Satış Ekipleri</h2>
|
<h2 className="text-xl font-bold text-gray-900">Satış Ekipleri</h2>
|
||||||
<p className="text-gray-600 mt-1">
|
<p className="text-gray-600 mt-1">Satış ekipleri ve performans yönetimi</p>
|
||||||
Satış ekipleri ve performans yönetimi
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
|
|
@ -234,19 +219,9 @@ const SalesTeams: React.FC = () => {
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
<Widget
|
<Widget title="Toplam Ekip" value={teams.length} color="blue" icon="FaUsers" />
|
||||||
title="Toplam Ekip"
|
|
||||||
value={teams.length}
|
|
||||||
color="blue"
|
|
||||||
icon="FaUsers"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget title="Toplam Üye" value={totalMembers} color="green" icon="FaUser" />
|
||||||
title="Toplam Üye"
|
|
||||||
value={totalMembers}
|
|
||||||
color="green"
|
|
||||||
icon="FaUser"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
<Widget
|
||||||
title="Toplam Gelir"
|
title="Toplam Gelir"
|
||||||
|
|
@ -265,9 +240,7 @@ const SalesTeams: React.FC = () => {
|
||||||
|
|
||||||
{/* Performance Overview */}
|
{/* Performance Overview */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3">
|
<h3 className="text-base font-semibold text-gray-900 mb-3">Performans Özeti</h3>
|
||||||
Performans Özeti
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-blue-600 mb-1">
|
<div className="text-2xl font-bold text-blue-600 mb-1">
|
||||||
|
|
@ -277,9 +250,7 @@ const SalesTeams: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-green-600 mb-1">
|
<div className="text-2xl font-bold text-green-600 mb-1">{topPerformingTeams}</div>
|
||||||
{topPerformingTeams}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-600">Hedef Aşan Ekip</p>
|
<p className="text-sm text-gray-600">Hedef Aşan Ekip</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -312,9 +283,7 @@ const SalesTeams: React.FC = () => {
|
||||||
<FaFilter className="w-4 h-4 text-gray-400" />
|
<FaFilter className="w-4 h-4 text-gray-400" />
|
||||||
<select
|
<select
|
||||||
value={filterStatus}
|
value={filterStatus}
|
||||||
onChange={(e) =>
|
onChange={(e) => setFilterStatus(e.target.value as 'all' | 'active' | 'inactive')}
|
||||||
setFilterStatus(e.target.value as "all" | "active" | "inactive")
|
|
||||||
}
|
|
||||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">Tüm Durumlar</option>
|
<option value="all">Tüm Durumlar</option>
|
||||||
|
|
@ -326,21 +295,21 @@ const SalesTeams: React.FC = () => {
|
||||||
{/* View Mode Toggle */}
|
{/* View Mode Toggle */}
|
||||||
<div className="flex bg-gray-100 rounded-lg p-1">
|
<div className="flex bg-gray-100 rounded-lg p-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("list")}
|
onClick={() => setViewMode('list')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
|
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
|
||||||
viewMode === "list"
|
viewMode === 'list'
|
||||||
? "bg-white text-blue-600 shadow-sm"
|
? 'bg-white text-blue-600 shadow-sm'
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaList className="w-4 h-4" />
|
<FaList className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("cards")}
|
onClick={() => setViewMode('cards')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
|
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${
|
||||||
viewMode === "cards"
|
viewMode === 'cards'
|
||||||
? "bg-white text-blue-600 shadow-sm"
|
? 'bg-white text-blue-600 shadow-sm'
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaTh className="w-4 h-4" />
|
<FaTh className="w-4 h-4" />
|
||||||
|
|
@ -350,23 +319,18 @@ const SalesTeams: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{viewMode === "cards" ? (
|
{viewMode === 'cards' ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
{filteredTeams.map((team) => {
|
{filteredTeams.map((team) => {
|
||||||
const performance = calculateTeamPerformance(team);
|
const performance = calculateTeamPerformance(team)
|
||||||
const revenue = calculateTeamRevenue(team);
|
const revenue = calculateTeamRevenue(team)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={team.id} className="bg-white rounded-lg shadow-sm border p-4">
|
||||||
key={team.id}
|
|
||||||
className="bg-white rounded-lg shadow-sm border p-4"
|
|
||||||
>
|
|
||||||
{/* Card Header */}
|
{/* Card Header */}
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-base font-semibold text-gray-900">
|
<h3 className="text-base font-semibold text-gray-900">{team.name}</h3>
|
||||||
{team.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-500">{team.code}</p>
|
<p className="text-sm text-gray-500">{team.code}</p>
|
||||||
{team.description && (
|
{team.description && (
|
||||||
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
||||||
|
|
@ -376,12 +340,10 @@ const SalesTeams: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||||
team.isActive
|
team.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: "bg-red-100 text-red-800"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{team.isActive ? "Aktif" : "Pasif"}
|
{team.isActive ? 'Aktif' : 'Pasif'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -392,9 +354,7 @@ const SalesTeams: React.FC = () => {
|
||||||
<FaUsers className="w-4 h-4 text-gray-400" />
|
<FaUsers className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-sm text-gray-600">Yönetici</span>
|
<span className="text-sm text-gray-600">Yönetici</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">{team.manager?.fullName}</span>
|
||||||
{team.manager?.fullName}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -402,9 +362,7 @@ const SalesTeams: React.FC = () => {
|
||||||
<FaUsers className="w-4 h-4 text-gray-400" />
|
<FaUsers className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-sm text-gray-600">Üye Sayısı</span>
|
<span className="text-sm text-gray-600">Üye Sayısı</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">{team.members?.length || 0}</span>
|
||||||
{team.members?.length || 0}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -412,9 +370,7 @@ const SalesTeams: React.FC = () => {
|
||||||
<FaMapMarkerAlt className="w-4 h-4 text-gray-400" />
|
<FaMapMarkerAlt className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-sm text-gray-600">Bölge</span>
|
<span className="text-sm text-gray-600">Bölge</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">{team.territories?.length || 0}</span>
|
||||||
{team.territories?.length || 0}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -425,10 +381,10 @@ const SalesTeams: React.FC = () => {
|
||||||
<span
|
<span
|
||||||
className={`font-medium ${
|
className={`font-medium ${
|
||||||
performance >= 100
|
performance >= 100
|
||||||
? "text-green-600"
|
? 'text-green-600'
|
||||||
: performance >= 80
|
: performance >= 80
|
||||||
? "text-yellow-600"
|
? 'text-yellow-600'
|
||||||
: "text-red-600"
|
: 'text-red-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
%{performance.toFixed(1)}
|
%{performance.toFixed(1)}
|
||||||
|
|
@ -440,9 +396,7 @@ const SalesTeams: React.FC = () => {
|
||||||
<FaDollarSign className="w-4 h-4 text-gray-400" />
|
<FaDollarSign className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-sm text-gray-600">Gelir</span>
|
<span className="text-sm text-gray-600">Gelir</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">₺{revenue.toLocaleString()}</span>
|
||||||
₺{revenue.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -469,7 +423,7 @@ const SalesTeams: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -481,16 +435,13 @@ const SalesTeams: React.FC = () => {
|
||||||
{filteredTeams.length === 0 && (
|
{filteredTeams.length === 0 && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaUsers className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
<FaUsers className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
<h3 className="text-base font-medium text-gray-900 mb-2">Ekip bulunamadı</h3>
|
||||||
Ekip bulunamadı
|
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500">
|
|
||||||
Arama kriterlerinizi değiştirmeyi deneyin.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</Container>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default SalesTeams;
|
export default SalesTeams
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue