import { useState, useCallback } from "react"; import { useRevalidator } from "react-router"; import { useForm, useFieldArray } from "react-hook-form"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { calcItemAmounts, calcItemAmountsKleinunternehmer, calcInvoiceTotals, formatCurrency, TAX_RATES } from "@/lib/tax"; import { Plus, Trash2 } from "lucide-react"; interface Customer { id: string; name: string; } interface ItemFormData { position: number; description: string; quantity: string; unit: string; unitPrice: string; taxRate: string; netAmount: number; taxAmount: number; grossAmount: number; } interface InvoiceFormValues { customerId: string; issueDate: string; deliveryDate: string; dueDate: string; notes: string; items: ItemFormData[]; } interface ServiceOption { id: string; name: string; description: string | null; unit: string | null; unitPrice: number; taxRate: number; } interface InvoiceFormProps { customers: Customer[]; companyId: string; onSubmit: (data: Record) => Promise; defaultValues?: Partial; defaultKleinunternehmer?: boolean; submitLabel?: string; services?: ServiceOption[]; } const defaultItem = (): ItemFormData => ({ position: 1, description: "", quantity: "1", unit: "Stück", unitPrice: "0.00", taxRate: "19", netAmount: 0, taxAmount: 0, grossAmount: 0, }); /** * A form for creating an invoice. * * @param {Object} props * @param {Customer[]} customers - List of customers * @param {string} companyId - Company ID * @param {(data: Record) => Promise} onSubmit - Callback for form submission * @param {Partial} defaultValues - Default values for the form * @param {boolean} defaultKleinunternehmer - Whether to use Kleinunternehmer (§19 UStG) by default * @param {string} submitLabel - Label for the submit button * @param {ServiceOption[]} services - List of services that can be added to the invoice */ export function InvoiceForm({ customers, companyId, onSubmit, defaultValues, defaultKleinunternehmer = false, submitLabel = "Rechnung erstellen", services = [] }: InvoiceFormProps) { const today = new Date().toISOString().split("T")[0]; const dueDefault = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]; const [kleinunternehmer, setKleinunternehmer] = useState(defaultKleinunternehmer); const [openDropdown, setOpenDropdown] = useState(null); const { revalidate } = useRevalidator(); const { register, handleSubmit, watch, setValue, control, formState: { errors, isSubmitting } } = useForm({ defaultValues: { customerId: defaultValues?.customerId ?? "", issueDate: defaultValues?.issueDate ?? today, deliveryDate: defaultValues?.deliveryDate ?? today, dueDate: defaultValues?.dueDate ?? dueDefault, notes: defaultValues?.notes ?? "", items: defaultValues?.items ?? [defaultItem()], }, }); const { fields, append, remove } = useFieldArray({ control, name: "items" }); const watchedItems = watch("items"); const recalcItem = useCallback((index: number) => { const item = watchedItems[index]; if (!item) return; const qty = parseFloat(item.quantity) || 0; const price = parseFloat(item.unitPrice) || 0; if (kleinunternehmer) { const { netAmount, taxAmount, grossAmount } = calcItemAmountsKleinunternehmer(qty, price); setValue(`items.${index}.netAmount`, netAmount); setValue(`items.${index}.taxAmount`, taxAmount); setValue(`items.${index}.grossAmount`, grossAmount); } else { const rate = parseFloat(item.taxRate) || 0; const { netAmount, taxAmount, grossAmount } = calcItemAmounts(qty, price, rate); setValue(`items.${index}.netAmount`, netAmount); setValue(`items.${index}.taxAmount`, taxAmount); setValue(`items.${index}.grossAmount`, grossAmount); } }, [watchedItems, setValue, kleinunternehmer]); const totals = calcInvoiceTotals( watchedItems.map((item) => ({ netAmount: item.netAmount ?? 0, taxAmount: item.taxAmount ?? 0, grossAmount: item.grossAmount ?? 0, })) ); /** * Handles form submission by processing the items and calculating the totals. * * If Kleinunternehmer are enabled, the tax rate for each item is set to 0. * * If the item does not have a description, the tax rate is set to 0. * * The function then saves the new services created and revalidates the form if necessary. * * Finally, the function calls the onSubmit callback with the processed data. */ async function handleFormSubmit(data: InvoiceFormValues) { const items = data.items.map((item, i) => { const qty = parseFloat(item.quantity) || 0; const price = parseFloat(item.unitPrice) || 0; if (kleinunternehmer) { const { netAmount, taxAmount, grossAmount } = calcItemAmountsKleinunternehmer(qty, price); return { position: i + 1, description: item.description, quantity: qty, unit: item.unit || undefined, unitPrice: price, taxRate: 0, netAmount, taxAmount, grossAmount, }; } const rate = parseFloat(item.taxRate) || 0; const { netAmount, taxAmount, grossAmount } = calcItemAmounts(qty, price, rate); return { position: i + 1, description: item.description, quantity: qty, unit: item.unit || undefined, unitPrice: price, taxRate: rate, netAmount, taxAmount, grossAmount, }; }); const totals = calcInvoiceTotals(items); // Neue Leistungen automatisch speichern const newServices = items.filter((item) => item.description.trim() && !services.some( (s) => s.name.toLowerCase() === item.description.trim().toLowerCase() || (s.description ?? "").toLowerCase() === item.description.trim().toLowerCase() ) ); await Promise.all( newServices.map((item) => fetch("/api/services", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ companyId, name: item.description.trim(), unit: item.unit || undefined, unitPrice: item.unitPrice, taxRate: item.taxRate, }), }) ) ); if (newServices.length > 0) revalidate(); await onSubmit({ companyId, customerId: data.customerId, issueDate: data.issueDate, deliveryDate: data.deliveryDate || undefined, dueDate: data.dueDate, notes: data.notes || undefined, kleinunternehmer, items, ...totals, }); } return (
{errors.customerId &&

Pflichtfeld

}

Rechnungspositionen

Beschreibung
Menge
{kleinunternehmer ? "Einzelpreis (brutto)" : "Einzelpreis"}
{!kleinunternehmer &&
MwSt.
}
Gesamt (brutto)
{fields.map((field, index) => (
{(() => { const descValue = watchedItems[index]?.description ?? ""; const filtered = services.filter((s) => descValue.length === 0 || s.name.toLowerCase().includes(descValue.toLowerCase()) || (s.description ?? "").toLowerCase().includes(descValue.toLowerCase()) ); return ( <> setValue(`items.${index}.description`, e.target.value)} onFocus={() => setOpenDropdown(index)} onBlur={() => setTimeout(() => setOpenDropdown(null), 100)} placeholder="Leistungsbeschreibung" className="text-sm" autoComplete="off" /> {openDropdown === index && services.length > 0 && filtered.length > 0 && (
{filtered.map((s) => ( ))}
)} ); })()}
recalcItem(index)} />
recalcItem(index)} />
{!kleinunternehmer && (
)}
{formatCurrency(watchedItems[index]?.grossAmount ?? 0)}
{fields.length > 1 && ( )}
))}
{kleinunternehmer ? ( <>
Gesamtbetrag {formatCurrency(totals.grossTotal)}

Dieser Rechnungsbetrag enthält nach §19 Abs. 1 UStG keine USt.

) : ( <>
Netto {formatCurrency(totals.netTotal)}
MwSt. {formatCurrency(totals.taxTotal)}
Gesamt (brutto) {formatCurrency(totals.grossTotal)}
)}