erp-platform/ui/src/views/accounting/components/InvoiceManagement.tsx
2025-09-17 11:58:20 +03:00

506 lines
18 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 {
FaPlus,
FaSearch,
FaCalendar,
FaClock,
FaExclamationTriangle,
FaFileAlt,
FaEdit,
FaSave,
} 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'
import {
getInvoiceTypeColor,
getInvoiceTypeText,
getInvoiceStatusColor,
getInvoiceStatusText,
getPaymentStatusColor,
getPaymentStatusText,
} from '../../../utils/erp'
interface InvoiceManagementProps {
invoices: FiInvoice[]
onAdd: () => void
onEdit: (invoice: FiInvoice) => void
onConvertFromWaybill: () => void
onCreatePayment: (invoice: FiInvoice) => void
onViewDetails: (invoice: FiInvoice) => void
}
const InvoiceManagement: React.FC<InvoiceManagementProps> = ({
invoices,
onAdd,
onEdit,
onConvertFromWaybill,
onCreatePayment,
onViewDetails,
}) => {
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')
const filteredInvoices = invoices
.filter((invoice) => {
if (
searchTerm &&
!invoice.invoiceNumber.toLowerCase().includes(searchTerm.toLowerCase()) &&
!invoice.currentAccount?.accountCode?.toLowerCase().includes(searchTerm.toLowerCase()) &&
!invoice.waybillNumber?.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false
}
if (selectedType !== 'all' && invoice.invoiceType !== selectedType) {
return false
}
if (selectedStatus !== 'all' && invoice.status !== selectedStatus) {
return false
}
if (selectedPaymentStatus !== 'all' && invoice.paymentStatus !== selectedPaymentStatus) {
return false
}
return true
})
.sort((a, b) => {
switch (sortBy) {
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()
default:
return 0
}
})
const getDaysUntilDue = (dueDate: Date) => {
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
}
const formatCurrency = (amount: number) => {
return amount.toLocaleString('tr-TR', {
style: 'currency',
currency: 'TRY',
minimumFractionDigits: 2,
})
}
const columns: Column<FiInvoice>[] = [
{
key: 'invoiceNumber',
header: 'Fatura No',
sortable: true,
render: (invoice: FiInvoice) => (
<div>
<div className="font-medium text-gray-900">{invoice.invoiceNumber}</div>
{invoice.waybillNumber && (
<div className="text-sm text-gray-500">İrs: {invoice.waybillNumber}</div>
)}
</div>
),
},
{
key: 'type',
header: 'Tür',
render: (invoice: FiInvoice) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getInvoiceTypeColor(
invoice.invoiceType,
)}`}
>
{getInvoiceTypeText(invoice.invoiceType)}
</span>
),
},
{
key: 'currentAccount',
header: 'Cari Hesap',
render: (invoice: FiInvoice) => (
<div>
<div className="font-medium text-gray-900">
{invoice.currentAccount?.accountCode || 'Bilinmeyen'}
</div>
</div>
),
},
{
key: 'dates',
header: 'Tarihler',
render: (invoice: FiInvoice) => {
const daysUntilDue = getDaysUntilDue(invoice.dueDate)
return (
<div className="text-sm">
<div className="flex items-center gap-1">
<FaCalendar className="w-3 h-3 text-gray-400" />
<span>Fatura: {new Date(invoice.invoiceDate).toLocaleDateString('tr-TR')}</span>
</div>
<div className="flex items-center gap-1">
<FaClock className="w-3 h-3 text-gray-400" />
<span
className={
daysUntilDue < 0
? 'text-red-600'
: daysUntilDue <= 7
? 'text-orange-600'
: 'text-gray-600'
}
>
Vade: {new Date(invoice.dueDate).toLocaleDateString('tr-TR')}
</span>
</div>
{daysUntilDue < 0 && (
<div className="text-xs text-red-600">{Math.abs(daysUntilDue)} gün gecikme</div>
)}
</div>
)
},
},
{
key: 'amounts',
header: 'Tutarlar',
render: (invoice: FiInvoice) => (
<div className="text-right">
<div className="font-medium text-gray-900">{formatCurrency(invoice.totalAmount)}</div>
{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>
),
},
{
key: 'status',
header: 'Durum',
render: (invoice: FiInvoice) => (
<div className="space-y-1">
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getInvoiceStatusColor(
invoice.status,
)}`}
>
{getInvoiceStatusText(invoice.status)}
</span>
<br />
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getPaymentStatusColor(
invoice.paymentStatus,
)}`}
>
{getPaymentStatusText(invoice.paymentStatus)}
</span>
</div>
),
},
{
key: 'actions',
header: 'İşlemler',
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>
),
},
]
// Calculate statistics
const totalInvoices = invoices.length
const totalSalesAmount = invoices
.filter((i) => i.invoiceType === InvoiceTypeEnum.Sales)
.reduce((sum, i) => sum + i.totalAmount, 0)
const totalPurchaseAmount = invoices
.filter((i) => i.invoiceType === InvoiceTypeEnum.Purchase)
.reduce((sum, i) => sum + i.totalAmount, 0)
const totalUnpaidAmount = invoices
.filter((i) => i.paymentStatus !== PaymentStatusEnum.Paid)
.reduce((sum, i) => sum + i.remainingAmount, 0)
// Overdue invoices
const overdueInvoices = invoices.filter(
(i) => i.paymentStatus !== PaymentStatusEnum.Paid && new Date(i.dueDate) < new Date(),
)
// Payment status distribution
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),
}))
// 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),
}))
return (
<div className="space-y-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Fatura Yönetimi</h2>
<p className="text-gray-600">Alış ve satış faturaları yönetimi</p>
</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">
<Widget title="Toplam Fatura" value={totalInvoices} color="blue" icon="FaFileAlt" />
<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">
<h3 className="text-base font-semibold text-gray-900 mb-3">Fatura Türü Dağılımı</h3>
<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(
type,
)}`}
>
{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">
<h3 className="text-base font-semibold text-gray-900 mb-3">Ödeme Durumu Dağılımı</h3>
<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(
status,
)}`}
>
{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" />
<h3 className="text-base font-semibold text-red-900">Vadesi Geçmiş Faturalar</h3>
</div>
<p className="text-sm text-red-700 mt-1">
{overdueInvoices.length} adet fatura vadesi geçmiş durumda. Toplam tutar:{' '}
{formatCurrency(overdueInvoices.reduce((sum, i) => sum + i.remainingAmount, 0))}
</p>
<div className="mt-2 space-y-1.5">
{overdueInvoices.slice(0, 3).map((invoice) => {
const daysOverdue = Math.abs(getDaysUntilDue(invoice.dueDate))
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>
)
})}
</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}
onChange={(e) => setSelectedType(e.target.value as InvoiceTypeEnum | 'all')}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">Tüm Türler</option>
{Object.values(InvoiceTypeEnum).map((type) => (
<option key={type} value={type}>
{getInvoiceTypeText(type)}
</option>
))}
</select>
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value as InvoiceStatusEnum | 'all')}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">Tüm Durumlar</option>
{Object.values(InvoiceStatusEnum).map((status) => (
<option key={status} value={status}>
{getInvoiceStatusText(status)}
</option>
))}
</select>
<select
value={selectedPaymentStatus}
onChange={(e) => setSelectedPaymentStatus(e.target.value as PaymentStatusEnum | 'all')}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">Tüm Ödeme Durumları</option>
{Object.values(PaymentStatusEnum).map((status) => (
<option key={status} value={status}>
{getPaymentStatusText(status)}
</option>
))}
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'date' | 'amount' | 'dueDate')}
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" />
<h3 className="text-base font-medium text-gray-900 mb-2">Fatura bulunamadı</h3>
<p className="text-sm text-gray-500">
Yeni bir fatura ekleyin veya arama kriterlerinizi değiştirin.
</p>
</div>
)}
</div>
)
}
export default InvoiceManagement