erp-platform/ui/src/views/crm/components/CustomerList.tsx
2025-09-16 15:33:57 +03:00

414 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import {
FaUserCheck,
FaPlus,
FaSearch,
FaFilter,
FaDownload,
FaEdit,
FaEye,
FaPhone,
FaEnvelope,
FaMapMarkerAlt,
FaBuilding,
FaExclamationTriangle,
FaStar,
FaCalendar,
} from 'react-icons/fa'
import classNames from 'classnames'
import { CustomerSegmentEnum } from '../../../types/crm'
import dayjs from 'dayjs'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { BusinessPartyStatusEnum, PartyType } from '../../../types/common'
import Widget from '../../../components/common/Widget'
import {
getBusinessPartyStatusColor,
getBusinessPartyStatusName,
getCustomerSegmentColor,
getCustomerSegmentName,
} from '../../../utils/erp'
import { ROUTES_ENUM } from '@/routes/route.constant'
const CustomerList: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('')
const [filterStatus, setFilterStatus] = useState('all')
const [filterSegment, setFilterSegment] = useState('all')
const [showFilters, setShowFilters] = useState(false)
const {
data: customers,
isLoading,
error,
} = useQuery({
queryKey: ['customers', searchTerm, filterStatus, filterSegment],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 500))
const mockCustomers = mockBusinessParties.filter(
(customer) => customer.partyType === PartyType.Customer,
)
return mockCustomers.filter((customer) => {
const matchesSearch =
customer.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.primaryContact?.fullName.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = filterStatus === 'all' || customer.status === filterStatus
const matchesSegment = filterSegment === 'all' || customer.customerSegment === filterSegment
return matchesSearch && matchesStatus && matchesSegment
})
},
})
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Müşteriler yükleniyor...</span>
</div>
)
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" />
<span className="text-red-800">Müşteriler yüklenirken hata oluştu.</span>
</div>
</div>
)
}
return (
<div className="space-y-2">
{/* Header Actions */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex items-center space-x-2">
<div className="relative">
<FaSearch
size={16}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"
/>
<input
type="text"
placeholder="Müşteri kodu, firma adı veya kişi..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-1.5 text-sm w-64 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<button
onClick={() => setShowFilters(!showFilters)}
className={classNames(
'flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors',
showFilters
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
)}
>
<FaFilter size={14} className="mr-2" />
Filtreler
</button>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
<FaDownload size={14} className="mr-2" />
Dışa Aktar
</button>
<Link
to={ROUTES_ENUM.protected.crm.customersNew}
className="flex items-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus size={14} className="mr-2" />
Yeni Müşteri
</Link>
</div>
</div>
{/* Filters Panel */}
{showFilters && (
<div className="bg-white border border-gray-200 rounded-lg p-3">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tümü</option>
<option value={BusinessPartyStatusEnum.Prospect}>Potansiyel</option>
<option value={BusinessPartyStatusEnum.Active}>Aktif</option>
<option value={BusinessPartyStatusEnum.Inactive}>Pasif</option>
<option value={BusinessPartyStatusEnum.Blocked}>Blokeli</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Segment</label>
<select
value={filterSegment}
onChange={(e) => setFilterSegment(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tümü</option>
<option value={CustomerSegmentEnum.Enterprise}>Kurumsal</option>
<option value={CustomerSegmentEnum.SMB}>KOBİ</option>
<option value={CustomerSegmentEnum.Startup}>Girişim</option>
<option value={CustomerSegmentEnum.Government}>Kamu</option>
</select>
</div>
<div className="flex items-end">
<button
onClick={() => {
setFilterStatus('all')
setFilterSegment('all')
setSearchTerm('')
}}
className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Filtreleri Temizle
</button>
</div>
</div>
</div>
)}
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Widget
title="Toplam Müşteri"
value={customers?.length || 0}
color="blue"
icon="FaBuilding"
/>
<Widget
title="Aktif Müşteri"
value={customers?.filter((c) => c.status === BusinessPartyStatusEnum.Active).length || 0}
color="green"
icon="FaUserCheck"
/>
<Widget
title="Toplam Ciro"
value={`${
customers?.reduce((acc, c) => acc + (c.totalRevenue ?? 0), 0).toLocaleString() || 0
}`}
color="purple"
icon="FaDollarSign"
/>
<Widget
title="Ortalama Sipariş"
value={`${
customers?.length
? Math.round(
customers.reduce((acc, c) => acc + (c.averageOrderValue ?? 0), 0) /
customers.length,
).toLocaleString()
: 0
}`}
color="yellow"
icon="FaArrowUp"
/>
</div>
{/* Customers Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="px-4 py-3 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">Müşteri Listesi</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Müşteri Bilgileri
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
İletişim
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Segment / Durum
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Satış Performansı
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kredi Limiti
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Son Aktivite
</th>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
İşlemler
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{customers?.map((customer) => (
<tr key={customer.id} className="hover:bg-gray-50 transition-colors">
<td className="px-4 py-3">
<div className="flex items-center">
<div className="flex-shrink-0 h-8 w-8">
<div className="h-8 w-8 rounded-lg bg-blue-100 flex items-center justify-center">
<FaBuilding className="h-5 w-5 text-blue-600" />
</div>
</div>
<div className="ml-4">
<div className="text-xs font-medium text-gray-900">{customer.code}</div>
<div className="text-sm text-gray-500">{customer.name}</div>
{customer.industry && (
<div className="text-xs text-gray-400 mt-1">{customer.industry}</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
<div className="space-y-1">
<div className="text-sm font-medium text-gray-900">
{customer.primaryContact?.fullName}
</div>
<div className="flex items-center text-sm text-gray-500">
<FaEnvelope size={14} className="mr-1" />
{customer.primaryContact?.email}
</div>
{customer.primaryContact?.phone && (
<div className="flex items-center text-sm text-gray-500">
<FaPhone size={14} className="mr-1" />
{customer.primaryContact?.phone}
</div>
)}
<div className="flex items-center text-sm text-gray-400">
<FaMapMarkerAlt size={14} className="mr-1" />
{customer.address?.city}, {customer.address?.country}
</div>
</div>
</td>
<td className="px-4 py-3">
<div className="space-y-2">
<div
className={classNames(
'text-sm font-medium',
getCustomerSegmentColor(
customer.customerSegment || CustomerSegmentEnum.SMB,
),
)}
>
{getCustomerSegmentName(
customer.customerSegment || CustomerSegmentEnum.SMB,
)}
</div>
<span
className={classNames(
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
getBusinessPartyStatusColor(
customer.status ?? BusinessPartyStatusEnum.Prospect,
),
)}
>
{getBusinessPartyStatusName(
customer.status ?? BusinessPartyStatusEnum.Prospect,
)}
</span>
</div>
</td>
<td className="px-4 py-3">
<div className="space-y-1">
<div className="text-sm font-medium text-gray-900">
{(customer.totalRevenue ?? 0).toLocaleString()}
</div>
<div className="text-sm text-gray-500">
Ort. Sipariş: {(customer.averageOrderValue ?? 0).toLocaleString()}
</div>
<div className="flex items-center">
<FaStar size={14} className="text-yellow-500 mr-1" />
<span className="text-sm text-gray-600">
LTV: {((customer.lifetimeValue ?? 0) / 1000000).toFixed(1)}M
</span>
</div>
</div>
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium text-gray-900">
{customer.creditLimit.toLocaleString()}
</div>
<div className="text-sm text-gray-500">{customer.paymentTerms}</div>
</td>
<td className="px-4 py-3">
<div className="space-y-1">
{customer.lastOrderDate && (
<div className="flex items-center text-sm text-gray-900">
<FaCalendar size={14} className="mr-1" />
{dayjs(customer.lastOrderDate).format('DD.MM.YYYY')}
</div>
)}
<div className="text-sm text-gray-500">Son sipariş</div>
</div>
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end space-x-2">
<Link
to={ROUTES_ENUM.protected.crm.customersDetail.replace(':id', customer.id)}
className="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Detayları Görüntüle"
>
<FaEye size={16} />
</Link>
<Link
to={ROUTES_ENUM.protected.crm.customersEdit.replace(':id', customer.id)}
className="p-2 text-gray-600 hover:text-yellow-600 hover:bg-yellow-50 rounded-lg transition-colors"
title="Düzenle"
>
<FaEdit size={16} />
</Link>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{(!customers || customers.length === 0) && (
<div className="text-center py-12">
<FaUserCheck className="mx-auto h-10 w-10 text-gray-400" />
<h3 className="mt-2 text-xs font-medium text-gray-900">Müşteri bulunamadı</h3>
<p className="mt-1 text-xs text-gray-500">Yeni müşteri ekleyerek başlayın.</p>
<div className="mt-6">
<Link
to={ROUTES_ENUM.protected.crm.customersNew}
className="inline-flex items-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus size={16} className="mr-2" />
Yeni Müşteri Ekle
</Link>
</div>
</div>
)}
</div>
</div>
)
}
export default CustomerList