import { FormSelectCurrency } from "@/components/forms/select-currency" import { FormAvatar, FormInput, FormTextarea } from "@/components/forms/simple" import { Button } from "@/components/ui/button" import { formatCurrency } from "@/lib/utils" import { Currency } from "@/prisma/client" import { X } from "lucide-react" import { InputHTMLAttributes, memo, useCallback, useMemo } from "react" export interface InvoiceItem { name: string subtitle: string showSubtitle: boolean quantity: number unitPrice: number subtotal: number } export interface AdditionalTax { name: string rate: number amount: number } export interface AdditionalFee { name: string amount: number } export interface InvoiceFormData { title: string businessLogo: string | null invoiceNumber: string date: string dueDate: string currency: string companyDetails: string companyDetailsLabel: string billTo: string billToLabel: string items: InvoiceItem[] taxIncluded: boolean additionalTaxes: AdditionalTax[] additionalFees: AdditionalFee[] notes: string bankDetails: string issueDateLabel: string dueDateLabel: string itemLabel: string quantityLabel: string unitPriceLabel: string subtotalLabel: string summarySubtotalLabel: string summaryTotalLabel: string } interface InvoicePageProps { invoiceData: InvoiceFormData dispatch: React.Dispatch currencies: Currency[] } // Memoized row for invoice items const ItemRow = memo(function ItemRow({ item, index, onChange, onRemove, currency, }: { item: InvoiceItem index: number onChange: (index: number, field: keyof InvoiceItem, value: string | number | boolean) => void onRemove: (index: number) => void currency: string }) { return (
{/* Mobile view label (visible only on small screens) */}
Item
{/* Item name and subtitle */}
onChange(index, "name", e.target.value)} className="w-full min-w-0 font-semibold" placeholder="Item name" required />
{!item.showSubtitle ? ( ) : ( onChange(index, "subtitle", e.target.value)} className="w-full mt-1 text-xs text-muted-foreground" placeholder="Detailed description (optional)" /> )}
{/* Mobile labels for small screens */}
Quantity
Unit Price
Subtotal
{/* Quantity, Unit Price, Subtotal, and Remove button */}
onChange(index, "quantity", Number(e.target.value))} className="w-full text-right" required />
onChange(index, "unitPrice", Number(e.target.value))} className="w-full text-right" required />
{formatCurrency(item.subtotal * 100, currency)}
) }) // Memoized row for additional taxes const TaxRow = memo(function TaxRow({ tax, index, onChange, onRemove, currency, }: { tax: AdditionalTax index: number onChange: (index: number, field: keyof AdditionalTax, value: string | number) => void onRemove: (index: number) => void currency: string }) { return (
onChange(index, "name", e.target.value)} placeholder="Tax name" /> onChange(index, "rate", Number(e.target.value))} className="w-12 text-right" /> % {formatCurrency(tax.amount * 100, currency)}
) }) // Memoized row for additional fees const FeeRow = memo(function FeeRow({ fee, index, onChange, onRemove, currency, }: { fee: AdditionalFee index: number onChange: (index: number, field: keyof AdditionalFee, value: string | number) => void onRemove: (index: number) => void currency: string }) { return (
onChange(index, "name", e.target.value)} placeholder="Fee or discount name" /> onChange(index, "amount", Number(e.target.value))} className="w-16 text-right" /> {formatCurrency(fee.amount * 100, currency)}
) }) export function InvoicePage({ invoiceData, dispatch, currencies }: InvoicePageProps) { const addItem = useCallback(() => dispatch({ type: "ADD_ITEM" }), [dispatch]) const removeItem = useCallback((index: number) => dispatch({ type: "REMOVE_ITEM", index }), [dispatch]) const updateItem = useCallback( (index: number, field: keyof InvoiceItem, value: string | number | boolean) => dispatch({ type: "UPDATE_ITEM", index, field, value }), [dispatch] ) const addAdditionalTax = useCallback(() => dispatch({ type: "ADD_TAX" }), [dispatch]) const removeAdditionalTax = useCallback((index: number) => dispatch({ type: "REMOVE_TAX", index }), [dispatch]) const updateAdditionalTax = useCallback( (index: number, field: keyof AdditionalTax, value: string | number) => dispatch({ type: "UPDATE_TAX", index, field, value }), [dispatch] ) const addAdditionalFee = useCallback(() => dispatch({ type: "ADD_FEE" }), [dispatch]) const removeAdditionalFee = useCallback((index: number) => dispatch({ type: "REMOVE_FEE", index }), [dispatch]) const updateAdditionalFee = useCallback( (index: number, field: keyof AdditionalFee, value: string | number) => dispatch({ type: "UPDATE_FEE", index, field, value }), [dispatch] ) const subtotal = useMemo(() => invoiceData.items.reduce((sum, item) => sum + item.subtotal, 0), [invoiceData.items]) const taxes = useMemo( () => invoiceData.additionalTaxes.reduce((sum, tax) => sum + tax.amount, 0), [invoiceData.additionalTaxes] ) const fees = useMemo( () => invoiceData.additionalFees.reduce((sum, fee) => sum + fee.amount, 0), [invoiceData.additionalFees] ) const total = useMemo( () => (invoiceData.taxIncluded ? subtotal : subtotal + taxes) + fees, [invoiceData.taxIncluded, subtotal, taxes, fees] ) return (
{/* Gradient Background */}
{/* Invoice Header */}
dispatch({ type: "UPDATE_FIELD", field: "title", value: e.target.value })} className="text-2xl sm:text-4xl font-extrabold" placeholder="INVOICE" required /> dispatch({ type: "UPDATE_FIELD", field: "invoiceNumber", value: e.target.value })} className="w-full sm:w-[200px] font-medium" />
{ const file = e.target.files?.[0] if (file) { const objectUrl = URL.createObjectURL(file) dispatch({ type: "UPDATE_FIELD", field: "businessLogo", value: objectUrl }) } else { dispatch({ type: "UPDATE_FIELD", field: "businessLogo", value: null }) } }} />
{/* Company and Bill To */}
dispatch({ type: "UPDATE_FIELD", field: "companyDetailsLabel", value: e.target.value })} className="text-xs sm:text-sm font-medium" /> dispatch({ type: "UPDATE_FIELD", field: "companyDetails", value: e.target.value })} rows={4} placeholder="Your Company Name, Address, City, State, ZIP, Country, Tax ID" required />
dispatch({ type: "UPDATE_FIELD", field: "billToLabel", value: e.target.value })} className="text-xs sm:text-sm font-medium" /> dispatch({ type: "UPDATE_FIELD", field: "billTo", value: e.target.value })} rows={4} placeholder="Client Name, Address, City, State, ZIP, Country, Tax ID" required />
dispatch({ type: "UPDATE_FIELD", field: "issueDateLabel", value: e.target.value })} className="text-xs sm:text-sm font-medium" /> dispatch({ type: "UPDATE_FIELD", field: "date", value: e.target.value })} className="w-full border-b border-gray-300 py-1" required />
dispatch({ type: "UPDATE_FIELD", field: "dueDateLabel", value: e.target.value })} className="text-xs sm:text-sm font-medium" /> dispatch({ type: "UPDATE_FIELD", field: "dueDate", value: e.target.value })} required />
dispatch({ type: "UPDATE_FIELD", field: "currency", value })} />
{/* Items Section - Refactored to use only flex divs */}
{/* Header row for column titles */}
dispatch({ type: "UPDATE_FIELD", field: "itemLabel", value: e.target.value })} className="text-xs font-medium text-gray-500 uppercase tracking-wider" />
dispatch({ type: "UPDATE_FIELD", field: "quantityLabel", value: e.target.value })} className="text-xs font-medium text-gray-500 uppercase tracking-wider text-right w-full" />
dispatch({ type: "UPDATE_FIELD", field: "unitPriceLabel", value: e.target.value })} className="text-xs font-medium text-gray-500 uppercase tracking-wider text-right w-full" />
dispatch({ type: "UPDATE_FIELD", field: "subtotalLabel", value: e.target.value })} className="text-xs font-medium text-gray-500 uppercase tracking-wider text-right w-full" />
{/* Invoice items */}
{invoiceData.items.map((item, index) => ( ))}
{/* Notes */}
dispatch({ type: "UPDATE_FIELD", field: "notes", value: e.target.value })} className="w-full border border-gray-300 rounded p-2 text-xs sm:text-sm" rows={3} placeholder="Additional notes or terms" />
{/* Summary */}
dispatch({ type: "UPDATE_FIELD", field: "summarySubtotalLabel", value: e.target.value })} className="text-xs sm:text-sm font-medium text-gray-600" /> {formatCurrency(subtotal * 100, invoiceData.currency)}
{/* Additional Taxes */} {invoiceData.additionalTaxes.map((tax, index) => ( ))}
{invoiceData.additionalFees.map((fee, index) => ( ))}
dispatch({ type: "UPDATE_FIELD", field: "summaryTotalLabel", value: e.target.value })} className="text-sm sm:text-md font-bold" /> {formatCurrency(total * 100, invoiceData.currency)}
{/* Bank Details Footer */}