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

509 lines
19 KiB
TypeScript
Raw Normal View History

2025-09-17 09:02:03 +00:00
import React, { useState, useEffect } from 'react'
import { FaSave, FaTimes, FaPlus, FaTrash, FaFileAlt } from 'react-icons/fa'
2025-09-15 09:31:47 +00:00
import {
FiInvoice,
FiInvoiceItem,
InvoiceTypeEnum,
InvoiceStatusEnum,
PaymentStatusEnum,
2025-09-17 09:02:03 +00:00
} from '../../../types/fi'
import { mockCurrentAccounts } from '../../../mocks/mockCurrentAccounts'
import { getInvoiceTypeText } from '@/utils/erp'
2025-09-15 09:31:47 +00:00
interface InvoiceFormProps {
2025-09-17 09:02:03 +00:00
invoice?: FiInvoice
onSave: (invoice: Partial<FiInvoice>) => void
onCancel: () => void
isVisible: boolean
2025-09-15 09:31:47 +00:00
}
2025-09-17 09:02:03 +00:00
const InvoiceForm: React.FC<InvoiceFormProps> = ({ invoice, onSave, onCancel, isVisible }) => {
2025-09-15 09:31:47 +00:00
const [formData, setFormData] = useState<Partial<FiInvoice>>({
2025-09-17 09:02:03 +00:00
invoiceNumber: '',
2025-09-15 09:31:47 +00:00
invoiceType: InvoiceTypeEnum.Sales,
2025-09-17 09:02:03 +00:00
currentAccountId: '',
2025-09-15 09:31:47 +00:00
invoiceDate: new Date(),
dueDate: new Date(),
deliveryDate: new Date(),
subtotal: 0,
taxAmount: 0,
discountAmount: 0,
totalAmount: 0,
paidAmount: 0,
remainingAmount: 0,
2025-09-17 09:02:03 +00:00
currency: 'TRY',
2025-09-15 09:31:47 +00:00
status: InvoiceStatusEnum.Draft,
paymentStatus: PaymentStatusEnum.Unpaid,
items: [],
2025-09-17 09:02:03 +00:00
waybillNumber: '',
notes: '',
})
2025-09-15 09:31:47 +00:00
const [newItem, setNewItem] = useState<Partial<FiInvoiceItem>>({
2025-09-17 09:02:03 +00:00
description: '',
2025-09-15 09:31:47 +00:00
quantity: 1,
unitPrice: 0,
taxRate: 18,
discountRate: 0,
2025-09-17 09:02:03 +00:00
unit: 'Adet',
})
2025-09-15 09:31:47 +00:00
useEffect(() => {
if (invoice) {
setFormData({
...invoice,
invoiceDate: new Date(invoice.invoiceDate),
dueDate: new Date(invoice.dueDate),
2025-09-17 09:02:03 +00:00
deliveryDate: invoice.deliveryDate ? new Date(invoice.deliveryDate) : new Date(),
})
2025-09-15 09:31:47 +00:00
} else {
// Generate new invoice number
2025-09-17 09:02:03 +00:00
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const invoiceNumber = `FT${year}${month}${String(Math.floor(Math.random() * 10000)).padStart(
4,
'0',
)}`
2025-09-15 09:31:47 +00:00
setFormData((prev) => ({
...prev,
invoiceNumber,
invoiceDate: now,
dueDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000), // 30 days
deliveryDate: now,
2025-09-17 09:02:03 +00:00
}))
2025-09-15 09:31:47 +00:00
}
2025-09-17 09:02:03 +00:00
}, [invoice])
2025-09-15 09:31:47 +00:00
const calculateItemTotal = (item: Partial<FiInvoiceItem>) => {
2025-09-17 09:02:03 +00:00
const subtotal = (item.quantity || 0) * (item.unitPrice || 0)
const discountAmount = subtotal * ((item.discountRate || 0) / 100)
const taxableAmount = subtotal - discountAmount
const taxAmount = taxableAmount * ((item.taxRate || 0) / 100)
return taxableAmount + taxAmount
}
2025-09-15 09:31:47 +00:00
const calculateInvoiceTotals = (items: FiInvoiceItem[]) => {
2025-09-17 09:02:03 +00:00
const subtotal = items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0)
const discountAmount = items.reduce((sum, item) => sum + item.discountAmount, 0)
const taxAmount = items.reduce((sum, item) => sum + item.taxAmount, 0)
const totalAmount = items.reduce((sum, item) => sum + item.lineTotal, 0)
2025-09-15 09:31:47 +00:00
return {
subtotal,
discountAmount,
taxAmount,
totalAmount,
2025-09-17 09:02:03 +00:00
}
}
2025-09-15 09:31:47 +00:00
const handleAddItem = () => {
if (!newItem.description || !newItem.quantity || !newItem.unitPrice) {
2025-09-17 09:02:03 +00:00
alert('Lütfen ürün bilgilerini doldurun')
return
2025-09-15 09:31:47 +00:00
}
const item: FiInvoiceItem = {
id: Date.now().toString(),
2025-09-17 09:02:03 +00:00
invoiceId: formData.id || '',
2025-09-15 09:31:47 +00:00
description: newItem.description!,
quantity: newItem.quantity!,
unitPrice: newItem.unitPrice!,
2025-09-17 09:02:03 +00:00
unit: newItem.unit || 'Adet',
2025-09-15 09:31:47 +00:00
taxRate: newItem.taxRate || 18,
discountRate: newItem.discountRate || 0,
lineTotal: calculateItemTotal(newItem),
discountAmount:
2025-09-17 09:02:03 +00:00
(newItem.quantity || 0) * (newItem.unitPrice || 0) * ((newItem.discountRate || 0) / 100),
2025-09-15 09:31:47 +00:00
taxAmount:
((newItem.quantity || 0) * (newItem.unitPrice || 0) -
(newItem.quantity || 0) *
(newItem.unitPrice || 0) *
((newItem.discountRate || 0) / 100)) *
((newItem.taxRate || 0) / 100),
netAmount: calculateItemTotal(newItem),
2025-09-17 09:02:03 +00:00
}
2025-09-15 09:31:47 +00:00
2025-09-17 09:02:03 +00:00
const updatedItems = [...(formData.items || []), item]
const totals = calculateInvoiceTotals(updatedItems)
2025-09-15 09:31:47 +00:00
setFormData((prev) => ({
...prev,
items: updatedItems,
...totals,
remainingAmount: totals.totalAmount - (prev.paidAmount || 0),
2025-09-17 09:02:03 +00:00
}))
2025-09-15 09:31:47 +00:00
setNewItem({
2025-09-17 09:02:03 +00:00
description: '',
2025-09-15 09:31:47 +00:00
quantity: 1,
unitPrice: 0,
taxRate: 18,
discountRate: 0,
2025-09-17 09:02:03 +00:00
unit: 'Adet',
})
}
2025-09-15 09:31:47 +00:00
const handleRemoveItem = (itemId: string) => {
2025-09-17 09:02:03 +00:00
const updatedItems = formData.items?.filter((item) => item.id !== itemId) || []
const totals = calculateInvoiceTotals(updatedItems)
2025-09-15 09:31:47 +00:00
setFormData((prev) => ({
...prev,
items: updatedItems,
...totals,
remainingAmount: totals.totalAmount - (prev.paidAmount || 0),
2025-09-17 09:02:03 +00:00
}))
}
2025-09-15 09:31:47 +00:00
const handleSubmit = (e: React.FormEvent) => {
2025-09-17 09:02:03 +00:00
e.preventDefault()
2025-09-15 09:31:47 +00:00
if (!formData.currentAccountId) {
2025-09-17 09:02:03 +00:00
alert('Lütfen cari hesap seçin')
return
2025-09-15 09:31:47 +00:00
}
if (!formData.items?.length) {
2025-09-17 09:02:03 +00:00
alert('Lütfen en az bir ürün ekleyin')
return
2025-09-15 09:31:47 +00:00
}
onSave({
...formData,
id: invoice?.id || Date.now().toString(),
creationTime: invoice?.creationTime || new Date(),
lastModificationTime: new Date(),
2025-09-17 09:02:03 +00:00
})
}
2025-09-15 09:31:47 +00:00
const formatCurrency = (amount: number) => {
2025-09-17 09:02:03 +00:00
return amount.toLocaleString('tr-TR', {
style: 'currency',
currency: 'TRY',
2025-09-15 09:31:47 +00:00
minimumFractionDigits: 2,
2025-09-17 09:02:03 +00:00
})
}
2025-09-15 09:31:47 +00:00
2025-09-17 09:02:03 +00:00
if (!isVisible) return null
2025-09-15 09:31:47 +00:00
return (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<FaFileAlt className="w-5 h-5 text-blue-600" />
<h2 className="text-lg font-semibold text-gray-900">
2025-09-17 09:02:03 +00:00
{invoice ? 'Fatura Düzenle' : 'Yeni Fatura'}
2025-09-15 09:31:47 +00:00
</h2>
</div>
<button
onClick={onCancel}
className="p-2 hover:bg-gray-100 rounded-md transition-colors"
>
<FaTimes className="w-5 h-5 text-gray-500" />
</button>
</div>
</div>
<form onSubmit={handleSubmit} className="p-4">
{/* Basic Information */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div>
2025-09-17 09:02:03 +00:00
<label className="block text-sm font-medium text-gray-700 mb-2">Fatura No</label>
2025-09-15 09:31:47 +00:00
<input
type="text"
value={formData.invoiceNumber}
2025-09-17 09:02:03 +00:00
onChange={(e) => setFormData({ ...formData, invoiceNumber: e.target.value })}
2025-09-15 09:31:47 +00:00
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"
required
/>
</div>
<div>
2025-09-17 09:02:03 +00:00
<label className="block text-sm font-medium text-gray-700 mb-2">Fatura Türü</label>
2025-09-15 09:31:47 +00:00
<select
value={formData.invoiceType}
onChange={(e) =>
setFormData({
...formData,
invoiceType: e.target.value as InvoiceTypeEnum,
})
}
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"
>
2025-09-17 09:02:03 +00:00
{Object.values(InvoiceTypeEnum).map((type) => (
<option key={type} value={type}>
{getInvoiceTypeText(type)}
</option>
))}
2025-09-15 09:31:47 +00:00
</select>
</div>
<div>
2025-09-17 09:02:03 +00:00
<label className="block text-sm font-medium text-gray-700 mb-2">İrsaliye No</label>
2025-09-15 09:31:47 +00:00
<input
type="text"
2025-09-17 09:02:03 +00:00
value={formData.waybillNumber || ''}
onChange={(e) => setFormData({ ...formData, waybillNumber: e.target.value })}
2025-09-15 09:31:47 +00:00
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>
2025-09-17 09:02:03 +00:00
<label className="block text-sm font-medium text-gray-700 mb-2">Cari Hesap</label>
2025-09-15 09:31:47 +00:00
<select
value={formData.currentAccountId}
2025-09-17 09:02:03 +00:00
onChange={(e) => setFormData({ ...formData, currentAccountId: e.target.value })}
2025-09-15 09:31:47 +00:00
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"
required
>
<option value="">Cari Hesap Seçin</option>
{mockCurrentAccounts.map((account) => (
<option key={account.id} value={account.id}>
{account.accountCode}
</option>
))}
</select>
</div>
<div>
2025-09-17 09:02:03 +00:00
<label className="block text-sm font-medium text-gray-700 mb-2">Fatura Tarihi</label>
2025-09-15 09:31:47 +00:00
<input
type="date"
2025-09-17 09:02:03 +00:00
value={formData.invoiceDate?.toISOString().split('T')[0]}
2025-09-15 09:31:47 +00:00
onChange={(e) =>
setFormData({
...formData,
invoiceDate: 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"
required
/>
</div>
<div>
2025-09-17 09:02:03 +00:00
<label className="block text-sm font-medium text-gray-700 mb-2">Vade Tarihi</label>
2025-09-15 09:31:47 +00:00
<input
type="date"
2025-09-17 09:02:03 +00:00
value={formData.dueDate?.toISOString().split('T')[0]}
2025-09-15 09:31:47 +00:00
onChange={(e) =>
setFormData({
...formData,
dueDate: 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"
required
/>
</div>
</div>
{/* Invoice Items */}
<div className="mb-6">
2025-09-17 09:02:03 +00:00
<h3 className="text-base font-semibold text-gray-900 mb-3">Fatura Kalemleri</h3>
2025-09-15 09:31:47 +00:00
{/* Add New Item */}
<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="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Ürün/Hizmet ıklaması
</label>
<input
type="text"
2025-09-17 09:02:03 +00:00
value={newItem.description || ''}
onChange={(e) => setNewItem({ ...newItem, description: e.target.value })}
2025-09-15 09:31:47 +00:00
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="Ürün/Hizmet açıklaması"
/>
</div>
<div>
2025-09-17 09:02:03 +00:00
<label className="block text-sm font-medium text-gray-700 mb-1">Miktar</label>
2025-09-15 09:31:47 +00:00
<input
type="number"
2025-09-17 09:02:03 +00:00
value={newItem.quantity || ''}
2025-09-15 09:31:47 +00:00
onChange={(e) =>
setNewItem({
...newItem,
quantity: 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"
step="0.01"
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Birim Fiyat
</label>
<input
type="number"
2025-09-17 09:02:03 +00:00
value={newItem.unitPrice || ''}
2025-09-15 09:31:47 +00:00
onChange={(e) =>
setNewItem({
...newItem,
unitPrice: 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"
step="0.01"
min="0"
/>
</div>
<div>
2025-09-17 09:02:03 +00:00
<label className="block text-sm font-medium text-gray-700 mb-1">KDV (%)</label>
2025-09-15 09:31:47 +00:00
<select
2025-09-17 09:02:03 +00:00
value={newItem.taxRate || 20}
2025-09-15 09:31:47 +00:00
onChange={(e) =>
setNewItem({
...newItem,
taxRate: parseFloat(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"
>
<option value={0}>0%</option>
<option value={1}>1%</option>
<option value={8}>8%</option>
<option value={18}>18%</option>
2025-09-17 09:02:03 +00:00
<option value={20}>20%</option>
2025-09-15 09:31:47 +00:00
</select>
</div>
<div className="flex items-end">
<button
type="button"
onClick={handleAddItem}
className="w-full px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center justify-center gap-2"
>
<FaPlus className="w-4 h-4" />
Ekle
</button>
</div>
</div>
</div>
{/* Items List */}
{formData.items && formData.items.length > 0 && (
<div className="border border-gray-200 rounded-lg overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase">
ıklama
</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-700 uppercase">
Miktar
</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-700 uppercase">
Birim Fiyat
</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-700 uppercase">
KDV %
</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-700 uppercase">
Toplam
</th>
<th className="px-3 py-2 text-center text-xs font-medium text-gray-700 uppercase">
İşlem
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{formData.items.map((item) => (
<tr key={item.id} className="text-sm">
2025-09-17 09:02:03 +00:00
<td className="px-3 py-2 text-gray-900">{item.description}</td>
2025-09-15 09:31:47 +00:00
<td className="px-3 py-2 text-gray-900 text-right">
2025-09-17 09:02:03 +00:00
{item.quantity.toLocaleString('tr-TR')}
2025-09-15 09:31:47 +00:00
</td>
<td className="px-3 py-2 text-gray-900 text-right">
{formatCurrency(item.unitPrice)}
</td>
2025-09-17 09:02:03 +00:00
<td className="px-3 py-2 text-gray-900 text-right">%{item.taxRate}</td>
2025-09-15 09:31:47 +00:00
<td className="px-3 py-2 text-gray-900 text-right font-medium">
{formatCurrency(item.lineTotal)}
</td>
<td className="px-3 py-2 text-center">
<button
type="button"
onClick={() => handleRemoveItem(item.id)}
className="p-1 text-red-600 hover:bg-red-50 rounded"
>
<FaTrash className="w-4 h-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Totals */}
<div className="bg-gray-50 p-4 rounded-lg mb-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
2025-09-17 09:02:03 +00:00
<label className="block text-sm font-medium text-gray-700 mb-2">Notlar</label>
2025-09-15 09:31:47 +00:00
<textarea
2025-09-17 09:02:03 +00:00
value={formData.notes || ''}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
2025-09-15 09:31:47 +00:00
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"
placeholder="Fatura ile ilgili notlar..."
/>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Ara Toplam:</span>
2025-09-17 09:02:03 +00:00
<span className="font-medium">{formatCurrency(formData.subtotal || 0)}</span>
2025-09-15 09:31:47 +00:00
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">İndirim:</span>
<span className="font-medium text-red-600">
-{formatCurrency(formData.discountAmount || 0)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">KDV:</span>
2025-09-17 09:02:03 +00:00
<span className="font-medium">{formatCurrency(formData.taxAmount || 0)}</span>
2025-09-15 09:31:47 +00:00
</div>
<div className="border-t pt-3">
<div className="flex justify-between items-center">
2025-09-17 09:02:03 +00:00
<span className="text-base font-semibold text-gray-900">Genel Toplam:</span>
2025-09-15 09:31:47 +00:00
<span className="text-base font-bold text-blue-600">
{formatCurrency(formData.totalAmount || 0)}
</span>
</div>
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-1.5 text-sm border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors"
>
İptal
</button>
<button
type="submit"
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center gap-2"
>
<FaSave className="w-4 h-4" />
2025-09-17 09:02:03 +00:00
{invoice ? 'Güncelle' : 'Kaydet'}
2025-09-15 09:31:47 +00:00
</button>
</div>
</form>
</div>
</div>
2025-09-17 09:02:03 +00:00
)
}
2025-09-15 09:31:47 +00:00
2025-09-17 09:02:03 +00:00
export default InvoiceForm