erp-platform/ui/src/views/accounting/components/InvoiceForm.tsx
2025-09-17 12:02:03 +03:00

508 lines
19 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, useEffect } from 'react'
import { FaSave, FaTimes, FaPlus, FaTrash, FaFileAlt } from 'react-icons/fa'
import {
FiInvoice,
FiInvoiceItem,
InvoiceTypeEnum,
InvoiceStatusEnum,
PaymentStatusEnum,
} from '../../../types/fi'
import { mockCurrentAccounts } from '../../../mocks/mockCurrentAccounts'
import { getInvoiceTypeText } from '@/utils/erp'
interface InvoiceFormProps {
invoice?: FiInvoice
onSave: (invoice: Partial<FiInvoice>) => void
onCancel: () => void
isVisible: boolean
}
const InvoiceForm: React.FC<InvoiceFormProps> = ({ invoice, onSave, onCancel, isVisible }) => {
const [formData, setFormData] = useState<Partial<FiInvoice>>({
invoiceNumber: '',
invoiceType: InvoiceTypeEnum.Sales,
currentAccountId: '',
invoiceDate: new Date(),
dueDate: new Date(),
deliveryDate: new Date(),
subtotal: 0,
taxAmount: 0,
discountAmount: 0,
totalAmount: 0,
paidAmount: 0,
remainingAmount: 0,
currency: 'TRY',
status: InvoiceStatusEnum.Draft,
paymentStatus: PaymentStatusEnum.Unpaid,
items: [],
waybillNumber: '',
notes: '',
})
const [newItem, setNewItem] = useState<Partial<FiInvoiceItem>>({
description: '',
quantity: 1,
unitPrice: 0,
taxRate: 18,
discountRate: 0,
unit: 'Adet',
})
useEffect(() => {
if (invoice) {
setFormData({
...invoice,
invoiceDate: new Date(invoice.invoiceDate),
dueDate: new Date(invoice.dueDate),
deliveryDate: invoice.deliveryDate ? new Date(invoice.deliveryDate) : new Date(),
})
} else {
// Generate new invoice number
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',
)}`
setFormData((prev) => ({
...prev,
invoiceNumber,
invoiceDate: now,
dueDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000), // 30 days
deliveryDate: now,
}))
}
}, [invoice])
const calculateItemTotal = (item: Partial<FiInvoiceItem>) => {
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
}
const calculateInvoiceTotals = (items: FiInvoiceItem[]) => {
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)
return {
subtotal,
discountAmount,
taxAmount,
totalAmount,
}
}
const handleAddItem = () => {
if (!newItem.description || !newItem.quantity || !newItem.unitPrice) {
alert('Lütfen ürün bilgilerini doldurun')
return
}
const item: FiInvoiceItem = {
id: Date.now().toString(),
invoiceId: formData.id || '',
description: newItem.description!,
quantity: newItem.quantity!,
unitPrice: newItem.unitPrice!,
unit: newItem.unit || 'Adet',
taxRate: newItem.taxRate || 18,
discountRate: newItem.discountRate || 0,
lineTotal: calculateItemTotal(newItem),
discountAmount:
(newItem.quantity || 0) * (newItem.unitPrice || 0) * ((newItem.discountRate || 0) / 100),
taxAmount:
((newItem.quantity || 0) * (newItem.unitPrice || 0) -
(newItem.quantity || 0) *
(newItem.unitPrice || 0) *
((newItem.discountRate || 0) / 100)) *
((newItem.taxRate || 0) / 100),
netAmount: calculateItemTotal(newItem),
}
const updatedItems = [...(formData.items || []), item]
const totals = calculateInvoiceTotals(updatedItems)
setFormData((prev) => ({
...prev,
items: updatedItems,
...totals,
remainingAmount: totals.totalAmount - (prev.paidAmount || 0),
}))
setNewItem({
description: '',
quantity: 1,
unitPrice: 0,
taxRate: 18,
discountRate: 0,
unit: 'Adet',
})
}
const handleRemoveItem = (itemId: string) => {
const updatedItems = formData.items?.filter((item) => item.id !== itemId) || []
const totals = calculateInvoiceTotals(updatedItems)
setFormData((prev) => ({
...prev,
items: updatedItems,
...totals,
remainingAmount: totals.totalAmount - (prev.paidAmount || 0),
}))
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!formData.currentAccountId) {
alert('Lütfen cari hesap seçin')
return
}
if (!formData.items?.length) {
alert('Lütfen en az bir ürün ekleyin')
return
}
onSave({
...formData,
id: invoice?.id || Date.now().toString(),
creationTime: invoice?.creationTime || new Date(),
lastModificationTime: new Date(),
})
}
const formatCurrency = (amount: number) => {
return amount.toLocaleString('tr-TR', {
style: 'currency',
currency: 'TRY',
minimumFractionDigits: 2,
})
}
if (!isVisible) return null
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">
{invoice ? 'Fatura Düzenle' : 'Yeni Fatura'}
</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>
<label className="block text-sm font-medium text-gray-700 mb-2">Fatura No</label>
<input
type="text"
value={formData.invoiceNumber}
onChange={(e) => setFormData({ ...formData, invoiceNumber: 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>
<label className="block text-sm font-medium text-gray-700 mb-2">Fatura Türü</label>
<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"
>
{Object.values(InvoiceTypeEnum).map((type) => (
<option key={type} value={type}>
{getInvoiceTypeText(type)}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">İrsaliye No</label>
<input
type="text"
value={formData.waybillNumber || ''}
onChange={(e) => setFormData({ ...formData, waybillNumber: 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Cari Hesap</label>
<select
value={formData.currentAccountId}
onChange={(e) => setFormData({ ...formData, currentAccountId: 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
>
<option value="">Cari Hesap Seçin</option>
{mockCurrentAccounts.map((account) => (
<option key={account.id} value={account.id}>
{account.accountCode}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Fatura Tarihi</label>
<input
type="date"
value={formData.invoiceDate?.toISOString().split('T')[0]}
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>
<label className="block text-sm font-medium text-gray-700 mb-2">Vade Tarihi</label>
<input
type="date"
value={formData.dueDate?.toISOString().split('T')[0]}
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">
<h3 className="text-base font-semibold text-gray-900 mb-3">Fatura Kalemleri</h3>
{/* 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"
value={newItem.description || ''}
onChange={(e) => setNewItem({ ...newItem, description: 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"
placeholder="Ürün/Hizmet açıklaması"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Miktar</label>
<input
type="number"
value={newItem.quantity || ''}
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"
value={newItem.unitPrice || ''}
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>
<label className="block text-sm font-medium text-gray-700 mb-1">KDV (%)</label>
<select
value={newItem.taxRate || 20}
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>
<option value={20}>20%</option>
</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">
<td className="px-3 py-2 text-gray-900">{item.description}</td>
<td className="px-3 py-2 text-gray-900 text-right">
{item.quantity.toLocaleString('tr-TR')}
</td>
<td className="px-3 py-2 text-gray-900 text-right">
{formatCurrency(item.unitPrice)}
</td>
<td className="px-3 py-2 text-gray-900 text-right">%{item.taxRate}</td>
<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>
<label className="block text-sm font-medium text-gray-700 mb-2">Notlar</label>
<textarea
value={formData.notes || ''}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
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>
<span className="font-medium">{formatCurrency(formData.subtotal || 0)}</span>
</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>
<span className="font-medium">{formatCurrency(formData.taxAmount || 0)}</span>
</div>
<div className="border-t pt-3">
<div className="flex justify-between items-center">
<span className="text-base font-semibold text-gray-900">Genel Toplam:</span>
<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" />
{invoice ? 'Güncelle' : 'Kaydet'}
</button>
</div>
</form>
</div>
</div>
)
}
export default InvoiceForm