import { TaxCatalogProvider } from "@erp/core"; import * as React from "react"; import { UseFormReturn } from "react-hook-form"; import { InvoiceItemCalcResult, calculateInvoiceHeaderAmounts, calculateInvoiceItemAmounts, } from "../../domain"; import { InvoiceFormData, InvoiceItemFormData } from "../../schemas"; export type UseInvoiceAutoRecalcParams = { currency_code: string; taxCatalog: TaxCatalogProvider; }; /** * Hook que recalcula automáticamente los totales de cada línea * y los totales generales de la factura cuando cambian los valores relevantes. * Adaptado a formulario con números planos (no DTOs). */ export function useInvoiceAutoRecalc( form: UseFormReturn, params: UseInvoiceAutoRecalcParams ) { const { watch, setValue, getValues, trigger, formState: { isDirty, isLoading, isSubmitting }, } = form; const { currency_code, taxCatalog } = params; // Cache de los totales de cada línea de factura const itemCache = React.useRef>>( new Map() ); // Cache de los totales de la factura const invoiceTotalsRef = React.useRef>({ subtotal_amount: 0, discount_amount: 0, taxable_amount: 0, taxes_amount: 0, total_amount: 0, }); // Cálculo de una línea (usa dominio puro) const calculateItemTotals = React.useCallback( (item: InvoiceItemFormData) => { const sanitizeString = (v?: number | string) => v && !Number.isNaN(Number(v)) ? String(v) : "0"; return calculateInvoiceItemAmounts( { quantity: sanitizeString(item.quantity), unit_amount: sanitizeString(item.unit_amount), discount_percentage: sanitizeString(item.discount_percentage), tax_codes: item.tax_codes, }, currency_code, taxCatalog ); }, [taxCatalog, currency_code] ); // Recalculo incremental de cabecera const recalcInvoiceTotalsIncrementally = React.useCallback( ( prevTotals: ReturnType, prevItem?: ReturnType, newItem?: ReturnType ): ReturnType => { const adjust = (field: keyof typeof prevTotals) => prevTotals[field] - (prevItem?.[field] ?? 0) + (newItem?.[field] ?? 0); return { subtotal_amount: adjust("subtotal_amount"), discount_amount: adjust("discount_amount"), taxable_amount: adjust("taxable_amount"), taxes_amount: adjust("taxes_amount"), total_amount: adjust("total_amount"), }; }, [] ); // Totales globales (usa funciones del dominio) const calculateInvoiceTotals = React.useCallback( (items: InvoiceItemFormData[]) => { const lines = items .filter((i) => !i.is_non_valued) .map((i) => { const totals = calculateItemTotals(i); return { subtotal_amount: totals.subtotal_amount, discount_amount: totals.discount_amount, taxable_amount: totals.taxable_amount, taxes_amount: totals.taxes_amount, total_amount: totals.total_amount, }; }); return calculateInvoiceHeaderAmounts(lines, currency_code); }, [calculateItemTotals, currency_code] ); // Suscripción reactiva a cambios del formulario React.useEffect(() => { if (!isDirty || isLoading || isSubmitting) return; const subscription = watch(async (formData, { name, type }) => { const items = (formData?.items || []) as InvoiceItemFormData[]; if (items.length === 0) return; // Detectar cambios en la cabecera if (name === "discount_percentage") { // Recalcular totales de factura const invoiceTotals = calculateInvoiceTotals(items); // Cabecera setInvoiceTotals(form, invoiceTotals); // 3) valida una vez (opcional) await trigger([ "subtotal_amount", "discount_amount", "taxable_amount", "taxes_amount", "total_amount", ]); } // 2. Cambio puntual de una línea if (name?.startsWith("items.") && type === "change") { const index = Number(name.split(".")[1]); const field = name.split(".")[2]; if (["quantity", "unit_amount", "discount_percentage", "tax_codes"].includes(field)) { const item = items[index] as InvoiceItemFormData; const prevTotals = itemCache.current.get(index); const newTotals = calculateItemTotals(item); // Si no hay cambios en los totales, no tocamos nada const itemHasChanges = prevTotals && JSON.stringify(prevTotals) !== JSON.stringify(newTotals); if (!itemHasChanges) { return; } // El total de esta línea ha cambiado => actualizamos la cache itemCache.current.set(index, newTotals); // Actualizar los campos de esta línea setInvoiceItemTotals(form, index, newTotals); // Recalcular totales de factura //const itemTotals = calculateItemTotals(item); const invoiceTotals = calculateInvoiceTotals(items); // Actualizar totales globales incrementalmente //const prevTotals = invoiceTotalsRef.current; //const newTotals = recalcTotalsIncrementally(prevTotals, prevLine, newLine); //invoiceTotalsRef.current = newTotals; console.log(invoiceTotals); // Cabecera setInvoiceTotals(form, invoiceTotals); // 3) valida una vez (opcional) await trigger([ "items", "subtotal_amount", "discount_amount", "taxable_amount", "taxes_amount", "total_amount", ]); } } }); return () => subscription.unsubscribe(); }, [ watch, setValue, getValues, isLoading, isSubmitting, calculateItemTotals, calculateInvoiceTotals, ]); } // Ayudante para rellenar los importes de una línea function setInvoiceItemTotals( form: UseFormReturn, index: number, newTotals: InvoiceItemCalcResult ) { const { setValue } = form; setValue(`items.${index}.subtotal_amount`, newTotals.subtotal_amount, { shouldDirty: true, shouldValidate: false, }); setValue(`items.${index}.discount_amount`, newTotals.discount_amount, { shouldDirty: true, shouldValidate: false, }); setValue(`items.${index}.taxable_amount`, newTotals.taxable_amount, { shouldDirty: true, shouldValidate: false, }); setValue(`items.${index}.taxes_amount`, newTotals.taxes_amount, { shouldDirty: true, shouldValidate: false, }); setValue(`items.${index}.total_amount`, newTotals.total_amount, { shouldDirty: true, shouldValidate: false, }); } // Ayudante para actualizar los importes de la cabecera function setInvoiceTotals( form: UseFormReturn, invoiceTotals: ReturnType ) { const { setValue } = form; setValue("subtotal_amount", invoiceTotals.subtotal_amount, { shouldDirty: true, shouldValidate: false, }); setValue("discount_amount", invoiceTotals.discount_amount, { shouldDirty: true, shouldValidate: false, }); setValue("taxable_amount", invoiceTotals.taxable_amount, { shouldDirty: true, shouldValidate: false, }); setValue("taxes_amount", invoiceTotals.taxes_amount, { shouldDirty: true, shouldValidate: false, }); setValue("total_amount", invoiceTotals.total_amount, { shouldDirty: true, shouldValidate: false, }); }