FactuGES_ERP/modules/customer-invoices/src/web/hooks/calcs/use-invoice-auto-recalc.ts

272 lines
8.0 KiB
TypeScript
Raw Normal View History

2025-10-12 18:36:33 +00:00
import { TaxCatalogProvider } from "@erp/core";
2025-10-11 16:44:38 +00:00
import * as React from "react";
import { UseFormReturn } from "react-hook-form";
2025-10-12 18:36:33 +00:00
import {
InvoiceItemCalcResult,
calculateInvoiceHeaderAmounts,
calculateInvoiceItemAmounts,
} from "../../domain";
2025-10-12 10:43:06 +00:00
import { InvoiceFormData, InvoiceItemFormData } from "../../schemas";
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
export type UseInvoiceAutoRecalcParams = {
currency_code: string;
taxCatalog: TaxCatalogProvider;
};
2025-10-11 16:44:38 +00:00
/**
* Hook que recalcula automáticamente los totales de cada línea
* y los totales generales de la factura cuando cambian los valores relevantes.
2025-10-12 18:36:33 +00:00
* Adaptado a formulario con números planos (no DTOs).
2025-10-11 16:44:38 +00:00
*/
2025-10-12 18:36:33 +00:00
export function useInvoiceAutoRecalc(
form: UseFormReturn<InvoiceFormData>,
params: UseInvoiceAutoRecalcParams
) {
2025-10-11 16:44:38 +00:00
const {
watch,
setValue,
getValues,
2025-10-12 18:36:33 +00:00
trigger,
2025-10-11 16:44:38 +00:00
formState: { isDirty, isLoading, isSubmitting },
} = form;
2025-10-12 18:36:33 +00:00
const { currency_code, taxCatalog } = params;
// Cache de los totales de cada línea de factura
const itemCache = React.useRef<Map<number, ReturnType<typeof calculateInvoiceItemAmounts>>>(
new Map()
);
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
// Cache de los totales de la factura
const invoiceTotalsRef = React.useRef<ReturnType<typeof calculateInvoiceHeaderAmounts>>({
subtotal_amount: 0,
discount_amount: 0,
taxable_amount: 0,
taxes_amount: 0,
total_amount: 0,
});
// Cálculo de una línea (usa dominio puro)
2025-10-11 16:44:38 +00:00
const calculateItemTotals = React.useCallback(
2025-10-12 10:43:06 +00:00
(item: InvoiceItemFormData) => {
2025-10-12 18:36:33 +00:00
const sanitizeString = (v?: number | string) =>
v && !Number.isNaN(Number(v)) ? String(v) : "0";
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
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
2025-10-11 16:44:38 +00:00
);
2025-10-12 18:36:33 +00:00
},
[taxCatalog, currency_code]
);
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
// Recalculo incremental de cabecera
const recalcInvoiceTotalsIncrementally = React.useCallback(
(
prevTotals: ReturnType<typeof calculateInvoiceHeaderAmounts>,
prevItem?: ReturnType<typeof calculateInvoiceItemAmounts>,
newItem?: ReturnType<typeof calculateInvoiceItemAmounts>
): ReturnType<typeof calculateInvoiceHeaderAmounts> => {
const adjust = (field: keyof typeof prevTotals) =>
prevTotals[field] - (prevItem?.[field] ?? 0) + (newItem?.[field] ?? 0);
2025-10-11 16:44:38 +00:00
return {
2025-10-12 18:36:33 +00:00
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"),
2025-10-11 16:44:38 +00:00
};
},
2025-10-12 18:36:33 +00:00
[]
2025-10-11 16:44:38 +00:00
);
2025-10-12 18:36:33 +00:00
// Totales globales (usa funciones del dominio)
2025-10-11 16:44:38 +00:00
const calculateInvoiceTotals = React.useCallback(
2025-10-12 10:43:06 +00:00
(items: InvoiceItemFormData[]) => {
2025-10-12 18:36:33 +00:00
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,
};
});
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
return calculateInvoiceHeaderAmounts(lines, currency_code);
2025-10-11 16:44:38 +00:00
},
2025-10-12 18:36:33 +00:00
[calculateItemTotals, currency_code]
2025-10-11 16:44:38 +00:00
);
2025-10-12 18:36:33 +00:00
// Suscripción reactiva a cambios del formulario
2025-10-11 16:44:38 +00:00
React.useEffect(() => {
2025-10-12 19:09:56 +00:00
console.log("recalculo algo?");
2025-10-12 18:36:33 +00:00
if (!isDirty || isLoading || isSubmitting) return;
2025-10-12 19:09:56 +00:00
const subscription = watch((formData, { name, type }) => {
console.log(name, type);
2025-10-12 18:36:33 +00:00
const items = (formData?.items || []) as InvoiceItemFormData[];
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
if (items.length === 0) return;
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
// 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)
2025-10-12 19:09:56 +00:00
trigger([
2025-10-12 18:36:33 +00:00
"subtotal_amount",
"discount_amount",
"taxable_amount",
"taxes_amount",
"total_amount",
]);
2025-10-11 16:44:38 +00:00
}
2025-10-12 18:36:33 +00:00
// 2. Cambio puntual de una línea
2025-10-11 16:44:38 +00:00
if (name?.startsWith("items.") && type === "change") {
2025-10-12 19:09:56 +00:00
console.log("2. items!");
2025-10-11 16:44:38 +00:00
const index = Number(name.split(".")[1]);
2025-10-12 18:36:33 +00:00
const field = name.split(".")[2];
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
if (["quantity", "unit_amount", "discount_percentage", "tax_codes"].includes(field)) {
2025-10-12 19:09:56 +00:00
console.log("2.1. recalculo items!");
2025-10-12 18:36:33 +00:00
const item = items[index] as InvoiceItemFormData;
const prevTotals = itemCache.current.get(index);
const newTotals = calculateItemTotals(item);
2025-10-11 16:44:38 +00:00
2025-10-12 19:09:56 +00:00
console.log(prevTotals, newTotals);
2025-10-12 18:36:33 +00:00
// Si no hay cambios en los totales, no tocamos nada
const itemHasChanges =
2025-10-12 19:09:56 +00:00
prevTotals || JSON.stringify(prevTotals) !== JSON.stringify(newTotals);
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
if (!itemHasChanges) {
2025-10-12 19:09:56 +00:00
console.log("No hay cambios, me voy!!!");
2025-10-12 18:36:33 +00:00
return;
2025-10-11 16:44:38 +00:00
}
2025-10-12 18:36:33 +00:00
// 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;
2025-10-11 16:44:38 +00:00
2025-10-12 18:36:33 +00:00
console.log(invoiceTotals);
// Cabecera
setInvoiceTotals(form, invoiceTotals);
// 3) valida una vez (opcional)
2025-10-12 19:09:56 +00:00
trigger([
2025-10-12 18:36:33 +00:00
"items",
"subtotal_amount",
"discount_amount",
"taxable_amount",
"taxes_amount",
"total_amount",
]);
2025-10-11 16:44:38 +00:00
}
}
});
return () => subscription.unsubscribe();
}, [
watch,
2025-10-12 19:09:56 +00:00
trigger,
2025-10-11 16:44:38 +00:00
setValue,
getValues,
2025-10-12 19:09:56 +00:00
isDirty,
2025-10-12 18:36:33 +00:00
isLoading,
isSubmitting,
2025-10-12 19:09:56 +00:00
itemCache,
setInvoiceItemTotals,
setInvoiceTotals,
2025-10-11 16:44:38 +00:00
calculateItemTotals,
calculateInvoiceTotals,
]);
}
2025-10-12 18:36:33 +00:00
// Ayudante para rellenar los importes de una línea
function setInvoiceItemTotals(
form: UseFormReturn<InvoiceFormData>,
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<InvoiceFormData>,
invoiceTotals: ReturnType<typeof calculateInvoiceHeaderAmounts>
) {
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,
});
}