erp-platform/ui/src/views/accounting/components/InvoiceManagement.tsx

507 lines
18 KiB
TypeScript
Raw Normal View History

2025-09-17 08:58:20 +00:00
import React, { useState } from 'react'
2025-09-15 09:31:47 +00:00
import {
FaPlus,
FaSearch,
FaCalendar,
FaClock,
FaExclamationTriangle,
FaFileAlt,
FaEdit,
FaSave,
2025-09-17 08:58:20 +00:00
} from 'react-icons/fa'
import { FiInvoice, InvoiceTypeEnum, InvoiceStatusEnum, PaymentStatusEnum } from '../../../types/fi'
import DataTable, { Column } from '../../../components/common/DataTable'
import Widget from '../../../components/common/Widget'
2025-09-15 09:31:47 +00:00
import {
getInvoiceTypeColor,
getInvoiceTypeText,
getInvoiceStatusColor,
getInvoiceStatusText,
getPaymentStatusColor,
getPaymentStatusText,
2025-09-17 08:58:20 +00:00
} from '../../../utils/erp'
2025-09-15 09:31:47 +00:00
interface InvoiceManagementProps {
2025-09-17 08:58:20 +00:00
invoices: FiInvoice[]
onAdd: () => void
onEdit: (invoice: FiInvoice) => void
onConvertFromWaybill: () => void
onCreatePayment: (invoice: FiInvoice) => void
onViewDetails: (invoice: FiInvoice) => void
2025-09-15 09:31:47 +00:00
}
const InvoiceManagement: React.FC<InvoiceManagementProps> = ({
invoices,
onAdd,
onEdit,
onConvertFromWaybill,
onCreatePayment,
onViewDetails,
}) => {
2025-09-17 08:58:20 +00:00
const [searchTerm, setSearchTerm] = useState('')
const [selectedType, setSelectedType] = useState<InvoiceTypeEnum | 'all'>('all')
const [selectedStatus, setSelectedStatus] = useState<InvoiceStatusEnum | 'all'>('all')
const [selectedPaymentStatus, setSelectedPaymentStatus] = useState<PaymentStatusEnum | 'all'>(
'all',
)
const [sortBy, setSortBy] = useState<'date' | 'amount' | 'dueDate'>('date')
2025-09-15 09:31:47 +00:00
const filteredInvoices = invoices
.filter((invoice) => {
if (
searchTerm &&
2025-09-17 08:58:20 +00:00
!invoice.invoiceNumber.toLowerCase().includes(searchTerm.toLowerCase()) &&
!invoice.currentAccount?.accountCode?.toLowerCase().includes(searchTerm.toLowerCase()) &&
2025-09-15 09:31:47 +00:00
!invoice.waybillNumber?.toLowerCase().includes(searchTerm.toLowerCase())
) {
2025-09-17 08:58:20 +00:00
return false
2025-09-15 09:31:47 +00:00
}
2025-09-17 08:58:20 +00:00
if (selectedType !== 'all' && invoice.invoiceType !== selectedType) {
return false
2025-09-15 09:31:47 +00:00
}
2025-09-17 08:58:20 +00:00
if (selectedStatus !== 'all' && invoice.status !== selectedStatus) {
return false
2025-09-15 09:31:47 +00:00
}
2025-09-17 08:58:20 +00:00
if (selectedPaymentStatus !== 'all' && invoice.paymentStatus !== selectedPaymentStatus) {
return false
2025-09-15 09:31:47 +00:00
}
2025-09-17 08:58:20 +00:00
return true
2025-09-15 09:31:47 +00:00
})
.sort((a, b) => {
switch (sortBy) {
2025-09-17 08:58:20 +00:00
case 'date':
return new Date(b.invoiceDate).getTime() - new Date(a.invoiceDate).getTime()
case 'amount':
return b.totalAmount - a.totalAmount
case 'dueDate':
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
2025-09-15 09:31:47 +00:00
default:
2025-09-17 08:58:20 +00:00
return 0
2025-09-15 09:31:47 +00:00
}
2025-09-17 08:58:20 +00:00
})
2025-09-15 09:31:47 +00:00
const getDaysUntilDue = (dueDate: Date) => {
2025-09-17 08:58:20 +00:00
const today = new Date()
const due = new Date(dueDate)
const diffTime = due.getTime() - today.getTime()
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
return diffDays
}
2025-09-15 09:31:47 +00:00
const formatCurrency = (amount: number) => {
2025-09-17 08:58:20 +00:00
return amount.toLocaleString('tr-TR', {
style: 'currency',
currency: 'TRY',
2025-09-15 09:31:47 +00:00
minimumFractionDigits: 2,
2025-09-17 08:58:20 +00:00
})
}
2025-09-15 09:31:47 +00:00
const columns: Column<FiInvoice>[] = [
{
2025-09-17 08:58:20 +00:00
key: 'invoiceNumber',
header: 'Fatura No',
2025-09-15 09:31:47 +00:00
sortable: true,
render: (invoice: FiInvoice) => (
<div>
2025-09-17 08:58:20 +00:00
<div className="font-medium text-gray-900">{invoice.invoiceNumber}</div>
2025-09-15 09:31:47 +00:00
{invoice.waybillNumber && (
2025-09-17 08:58:20 +00:00
<div className="text-sm text-gray-500">İrs: {invoice.waybillNumber}</div>
2025-09-15 09:31:47 +00:00
)}
</div>
),
},
{
2025-09-17 08:58:20 +00:00
key: 'type',
header: 'Tür',
2025-09-15 09:31:47 +00:00
render: (invoice: FiInvoice) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getInvoiceTypeColor(
2025-09-17 08:58:20 +00:00
invoice.invoiceType,
2025-09-15 09:31:47 +00:00
)}`}
>
{getInvoiceTypeText(invoice.invoiceType)}
</span>
),
},
{
2025-09-17 08:58:20 +00:00
key: 'currentAccount',
header: 'Cari Hesap',
2025-09-15 09:31:47 +00:00
render: (invoice: FiInvoice) => (
<div>
<div className="font-medium text-gray-900">
2025-09-17 08:58:20 +00:00
{invoice.currentAccount?.accountCode || 'Bilinmeyen'}
2025-09-15 09:31:47 +00:00
</div>
</div>
),
},
{
2025-09-17 08:58:20 +00:00
key: 'dates',
header: 'Tarihler',
2025-09-15 09:31:47 +00:00
render: (invoice: FiInvoice) => {
2025-09-17 08:58:20 +00:00
const daysUntilDue = getDaysUntilDue(invoice.dueDate)
2025-09-15 09:31:47 +00:00
return (
<div className="text-sm">
<div className="flex items-center gap-1">
<FaCalendar className="w-3 h-3 text-gray-400" />
2025-09-17 08:58:20 +00:00
<span>Fatura: {new Date(invoice.invoiceDate).toLocaleDateString('tr-TR')}</span>
2025-09-15 09:31:47 +00:00
</div>
<div className="flex items-center gap-1">
<FaClock className="w-3 h-3 text-gray-400" />
<span
className={
daysUntilDue < 0
2025-09-17 08:58:20 +00:00
? 'text-red-600'
2025-09-15 09:31:47 +00:00
: daysUntilDue <= 7
2025-09-17 08:58:20 +00:00
? 'text-orange-600'
: 'text-gray-600'
2025-09-15 09:31:47 +00:00
}
>
2025-09-17 08:58:20 +00:00
Vade: {new Date(invoice.dueDate).toLocaleDateString('tr-TR')}
2025-09-15 09:31:47 +00:00
</span>
</div>
{daysUntilDue < 0 && (
2025-09-17 08:58:20 +00:00
<div className="text-xs text-red-600">{Math.abs(daysUntilDue)} gün gecikme</div>
2025-09-15 09:31:47 +00:00
)}
</div>
2025-09-17 08:58:20 +00:00
)
2025-09-15 09:31:47 +00:00
},
},
{
2025-09-17 08:58:20 +00:00
key: 'amounts',
header: 'Tutarlar',
2025-09-15 09:31:47 +00:00
render: (invoice: FiInvoice) => (
<div className="text-right">
2025-09-17 08:58:20 +00:00
<div className="font-medium text-gray-900">{formatCurrency(invoice.totalAmount)}</div>
2025-09-15 09:31:47 +00:00
{invoice.paidAmount > 0 && (
<div className="text-sm text-green-600">
Ödenen: {formatCurrency(invoice.paidAmount)}
</div>
)}
{invoice.remainingAmount > 0 && (
<div className="text-sm text-orange-600">
Kalan: {formatCurrency(invoice.remainingAmount)}
</div>
)}
</div>
),
},
{
2025-09-17 08:58:20 +00:00
key: 'status',
header: 'Durum',
2025-09-15 09:31:47 +00:00
render: (invoice: FiInvoice) => (
<div className="space-y-1">
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getInvoiceStatusColor(
2025-09-17 08:58:20 +00:00
invoice.status,
2025-09-15 09:31:47 +00:00
)}`}
>
{getInvoiceStatusText(invoice.status)}
</span>
<br />
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getPaymentStatusColor(
2025-09-17 08:58:20 +00:00
invoice.paymentStatus,
2025-09-15 09:31:47 +00:00
)}`}
>
{getPaymentStatusText(invoice.paymentStatus)}
</span>
</div>
),
},
{
2025-09-17 08:58:20 +00:00
key: 'actions',
header: 'İşlemler',
2025-09-15 09:31:47 +00:00
render: (invoice: FiInvoice) => (
<div className="flex gap-1">
{invoice.paymentStatus !== PaymentStatusEnum.Paid && (
<button
onClick={() => onCreatePayment(invoice)}
className="px-2 py-1 text-xs bg-green-50 text-green-600 rounded hover:bg-green-100 transition-colors"
title="Ödeme Kaydet"
>
<FaSave className="w-4 h-4" />
</button>
)}
<button
onClick={() => onViewDetails(invoice)}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
title="Detayları Görüntüle"
>
<FaFileAlt className="w-4 h-4" />
</button>
<button
onClick={() => onEdit(invoice)}
className="p-1 text-green-600 hover:bg-green-50 rounded"
title="Düzenle"
>
<FaEdit className="w-4 h-4" />
</button>
</div>
),
},
2025-09-17 08:58:20 +00:00
]
2025-09-15 09:31:47 +00:00
// Calculate statistics
2025-09-17 08:58:20 +00:00
const totalInvoices = invoices.length
2025-09-15 09:31:47 +00:00
const totalSalesAmount = invoices
.filter((i) => i.invoiceType === InvoiceTypeEnum.Sales)
2025-09-17 08:58:20 +00:00
.reduce((sum, i) => sum + i.totalAmount, 0)
2025-09-15 09:31:47 +00:00
const totalPurchaseAmount = invoices
.filter((i) => i.invoiceType === InvoiceTypeEnum.Purchase)
2025-09-17 08:58:20 +00:00
.reduce((sum, i) => sum + i.totalAmount, 0)
2025-09-15 09:31:47 +00:00
const totalUnpaidAmount = invoices
.filter((i) => i.paymentStatus !== PaymentStatusEnum.Paid)
2025-09-17 08:58:20 +00:00
.reduce((sum, i) => sum + i.remainingAmount, 0)
2025-09-15 09:31:47 +00:00
// Overdue invoices
const overdueInvoices = invoices.filter(
2025-09-17 08:58:20 +00:00
(i) => i.paymentStatus !== PaymentStatusEnum.Paid && new Date(i.dueDate) < new Date(),
)
2025-09-15 09:31:47 +00:00
// Payment status distribution
2025-09-17 08:58:20 +00:00
const paymentDistribution = Object.values(PaymentStatusEnum).map((status) => ({
status,
count: invoices.filter((i) => i.paymentStatus === status).length,
amount: invoices
.filter((i) => i.paymentStatus === status)
.reduce((sum, i) => sum + i.totalAmount, 0),
}))
2025-09-15 09:31:47 +00:00
// Invoice type distribution
const typeDistribution = Object.values(InvoiceTypeEnum).map((type) => ({
type,
count: invoices.filter((i) => i.invoiceType === type).length,
amount: invoices
.filter((i) => i.invoiceType === type)
.reduce((sum, i) => sum + i.totalAmount, 0),
2025-09-17 08:58:20 +00:00
}))
2025-09-15 09:31:47 +00:00
return (
2025-09-15 19:22:43 +00:00
<div className="space-y-2">
2025-09-15 09:31:47 +00:00
{/* Header */}
<div className="flex items-center justify-between">
<div>
2025-09-15 21:02:48 +00:00
<h2 className="text-2xl font-bold text-gray-900">Fatura Yönetimi</h2>
2025-09-17 08:58:20 +00:00
<p className="text-gray-600">Alış ve satış faturaları yönetimi</p>
2025-09-15 09:31:47 +00:00
</div>
<div className="flex gap-2 text-sm">
<button
onClick={onConvertFromWaybill}
className="flex items-center gap-2 px-3 py-1.5 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
>
<FaFileAlt className="w-4 h-4" />
İrsaliyeden Fatura
</button>
<button
onClick={onAdd}
className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
<FaPlus className="w-4 h-4" />
Yeni Fatura
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
2025-09-17 08:58:20 +00:00
<Widget title="Toplam Fatura" value={totalInvoices} color="blue" icon="FaFileAlt" />
2025-09-15 09:31:47 +00:00
<Widget
title="Satış Tutarı"
value={formatCurrency(totalSalesAmount)}
color="green"
icon="FaDollarSign"
/>
<Widget
title="Alış Tutarı"
value={formatCurrency(totalPurchaseAmount)}
color="red"
icon="FaDollarSign"
/>
<Widget
title="Ödenmemiş"
value={formatCurrency(totalUnpaidAmount)}
color="orange"
icon="FaExclamationTriangle"
/>
</div>
{/* Distribution Charts */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white rounded-lg shadow-sm border p-4">
2025-09-17 08:58:20 +00:00
<h3 className="text-base font-semibold text-gray-900 mb-3">Fatura Türü Dağılımı</h3>
2025-09-15 09:31:47 +00:00
<div className="space-y-2">
{typeDistribution.map(({ type, count, amount }) => (
<div key={type} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getInvoiceTypeColor(
2025-09-17 08:58:20 +00:00
type,
2025-09-15 09:31:47 +00:00
)}`}
>
{getInvoiceTypeText(type)}
</span>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">{count} fatura</span>
<span className="text-sm font-medium text-gray-900">
{formatCurrency(amount)}
</span>
</div>
</div>
))}
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border p-4">
2025-09-17 08:58:20 +00:00
<h3 className="text-base font-semibold text-gray-900 mb-3">Ödeme Durumu Dağılımı</h3>
2025-09-15 09:31:47 +00:00
<div className="space-y-2">
{paymentDistribution.map(({ status, count, amount }) => (
<div key={status} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getPaymentStatusColor(
2025-09-17 08:58:20 +00:00
status,
2025-09-15 09:31:47 +00:00
)}`}
>
{getPaymentStatusText(status)}
</span>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">{count} fatura</span>
<span className="text-sm font-medium text-gray-900">
{formatCurrency(amount)}
</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Overdue Invoices Alert */}
{overdueInvoices.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<div className="flex items-center gap-2">
<FaExclamationTriangle className="w-5 h-5 text-red-600" />
2025-09-17 08:58:20 +00:00
<h3 className="text-base font-semibold text-red-900">Vadesi Geçmiş Faturalar</h3>
2025-09-15 09:31:47 +00:00
</div>
<p className="text-sm text-red-700 mt-1">
2025-09-17 08:58:20 +00:00
{overdueInvoices.length} adet fatura vadesi geçmiş durumda. Toplam tutar:{' '}
{formatCurrency(overdueInvoices.reduce((sum, i) => sum + i.remainingAmount, 0))}
2025-09-15 09:31:47 +00:00
</p>
<div className="mt-2 space-y-1.5">
{overdueInvoices.slice(0, 3).map((invoice) => {
2025-09-17 08:58:20 +00:00
const daysOverdue = Math.abs(getDaysUntilDue(invoice.dueDate))
2025-09-15 09:31:47 +00:00
return (
<div
key={invoice.id}
className="flex items-center justify-between p-2 bg-white rounded border"
>
<div>
<span className="font-medium">{invoice.invoiceNumber}</span>
<span className="ml-2 text-sm text-gray-500">
- {invoice.currentAccount?.accountCode}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs bg-red-100 text-red-800 px-2 py-1 rounded">
{daysOverdue} gün gecikme
</span>
<span className="text-sm text-red-600">
{formatCurrency(invoice.remainingAmount)}
</span>
</div>
</div>
2025-09-17 08:58:20 +00:00
)
2025-09-15 09:31:47 +00:00
})}
</div>
</div>
)}
{/* Filters */}
<div className="flex gap-3 items-center">
<div className="flex-1 relative">
<FaSearch className="w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Fatura no, cari hesap veya irsaliye no ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<select
value={selectedType}
2025-09-17 08:58:20 +00:00
onChange={(e) => setSelectedType(e.target.value as InvoiceTypeEnum | 'all')}
2025-09-15 09:31:47 +00:00
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>
{Object.values(InvoiceTypeEnum).map((type) => (
<option key={type} value={type}>
{getInvoiceTypeText(type)}
</option>
))}
</select>
<select
value={selectedStatus}
2025-09-17 08:58:20 +00:00
onChange={(e) => setSelectedStatus(e.target.value as InvoiceStatusEnum | 'all')}
2025-09-15 09:31:47 +00:00
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">Tüm Durumlar</option>
{Object.values(InvoiceStatusEnum).map((status) => (
<option key={status} value={status}>
{getInvoiceStatusText(status)}
</option>
))}
</select>
<select
value={selectedPaymentStatus}
2025-09-17 08:58:20 +00:00
onChange={(e) => setSelectedPaymentStatus(e.target.value as PaymentStatusEnum | 'all')}
2025-09-15 09:31:47 +00:00
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 Ödeme Durumları</option>
{Object.values(PaymentStatusEnum).map((status) => (
<option key={status} value={status}>
{getPaymentStatusText(status)}
</option>
))}
</select>
<select
value={sortBy}
2025-09-17 08:58:20 +00:00
onChange={(e) => setSortBy(e.target.value as 'date' | 'amount' | 'dueDate')}
2025-09-15 09:31:47 +00:00
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="date">Tarihe Göre</option>
<option value="amount">Tutara Göre</option>
<option value="dueDate">Vade Tarihine Göre</option>
</select>
</div>
{/* Data Table */}
<div className="bg-white rounded-lg shadow-sm border overflow-x-auto">
<DataTable data={filteredInvoices} columns={columns} />
</div>
{filteredInvoices.length === 0 && (
<div className="text-center py-10">
<FaFileAlt className="w-12 h-12 text-gray-400 mx-auto mb-4" />
2025-09-17 08:58:20 +00:00
<h3 className="text-base font-medium text-gray-900 mb-2">Fatura bulunamadı</h3>
2025-09-15 09:31:47 +00:00
<p className="text-sm text-gray-500">
Yeni bir fatura ekleyin veya arama kriterlerinizi değiştirin.
</p>
</div>
)}
</div>
2025-09-17 08:58:20 +00:00
)
}
2025-09-15 09:31:47 +00:00
2025-09-17 08:58:20 +00:00
export default InvoiceManagement