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

561 lines
20 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
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";
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"
>
<option value={InvoiceTypeEnum.Sales}>Satış</option>
<option value={InvoiceTypeEnum.Purchase}>Alış</option>
<option value={InvoiceTypeEnum.Return}>İade</option>
<option value={InvoiceTypeEnum.Proforma}>Proforma</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 || 18}
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>
</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;