import { useState, useEffect } from "react"; import { useTranslate, useInput } from "ra-core"; import { Button } from "@/components/ds/ui/button"; import { Input } from "@/components/ds/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ds/ui/select"; import { Separator } from "@/components/ds/ui/separator"; import { Trash2, Plus, GripVertical } from "lucide-react"; import { TaxPresetSelector } from "./TaxPresetSelector"; import type { InvoiceItem } from "../types"; export const InvoiceItemsInput = () => { const translate = useTranslate(); const { field } = useInput({ source: "items" }); const { field: discountField } = useInput({ source: "discount" }); const { field: discountTypeField } = useInput({ source: "discount_type" }); const [items, setItems] = useState[]>(field.value || []); const [discount, setDiscount] = useState( Number(discountField.value) || 0, ); const [discountType, setDiscountType] = useState<"fixed" | "percentage">( discountTypeField.value || "fixed", ); const { field: subtotalField } = useInput({ source: "subtotal" }); const { field: taxTotalField } = useInput({ source: "tax_total" }); const { field: totalField } = useInput({ source: "total" }); const { field: amountPaidField } = useInput({ source: "amount_paid" }); const currencySymbol = "$"; // Multi-currency handled elsewhere via props, but $ is a safe default for symbols here. // Sync with form state useEffect(() => { field.onChange(items); }, [items]); // Sync from form state (e.g. when template loaded) useEffect(() => { if (field.value && JSON.stringify(field.value) !== JSON.stringify(items)) { setItems(field.value); } }, [field.value]); useEffect(() => { discountField.onChange(discount); }, [discount]); useEffect(() => { discountTypeField.onChange(discountType); }, [discountType]); // Totals logic const subtotal = items.reduce((sum, item) => sum + (item.line_total || 0), 0); const taxTotal = items.reduce((sum, item) => sum + (item.tax_amount || 0), 0); const calculatedTotal = discountType === "percentage" ? subtotal * (1 - discount / 100) + taxTotal : subtotal + taxTotal - discount; const total = Math.max(0, calculatedTotal); useEffect(() => { subtotalField.onChange(subtotal); }, [subtotal]); useEffect(() => { taxTotalField.onChange(taxTotal); }, [taxTotal]); useEffect(() => { totalField.onChange(total); }, [total]); useEffect(() => { if (amountPaidField.value === undefined || amountPaidField.value === null) { amountPaidField.onChange(0); } }, []); const addItem = () => { setItems([ ...items, { description: "", quantity: 1, unit_price: 0, tax_rate: 0, tax_name: "", item_type: "service", sort_order: items.length, }, ]); }; const removeItem = (index: number) => { setItems(items.filter((_, i) => i !== index)); }; const updateItem = (index: number, field: string, value: any) => { const newItems = [...items]; newItems[index] = { ...newItems[index], [field]: value, }; // Recalculate line totals const item = newItems[index]; const quantity = Number(item.quantity) || 0; const unitPrice = Number(item.unit_price) || 0; const taxRate = Number(item.tax_rate) || 0; item.line_total = quantity * unitPrice; item.tax_amount = (item.line_total * taxRate) / 100; item.line_total_with_tax = item.line_total + item.tax_amount; setItems(newItems); }; const itemTypeChoices = [ { value: "service", label: translate("resources.invoices.item_type.service"), }, { value: "product", label: translate("resources.invoices.item_type.product"), }, { value: "hour", label: translate("resources.invoices.item_type.hour") }, { value: "day", label: translate("resources.invoices.item_type.day") }, { value: "deposit", label: translate("resources.invoices.item_type.deposit"), }, ]; // Totals moved up to be used in effects return (
{items.length === 0 ? (

{translate("resources.invoices.empty_items")}

) : ( <>
{items.map((item, index) => (
{/* Drag Handle */}
{/* Description (Title) */}
updateItem(index, "description", e.target.value) } />
{/* Item Type */}
{/* Quantity */}
updateItem( index, "quantity", parseFloat(e.target.value) || 0, ) } />
{/* Unit Price */}
updateItem( index, "unit_price", parseFloat(e.target.value) || 0, ) } />
updateItem( index, "tax_rate", parseFloat(e.target.value) || 0, ) } />
updateItem(index, "tax_rate", rate)} />
{/* Line Total */}
{(item.line_total_with_tax || 0).toFixed(2)}
{/* Delete Button */}
{/* Extended Description Row */}
updateItem(index, "item_description", e.target.value) } />
))}
{/* Add Item Button */} {/* Totals Summary */}
{translate("resources.invoices.fields.subtotal")}: {subtotal.toFixed(2)}
{translate("resources.invoices.fields.discount") || "Discount"} :
setDiscount(parseFloat(e.target.value) || 0) } />
{translate("resources.invoices.fields.tax_total")}: {taxTotal.toFixed(2)}
{translate("resources.invoices.fields.total")}: {total.toFixed(2)}
{/* Totals are now synced via useInput effects */} )}
); };