erp-platform/ui/src/views/supplychains/components/SupplierList.tsx
2025-09-16 11:40:38 +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 {
FaTruck,
FaPlus,
FaSearch,
FaFilter,
FaDownload,
FaEdit,
FaEye,
FaPhone,
FaEnvelope,
FaMapMarkerAlt,
FaStar,
FaExclamationTriangle,
FaArrowUp,
FaUsers,
FaBuilding,
} from 'react-icons/fa'
import classNames from 'classnames'
import { SupplierTypeEnum } from '../../../types/mm'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { PartyType } from '../../../types/common'
import { getSupplierTypeColor, getSupplierTypeText, getRatingColor } from '../../../utils/erp'
import { Container } from '@/components/shared'
const SupplierList: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('')
const [filterType, setFilterType] = useState('all')
const [showFilters, setShowFilters] = useState(false)
const {
data: suppliers,
isLoading,
error,
} = useQuery({
queryKey: ['suppliers', searchTerm, filterType],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 500))
const filtredSuppliers = mockBusinessParties.filter((a) => a.partyType === PartyType.Supplier)
return filtredSuppliers.filter((supplier) => {
const matchesSearch =
supplier.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
supplier.name.toLowerCase().includes(searchTerm.toLowerCase())
const matchesType = filterType === 'all' || supplier.supplierType === filterType
return matchesSearch && matchesType
})
},
})
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">Tedarikçiler 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">Tedarikçiler yüklenirken hata oluştu.</span>
</div>
</div>
)
}
return (
<Container>
<div className="space-y-2">
{/* Header Actions */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center space-x-4">
<div className="relative">
<FaSearch
size={20}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
/>
<input
type="text"
placeholder="Tedarikçi kodu veya firma adı..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-1.5 w-80 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 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={16} className="mr-2" />
Filtreler
</button>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
className="flex items-center px-3 py-1.5 border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
<FaDownload size={16} className="mr-2" />
Dışa Aktar
</button>
<Link
to="/admin/supplychain/suppliers/new"
className="flex items-center px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus size={16} className="mr-2" />
Yeni Tedarikçi
</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-3 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tedarikçi Türü
</label>
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tümü</option>
<option value={SupplierTypeEnum.Material}>Malzeme</option>
<option value={SupplierTypeEnum.Service}>Hizmet</option>
<option value={SupplierTypeEnum.Both}>Karma</option>
</select>
</div>
<div className="flex items-end">
<button
onClick={() => {
setFilterType('all')
setSearchTerm('')
}}
className="w-full px-3 py-1.5 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">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Toplam Tedarikçi</p>
<p className="text-xl font-bold text-gray-900">{suppliers?.length || 0}</p>
</div>
<FaBuilding className="h-8 w-8 text-blue-600" />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Aktif Tedarikçi</p>
<p className="text-xl font-bold text-green-600">
{suppliers?.filter((s) => s.isActive).length || 0}
</p>
</div>
<FaUsers className="h-8 w-8 text-green-600" />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Ortalama Puan</p>
<p className="text-xl font-bold text-yellow-600">
{suppliers?.length
? (
suppliers.reduce(
(acc, s) => acc + (s.performanceMetrics?.overallScore ?? 0),
0,
) / suppliers.length
).toFixed(1)
: '0.0'}
</p>
</div>
<FaStar className="h-8 w-8 text-yellow-600" />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Yüksek Performans</p>
<p className="text-xl font-bold text-purple-600">
{suppliers?.filter((s) => (s.performanceMetrics?.overallScore ?? 0) >= 4.5)
.length || 0}
</p>
</div>
<FaArrowUp className="h-8 w-8 text-purple-600" />
</div>
</div>
</div>
{/* Suppliers 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">Tedarikçi 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">
Tedarikçi 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">
Tür / Konum
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
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">
Durum
</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">
{suppliers?.map((supplier) => (
<tr key={supplier.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-10 w-10">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center">
<FaTruck className="h-5 w-5 text-blue-600" />
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{supplier.code}</div>
<div className="text-sm text-gray-500">{supplier.name}</div>
{supplier.taxNumber && (
<div className="text-xs text-gray-400 mt-1">
VKN: {supplier.taxNumber}
</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
<div className="space-y-1">
{supplier.primaryContact && (
<div className="text-sm font-medium text-gray-900">
{supplier.primaryContact.fullName}
</div>
)}
{supplier.email && (
<div className="flex items-center text-sm text-gray-500">
<FaEnvelope size={14} className="mr-1" />
{supplier.email}
</div>
)}
{supplier.phone && (
<div className="flex items-center text-sm text-gray-500">
<FaPhone size={14} className="mr-1" />
{supplier.phone}
</div>
)}
</div>
</td>
<td className="px-4 py-3">
<div className="space-y-1">
<span
className={classNames(
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
getSupplierTypeColor(supplier.supplierType!),
)}
>
{getSupplierTypeText(supplier.supplierType!)}
</span>
<div className="flex items-center text-sm text-gray-500">
<FaMapMarkerAlt size={14} className="mr-1" />
{supplier.address?.city}, {supplier.address?.country}
</div>
</div>
</td>
<td className="px-4 py-3">
<div className="space-y-1">
<div className="flex items-center">
<FaStar
size={14}
className={classNames(
'mr-1',
getRatingColor(supplier.performanceMetrics?.overallScore ?? 0),
)}
/>
<span
className={classNames(
'text-sm font-medium',
getRatingColor(supplier.performanceMetrics?.overallScore ?? 0),
)}
>
{(supplier.performanceMetrics?.overallScore ?? 0).toFixed(1)}
</span>
</div>
<div className="text-xs text-gray-500">
K:
{(supplier.performanceMetrics?.qualityRating ?? 0).toFixed(1)} T:
{(supplier.performanceMetrics?.deliveryPerformance ?? 0).toFixed(1)} F:
{(supplier.performanceMetrics?.priceCompetitiveness ?? 0).toFixed(1)}
</div>
{supplier.certifications && supplier.certifications.length > 0 && (
<div className="text-xs text-blue-600">
{supplier.certifications.join(', ')}
</div>
)}
</div>
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium text-gray-900">
{supplier.creditLimit.toLocaleString()}
</div>
<div className="text-sm text-gray-500">{supplier.paymentTerms}</div>
</td>
<td className="px-4 py-3">
<span
className={classNames(
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
supplier.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800',
)}
>
{supplier.isActive ? 'Aktif' : 'Pasif'}
</span>
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end space-x-2">
<Link
to={`/admin/supplychain/suppliers/${supplier.id}`}
className="p-1.5 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={`/admin/supplychain/suppliers/edit/${supplier.id}`}
className="p-1.5 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>
{(!suppliers || suppliers.length === 0) && (
<div className="text-center py-10">
<FaTruck className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">Tedarikçi bulunamadı</h3>
<p className="mt-1 text-sm text-gray-500">Yeni tedarikçi ekleyerek başlayın.</p>
<div className="mt-6">
<Link
to="/admin/supplychain/suppliers/new"
className="inline-flex items-center px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus size={16} className="mr-2" />
Yeni Tedarikçi Ekle
</Link>
</div>
</div>
)}
</div>
</div>
</Container>
)
}
export default SupplierList