Facturas de cliente

This commit is contained in:
David Arranz 2025-10-12 21:09:56 +02:00
parent ef8a20d296
commit 27a5e30d37
5 changed files with 33 additions and 18 deletions

View File

@ -39,7 +39,7 @@ export function AmountInputField<T extends FieldValues>({
<FormControl> <FormControl>
<AmountInput <AmountInput
id={inputId} id={inputId}
value={field.value} value={field.value ?? ""}
onChange={field.onChange} onChange={field.onChange}
{...inputProps} {...inputProps}
/> />

View File

@ -5,8 +5,8 @@ import { InputEmptyMode, InputReadOnlyMode } from './quantity-input';
export type AmountInputProps = { export type AmountInputProps = {
value: number | "" | string; // "" → no mostrar nada; string puede venir con separadores value: number | string; // "" → no mostrar nada; string puede venir con separadores
onChange: (next: number | "") => void; onChange: (next: number | string) => void;
readOnly?: boolean; readOnly?: boolean;
readOnlyMode?: InputReadOnlyMode; // default "textlike-input" readOnlyMode?: InputReadOnlyMode; // default "textlike-input"
id?: string; id?: string;

View File

@ -28,25 +28,27 @@ export function QuantityInputField<TFormValues extends FieldValues>({
<FormField <FormField
control={control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => {
<FormItem> const { value, onChange } = field;
console.log(value);
return <FormItem>
{label ? ( {label ? (
<FormLabel htmlFor={inputId}> <FormLabel htmlFor={inputId}>
{label} {required ? <span aria-hidden="true">*</span> : null} {label} {required ? <span aria-hidden='true'>*</span> : null}
</FormLabel> </FormLabel>
) : null} ) : null}
<FormControl> <FormControl>
<QuantityInput <QuantityInput
id={inputId} id={inputId}
value={field.value} value={value}
onChange={field.onChange} onChange={onChange}
{...inputProps} {...inputProps}
/> />
</FormControl> </FormControl>
{description ? <FormDescription>{description}</FormDescription> : null} {description ? <FormDescription>{description}</FormDescription> : null}
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} }}
/> />
); );
} }

View File

@ -110,9 +110,12 @@ export function useInvoiceAutoRecalc(
// Suscripción reactiva a cambios del formulario // Suscripción reactiva a cambios del formulario
React.useEffect(() => { React.useEffect(() => {
console.log("recalculo algo?");
if (!isDirty || isLoading || isSubmitting) return; if (!isDirty || isLoading || isSubmitting) return;
const subscription = watch(async (formData, { name, type }) => { const subscription = watch((formData, { name, type }) => {
console.log(name, type);
const items = (formData?.items || []) as InvoiceItemFormData[]; const items = (formData?.items || []) as InvoiceItemFormData[];
if (items.length === 0) return; if (items.length === 0) return;
@ -126,7 +129,7 @@ export function useInvoiceAutoRecalc(
setInvoiceTotals(form, invoiceTotals); setInvoiceTotals(form, invoiceTotals);
// 3) valida una vez (opcional) // 3) valida una vez (opcional)
await trigger([ trigger([
"subtotal_amount", "subtotal_amount",
"discount_amount", "discount_amount",
"taxable_amount", "taxable_amount",
@ -137,19 +140,25 @@ export function useInvoiceAutoRecalc(
// 2. Cambio puntual de una línea // 2. Cambio puntual de una línea
if (name?.startsWith("items.") && type === "change") { if (name?.startsWith("items.") && type === "change") {
console.log("2. items!");
const index = Number(name.split(".")[1]); const index = Number(name.split(".")[1]);
const field = name.split(".")[2]; const field = name.split(".")[2];
if (["quantity", "unit_amount", "discount_percentage", "tax_codes"].includes(field)) { if (["quantity", "unit_amount", "discount_percentage", "tax_codes"].includes(field)) {
console.log("2.1. recalculo items!");
const item = items[index] as InvoiceItemFormData; const item = items[index] as InvoiceItemFormData;
const prevTotals = itemCache.current.get(index); const prevTotals = itemCache.current.get(index);
const newTotals = calculateItemTotals(item); const newTotals = calculateItemTotals(item);
console.log(prevTotals, newTotals);
// Si no hay cambios en los totales, no tocamos nada // Si no hay cambios en los totales, no tocamos nada
const itemHasChanges = const itemHasChanges =
prevTotals && JSON.stringify(prevTotals) !== JSON.stringify(newTotals); prevTotals || JSON.stringify(prevTotals) !== JSON.stringify(newTotals);
if (!itemHasChanges) { if (!itemHasChanges) {
console.log("No hay cambios, me voy!!!");
return; return;
} }
@ -173,7 +182,7 @@ export function useInvoiceAutoRecalc(
setInvoiceTotals(form, invoiceTotals); setInvoiceTotals(form, invoiceTotals);
// 3) valida una vez (opcional) // 3) valida una vez (opcional)
await trigger([ trigger([
"items", "items",
"subtotal_amount", "subtotal_amount",
"discount_amount", "discount_amount",
@ -188,10 +197,15 @@ export function useInvoiceAutoRecalc(
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();
}, [ }, [
watch, watch,
trigger,
setValue, setValue,
getValues, getValues,
isDirty,
isLoading, isLoading,
isSubmitting, isSubmitting,
itemCache,
setInvoiceItemTotals,
setInvoiceTotals,
calculateItemTotals, calculateItemTotals,
calculateInvoiceTotals, calculateInvoiceTotals,
]); ]);

View File

@ -1,15 +1,14 @@
import { NumericStringSchema } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
export const InvoiceItemFormSchema = z.object({ export const InvoiceItemFormSchema = z.object({
is_non_valued: z.boolean(), is_non_valued: z.boolean(),
description: z.string().max(2000).optional().default(""), description: z.string().max(2000).optional().default(""),
quantity: NumericStringSchema.optional(), quantity: z.any(), //NumericStringSchema.optional(),
unit_amount: NumericStringSchema.optional(), unit_amount: z.any(), //NumericStringSchema.optional(),
subtotal_amount: z.number(), subtotal_amount: z.any(), //z.number(),
discount_percentage: NumericStringSchema.optional(), discount_percentage: z.any(), //NumericStringSchema.optional(),
discount_amount: z.number(), discount_amount: z.number(),
taxable_amount: z.number(), taxable_amount: z.number(),