import { areMoneyDTOEqual } from "@erp/core"; import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks"; import * as React from "react"; import { UseFormReturn } from "react-hook-form"; import { CustomerInvoiceFormData, CustomerInvoiceItemFormData } from "../../schemas"; /** * Hook que recalcula automáticamente los totales de cada línea * y los totales generales de la factura cuando cambian los valores relevantes. */ export function useInvoiceAutoRecalc(form: UseFormReturn) { const { watch, setValue, getValues, formState: { isDirty, isLoading, isSubmitting }, } = form; const moneyHelper = useMoney(); const qtyHelper = useQuantity(); const pctHelper = usePercentage(); // Cálculo de una línea const calculateItemTotals = React.useCallback( (item: CustomerInvoiceItemFormData) => { if (!item) { const zero = moneyHelper.fromNumber(0); return { subtotalDTO: zero, discountAmountDTO: zero, taxableBaseDTO: zero, taxesDTO: zero, totalDTO: zero, }; } // Subtotal = unit_amount × quantity const subtotalDTO = moneyHelper.multiply(item.unit_amount, qtyHelper.toNumber(item.quantity)); // Descuento = subtotal × (discount_percentage / 100) const discountDTO = moneyHelper.percentage( subtotalDTO, pctHelper.toNumber(item.discount_percentage) ); // Base imponible = subtotal − descuento const taxableBaseDTO = moneyHelper.sub(subtotalDTO, discountDTO); // Impuestos (placeholder: se integrará con tax catalog) const taxesDTO = moneyHelper.fromNumber(0); // Total = base imponible + impuestos const totalDTO = moneyHelper.add(taxableBaseDTO, taxesDTO); return { subtotalDTO, discountAmountDTO: discountDTO, taxableBaseDTO, taxesDTO, totalDTO, }; }, [moneyHelper, qtyHelper, pctHelper] ); // Cálculo de los totales de la factura a partir de los conceptos const calculateInvoiceTotals = React.useCallback( (items: CustomerInvoiceItemFormData[]) => { let subtotalDTO = moneyHelper.fromNumber(0); let discountTotalDTO = moneyHelper.fromNumber(0); let taxableBaseDTO = moneyHelper.fromNumber(0); let taxesDTO = moneyHelper.fromNumber(0); let totalDTO = moneyHelper.fromNumber(0); for (const item of items) { const t = calculateItemTotals(item); subtotalDTO = moneyHelper.add(subtotalDTO, t.subtotalDTO); discountTotalDTO = moneyHelper.add(discountTotalDTO, t.discountAmountDTO); taxableBaseDTO = moneyHelper.add(taxableBaseDTO, t.taxableBaseDTO); taxesDTO = moneyHelper.add(taxesDTO, t.taxesDTO); totalDTO = moneyHelper.add(totalDTO, t.totalDTO); } return { subtotalDTO, discountTotalDTO, taxableBaseDTO, taxesDTO, totalDTO, }; }, [moneyHelper, calculateItemTotals] ); // Suscribirse a cambios del formulario React.useEffect(() => { if (!isDirty || isLoading || isSubmitting) { return; } const subscription = watch((formData, { name, type }) => { if (!formData?.items?.length) return; // 1. Si cambia una línea completa (add/remove/move) if (name === "items" && type === "change") { formData.items.forEach((item, i) => { if (!item) return; const typedItem = item as CustomerInvoiceItemFormData; const totals = calculateItemTotals(typedItem); const current = getValues(`items.${i}.total_amount`); if (!areMoneyDTOEqual(current, totals.totalDTO)) { setValue(`items.${i}.total_amount`, totals.totalDTO, { shouldDirty: true, shouldValidate: false, }); } }); // Recalcular importes totales de la factura y // actualizar valores calculados. const typedItems = formData.items as CustomerInvoiceItemFormData[]; const totalsGlobal = calculateInvoiceTotals(typedItems); setValue("subtotal_amount", totalsGlobal.subtotalDTO); setValue("discount_amount", totalsGlobal.discountTotalDTO); setValue("taxable_amount", totalsGlobal.taxableBaseDTO); setValue("taxes_amount", totalsGlobal.taxesDTO); setValue("total_amount", totalsGlobal.totalDTO); } // 2. Si cambia un campo dentro de un concepto if (name?.startsWith("items.") && type === "change") { const index = Number(name.split(".")[1]); const fieldName = name.split(".")[2]; if (["quantity", "unit_amount", "discount_percentage"].includes(fieldName)) { const typedItem = formData.items[index] as CustomerInvoiceItemFormData; if (!typedItem) return; // Recalcular línea const totals = calculateItemTotals(typedItem); const current = getValues(`items.${index}.total_amount`); if (!areMoneyDTOEqual(current, totals.totalDTO)) { setValue(`items.${index}.total_amount`, totals.totalDTO, { shouldDirty: true, shouldValidate: false, }); } // Recalcular importes totales de la factura y // actualizar valores calculados. const typedItems = formData.items as CustomerInvoiceItemFormData[]; const totalsGlobal = calculateInvoiceTotals(typedItems); setValue("subtotal_amount", totalsGlobal.subtotalDTO); setValue("discount_amount", totalsGlobal.discountTotalDTO); setValue("taxable_amount", totalsGlobal.taxableBaseDTO); setValue("taxes_amount", totalsGlobal.taxesDTO); setValue("total_amount", totalsGlobal.totalDTO); } } }); return () => subscription.unsubscribe(); }, [ watch, isDirty, isLoading, isSubmitting, setValue, getValues, calculateItemTotals, calculateInvoiceTotals, ]); }