From 78cc422d9a91149d750850d02794edc4a9bd0339 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 12 Oct 2025 12:43:06 +0200 Subject: [PATCH] Facturas de cliente --- biome.json | 4 +- modules/core/src/common/helpers/index.ts | 5 +- .../src/common/helpers/money-dto-helper.ts | 145 ++++++++++++++++ .../core/src/common/helpers/money-helper.ts | 56 ++++++ .../core/src/common/helpers/money-utils.ts | 66 -------- .../common/helpers/percentage-dto-helpers.ts | 60 +++++++ .../common/helpers/quantity-dto-helpers.ts | 60 +++++++ .../web/hooks/use-hook-form/use-hook-form.ts | 13 +- modules/core/src/web/hooks/use-money-dto.ts | 101 +++++++++++ modules/core/src/web/hooks/use-money.ts | 32 ++-- modules/core/src/web/hooks/use-percentage.ts | 1 + .../domain/customer-invoice.full.presenter.ts | 3 - .../api/domain/aggregates/customer-invoice.ts | 7 +- ...date-customer-invoice-by-id.request.dto.ts | 15 ++ ...get-customer-invoice-by-id.response.dto.ts | 2 +- .../src/common/locales/en.json | 5 + .../src/common/locales/es.json | 6 + .../components/customer-invoices-layout.tsx | 3 +- .../customer-invoices-list-grid.tsx | 16 +- .../editor/customer-invoice-edit-form.tsx | 52 +++--- .../editor/invoice-basic-info-fields.tsx | 108 ++++++------ .../editor/invoice-items-editor.tsx | 8 +- .../components/editor/invoice-tax-notes.tsx | 32 ++++ .../components/editor/invoice-tax-summary.tsx | 77 +++------ .../web/components/editor/invoice-totals.tsx | 64 +++---- .../components/editor/items/blocks-view.tsx | 4 +- .../web/components/editor/items/item-row.tsx | 6 +- .../components/editor/items/items-editor.tsx | 6 +- .../components/editor/items/table-view.tsx | 16 +- .../editor/recipient/invoice-recipient.tsx | 5 +- .../src/web/context/invoice-context.tsx | 6 +- .../src/web/customer-invoice-routes.tsx | 2 +- .../calcs/use-calc-invoice-items-totals.ts | 4 +- .../hooks/calcs/use-calc-invoice-totals.ts | 6 +- .../hooks/calcs/use-invoice-auto-recalc.ts | 16 +- .../customer-invoices/src/web/hooks/index.ts | 2 +- .../use-create-customer-invoice-mutation.ts | 4 +- ...-invoice-query.ts => use-invoice-query.ts} | 2 +- .../use-update-customer-invoice-mutation.ts | 12 +- .../src/web/pages/update/index.ts | 2 +- ...pdate-page.tsx => invoice-update-page.tsx} | 31 ++-- .../schemas/customer-invoices.form.schema.ts | 160 ------------------ .../src/web/schemas/index.ts | 3 +- .../src/web/schemas/invoice-dto.adapter.ts | 96 +++++++++++ .../src/web/schemas/invoice.form.schema.ts | 124 ++++++++++++++ .../web/components/client-selector-modal.tsx | 4 +- .../create-customer-form-dialog.tsx | 2 +- .../customer-search-dialog.tsx | 2 +- .../src/web/pages/view/customer-view-page.tsx | 8 +- ...get-verifactu-record-by-id.response.dto.ts | 2 +- .../src/components/form/TextAreaField.tsx | 10 +- .../src/components/layout/data-table.tsx | 4 +- packages/rdx-utils/src/helpers/collection.ts | 12 ++ 53 files changed, 990 insertions(+), 502 deletions(-) create mode 100644 modules/core/src/common/helpers/money-dto-helper.ts create mode 100644 modules/core/src/common/helpers/money-helper.ts delete mode 100644 modules/core/src/common/helpers/money-utils.ts create mode 100644 modules/core/src/common/helpers/percentage-dto-helpers.ts create mode 100644 modules/core/src/common/helpers/quantity-dto-helpers.ts create mode 100644 modules/core/src/web/hooks/use-money-dto.ts create mode 100644 modules/customer-invoices/src/web/components/editor/invoice-tax-notes.tsx rename modules/customer-invoices/src/web/hooks/{use-customer-invoice-query.ts => use-invoice-query.ts} (93%) rename modules/customer-invoices/src/web/pages/update/{customer-invoices-update-page.tsx => invoice-update-page.tsx} (85%) delete mode 100644 modules/customer-invoices/src/web/schemas/customer-invoices.form.schema.ts create mode 100644 modules/customer-invoices/src/web/schemas/invoice-dto.adapter.ts create mode 100644 modules/customer-invoices/src/web/schemas/invoice.form.schema.ts diff --git a/biome.json b/biome.json index be879bd7..c659f6a8 100644 --- a/biome.json +++ b/biome.json @@ -35,7 +35,9 @@ }, "style": { "useImportType": "off", - "noNonNullAssertion": "info" + "noInferrableTypes": "off", + "noNonNullAssertion": "info", + "noUselessElse": "off" }, "a11y": { "useSemanticElements": "info" diff --git a/modules/core/src/common/helpers/index.ts b/modules/core/src/common/helpers/index.ts index f73ab18f..55b76df1 100644 --- a/modules/core/src/common/helpers/index.ts +++ b/modules/core/src/common/helpers/index.ts @@ -1,2 +1,5 @@ export * from "./dto-compare-helper"; -export * from "./money-utils"; +export * from "./money-dto-helper"; +export * from "./money-helper"; +export * from "./percentage-dto-helpers"; +export * from "./quantity-dto-helpers"; diff --git a/modules/core/src/common/helpers/money-dto-helper.ts b/modules/core/src/common/helpers/money-dto-helper.ts new file mode 100644 index 00000000..1d4dddc2 --- /dev/null +++ b/modules/core/src/common/helpers/money-dto-helper.ts @@ -0,0 +1,145 @@ +import type { MoneyDTO } from "@erp/core/common"; +import Dinero from "dinero.js"; + +type DineroPlain = { amount: number; precision: number; currency: string }; + +const isEmptyMoneyDTO = (dto?: MoneyDTO | null) => + !dto || dto.value?.trim?.() === "" || dto.scale?.trim?.() === ""; + +/** + * Convierte un MoneyDTO a número con precisión (sin moneda). + */ +const toNumber = (dto?: MoneyDTO | null, fallbackScale = 2): number => { + if (isEmptyMoneyDTO(dto)) { + return 0; + } + const scale = Number(dto!.scale || fallbackScale); + return Number(dto!.value || 0) / 10 ** scale; +}; + +/** + * Convierte un MoneyDTO a cadena numérica con precisión (sin moneda). + * Puede devolver cadena vacía + */ +const toNumericString = (dto?: MoneyDTO | null, fallbackScale = 2): string => { + if (isEmptyMoneyDTO(dto)) { + return ""; + } + return toNumber(dto, fallbackScale).toString(); +}; + +/** + * Convierte número a MoneyDTO. + */ +const fromNumber = (amount: number, currency: string = "EUR", scale = 2): MoneyDTO => { + return { + value: String(Math.round(amount * 10 ** scale)), + scale: String(scale), + currency_code: currency, + }; +}; + +/** + * Convierte cadena numérica a MoneyDTO. + */ +const fromNumericString = (amount?: string, currency: string = "EUR", scale = 2): MoneyDTO => { + if (!amount || amount?.trim?.() === "") { + return { + value: "", + scale: "", + currency_code: currency, + }; + } + return { + value: String(Math.round(Number(amount) * 10 ** scale)), + scale: String(scale), + currency_code: currency, + }; +}; + +/** + * Normaliza un MoneyDTO incompleto o malformado. + */ +const normalizeDTO = (dto: MoneyDTO, fallbackCurrency = "EUR"): Required => { + const v = /^-?\d+$/.test(dto?.value ?? "") ? dto.value : "0"; + const s = /^\d+$/.test(dto?.scale ?? "") ? dto.scale : "2"; + const c = (dto?.currency_code || fallbackCurrency) as string; + return { value: v, scale: s, currency_code: c }; +}; + +/** + * Formatea un MoneyDTO según locale. + */ +const formatDTO = (dto: MoneyDTO, locale: string = "es-ES"): string => { + const { value, scale, currency_code } = normalizeDTO(dto); + const num = Number(value) / 10 ** Number(scale); + return new Intl.NumberFormat(locale, { style: "currency", currency: currency_code }).format(num); +}; + +export const MoneyDTOHelper = { + isEmpty: isEmptyMoneyDTO, + toNumber, + toNumericString, + fromNumber, + fromNumericString, + formatDTO, +}; + +/** + * Convierte un DTO a una instancia Dinero.js. + */ +function dineroFromDTO(dto: MoneyDTO, fallbackCurrency = "EUR"): Dinero.Dinero { + const n = normalizeDTO(dto, fallbackCurrency); + return Dinero({ + amount: Number.parseInt(n.value, 10), + precision: Number.parseInt(n.scale, 10), + currency: n.currency_code as string, + }); +} + +/** + * Convierte una instancia Dinero a un DTO. + */ +function dtoFromDinero(d: Dinero.Dinero): MoneyDTO { + const { amount, precision, currency } = d.toObject() as DineroPlain; + return { + value: amount.toString(), + scale: precision.toString(), + currency_code: currency, + }; +} + +/** + * Suma una lista de MoneyDTO. + */ +function sumDTO(list: MoneyDTO[], fallbackCurrency = "EUR"): MoneyDTO { + if (list.length === 0) return { value: "0", scale: "2", currency_code: fallbackCurrency }; + const sum = list.map((x) => dineroFromDTO(x, fallbackCurrency)).reduce((a, b) => a.add(b)); + return dtoFromDinero(sum); +} + +/** + * Multiplica un MoneyDTO por un número. + */ +function multiplyDTO( + dto: MoneyDTO, + multiplier: number, + rounding: Dinero.RoundingMode = "HALF_EVEN", + fallbackCurrency = "EUR" +): MoneyDTO { + const d = dineroFromDTO(dto, fallbackCurrency).multiply(multiplier, rounding); + return dtoFromDinero(d); +} + +/** + * Calcula un porcentaje de un MoneyDTO. + */ +function percentageDTO( + dto: MoneyDTO, + percent: number, + rounding: Dinero.RoundingMode = "HALF_EVEN", + fallbackCurrency = "EUR" +): MoneyDTO { + const d = dineroFromDTO(dto, fallbackCurrency).percentage(percent, rounding); + return dtoFromDinero(d); +} diff --git a/modules/core/src/common/helpers/money-helper.ts b/modules/core/src/common/helpers/money-helper.ts new file mode 100644 index 00000000..e9ccfa87 --- /dev/null +++ b/modules/core/src/common/helpers/money-helper.ts @@ -0,0 +1,56 @@ +/** + * Funciones para manipular valores monetarios numéricos. + */ + +/** + * Elimina símbolos de moneda y caracteres no numéricos. + * @param s Texto de entrada, e.g. "€ 1.234,56" + * @returns Solo dígitos, signos y separadores. + * @example stripCurrencySymbols("€ -1.234,56") // "-1.234,56" + */ +export const stripCurrencySymbols = (s: string): string => + s + .replace(/[^\d.,\-]/g, "") + .replace(/\s+/g, " ") + .trim(); + +/** + * Parsea un número localizado a float (soporta "," y "."). + * @param raw Texto con número localizado. + * @returns número o null si no se puede parsear. + * @example parseLocaleNumber("1.234,56") // 1234.56 + */ +export const parseLocaleNumber = (raw: string): number | null => { + if (!raw) return null; + const s = stripCurrencySymbols(raw); + if (!s) return null; + const lastComma = s.lastIndexOf(","); + const lastDot = s.lastIndexOf("."); + let normalized = s; + if (lastComma > -1 && lastDot > -1) { + if (lastComma > lastDot) normalized = s.replace(/\./g, "").replace(",", "."); + else normalized = s.replace(/,/g, ""); + } else if (lastComma > -1) normalized = s.replace(/\s/g, "").replace(",", "."); + else normalized = s.replace(/\s/g, ""); + const n = Number(normalized); + return Number.isFinite(n) ? n : null; +}; + +/** + * Redondea a una escala decimal determinada. + * @param n número base + * @param scale cantidad de decimales + * @returns número redondeado + * @example roundToScale(1.2345, 2) // 1.23 + */ +export const roundToScale = (n: number, scale = 2): number => { + const f = 10 ** scale; + return Math.round(n * f) / f; +}; + +/** + * Suma o resta con step (para inputs numéricos). + * @example stepNumber(1.2, 0.1) // 1.3 + */ +export const stepNumber = (base: number, step = 0.01, scale = 2): number => + roundToScale(base + step, scale); diff --git a/modules/core/src/common/helpers/money-utils.ts b/modules/core/src/common/helpers/money-utils.ts deleted file mode 100644 index 0fa55ce2..00000000 --- a/modules/core/src/common/helpers/money-utils.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { MoneyDTO } from "@erp/core/common"; -import Dinero, { Currency } from "dinero.js"; - -// Tipo compatible con API => MoneyDTO - -// Snapshot mínimo de toObject() en v1 -type DineroPlain = { amount: number; precision: number; currency: string }; - -// --- Helpers --- - -function normalizeDTO(dto: MoneyDTO, fallbackCurrency: Currency = "EUR"): Required { - const v = /^-?\d+$/.test(dto?.value ?? "") ? dto.value : "0"; - const s = /^\d+$/.test(dto?.scale ?? "") ? dto.scale : "2"; - const c = (dto?.currency_code || fallbackCurrency) as string; - return { value: v, scale: s, currency_code: c }; -} - -export function dineroFromDTO(dto: MoneyDTO, fallbackCurrency: Currency = "EUR"): Dinero.Dinero { - const n = normalizeDTO(dto, fallbackCurrency); - return Dinero({ - amount: Number.parseInt(n.value, 10), - precision: Number.parseInt(n.scale, 10), - currency: n.currency_code as Currency, - }); -} - -export function dtoFromDinero(d: Dinero.Dinero): MoneyDTO { - const { amount, precision, currency } = d.toObject() as DineroPlain; - return { - value: amount.toString(), - scale: precision.toString(), - currency_code: currency, - }; -} - -export function sumDTO(list: MoneyDTO[], fallbackCurrency: Currency = "EUR"): MoneyDTO { - if (list.length === 0) return { value: "0", scale: "2", currency_code: fallbackCurrency }; - const sum = list.map((x) => dineroFromDTO(x, fallbackCurrency)).reduce((a, b) => a.add(b)); - return dtoFromDinero(sum); -} - -export function multiplyDTO( - dto: MoneyDTO, - multiplier: number, - rounding: Dinero.RoundingMode = "HALF_EVEN", - fallbackCurrency: Currency = "EUR" -): MoneyDTO { - const d = dineroFromDTO(dto, fallbackCurrency).multiply(multiplier, rounding); - return dtoFromDinero(d); -} - -export function percentageDTO( - dto: MoneyDTO, - percent: number, // 25 = 25% - rounding: Dinero.RoundingMode = "HALF_EVEN", - fallbackCurrency: Currency = "EUR" -): MoneyDTO { - const d = dineroFromDTO(dto, fallbackCurrency).percentage(percent, rounding); - return dtoFromDinero(d); -} - -export function formatDTO(dto: MoneyDTO, locale = "es-ES"): string { - const { value, scale, currency_code } = normalizeDTO(dto); - const num = Number(value) / 10 ** Number(scale); // solo presentación - return new Intl.NumberFormat(locale, { style: "currency", currency: currency_code }).format(num); -} diff --git a/modules/core/src/common/helpers/percentage-dto-helpers.ts b/modules/core/src/common/helpers/percentage-dto-helpers.ts new file mode 100644 index 00000000..0a499633 --- /dev/null +++ b/modules/core/src/common/helpers/percentage-dto-helpers.ts @@ -0,0 +1,60 @@ +import { PercentageDTO } from "../dto"; + +const isEmptyPercentageDTO = (dto?: PercentageDTO | null) => + !dto || dto.value?.trim?.() === "" || dto.scale?.trim?.() === ""; + +/** + * Convierte un QuantityDTO a número con precisión. + */ +const toNumber = (dto?: PercentageDTO | null, fallbackScale = 2): number => { + if (isEmptyPercentageDTO(dto)) { + return 0; + } + const scale = Number(dto!.scale || fallbackScale); + return Number(dto!.value || 0) / 10 ** scale; +}; + +/** + * Convierte un QuantityDTO a cadena numérica con precisión. + * Puede devolver cadena vacía + */ +const toNumericString = (dto?: PercentageDTO | null, fallbackScale = 2): string => { + if (isEmptyPercentageDTO(dto)) { + return ""; + } + return toNumber(dto, fallbackScale).toString(); +}; + +/** + * Convierte número a QuantityDTO. + */ +const fromNumber = (amount: number, scale = 2): PercentageDTO => { + return { + value: String(Math.round(amount * 10 ** scale)), + scale: String(scale), + }; +}; + +/** + * Convierte cadena numérica a QuantityDTO. + */ +const fromNumericString = (amount?: string, scale = 2): PercentageDTO => { + if (!amount || amount?.trim?.() === "") { + return { + value: "", + scale: "", + }; + } + return { + value: String(Math.round(Number(amount) * 10 ** scale)), + scale: String(scale), + }; +}; + +export const PercentageDTOHelper = { + isEmpty: isEmptyPercentageDTO, + toNumber, + toNumericString, + fromNumber, + fromNumericString, +}; diff --git a/modules/core/src/common/helpers/quantity-dto-helpers.ts b/modules/core/src/common/helpers/quantity-dto-helpers.ts new file mode 100644 index 00000000..9577ef23 --- /dev/null +++ b/modules/core/src/common/helpers/quantity-dto-helpers.ts @@ -0,0 +1,60 @@ +import { QuantityDTO } from "../dto"; + +const isEmptyQuantityDTO = (dto?: QuantityDTO | null) => + !dto || dto.value?.trim?.() === "" || dto.scale?.trim?.() === ""; + +/** + * Convierte un QuantityDTO a número con precisión. + */ +const toNumber = (dto?: QuantityDTO | null, fallbackScale = 2): number => { + if (isEmptyQuantityDTO(dto)) { + return 0; + } + const scale = Number(dto!.scale || fallbackScale); + return Number(dto!.value || 0) / 10 ** scale; +}; + +/** + * Convierte un QuantityDTO a cadena numérica con precisión. + * Puede devolver cadena vacía + */ +const toNumericString = (dto?: QuantityDTO | null, fallbackScale = 2): string => { + if (isEmptyQuantityDTO(dto)) { + return ""; + } + return toNumber(dto, fallbackScale).toString(); +}; + +/** + * Convierte número a QuantityDTO. + */ +const fromNumber = (amount: number, scale = 2): QuantityDTO => { + return { + value: String(Math.round(amount * 10 ** scale)), + scale: String(scale), + }; +}; + +/** + * Convierte cadena numérica a QuantityDTO. + */ +const fromNumericString = (amount?: string, scale = 2): QuantityDTO => { + if (!amount || amount?.trim?.() === "") { + return { + value: "", + scale: "", + }; + } + return { + value: String(Math.round(Number(amount) * 10 ** scale)), + scale: String(scale), + }; +}; + +export const QuantityDTOHelper = { + isEmpty: isEmptyQuantityDTO, + toNumber, + toNumericString, + fromNumber, + fromNumericString, +}; diff --git a/modules/core/src/web/hooks/use-hook-form/use-hook-form.ts b/modules/core/src/web/hooks/use-hook-form/use-hook-form.ts index 1fd33a75..dab1a878 100644 --- a/modules/core/src/web/hooks/use-hook-form/use-hook-form.ts +++ b/modules/core/src/web/hooks/use-hook-form/use-hook-form.ts @@ -8,13 +8,15 @@ type UseHookFormProps TContext > & { resolverSchema: z4.$ZodType; - initialValues: UseFormProps["defaultValues"]; + defaultValues: UseFormProps["defaultValues"]; + values: UseFormProps["values"]; onDirtyChange?: (isDirty: boolean) => void; }; export function useHookForm({ resolverSchema, - initialValues, + defaultValues, + values, disabled, onDirtyChange, ...rest @@ -22,7 +24,8 @@ export function useHookForm({ ...rest, resolver: zodResolver(resolverSchema), - defaultValues: initialValues, + defaultValues, + values, disabled, }); @@ -36,12 +39,12 @@ export function useHookForm { const applyReset = async () => { - const values = typeof initialValues === "function" ? await initialValues() : initialValues; + const values = typeof defaultValues === "function" ? await defaultValues() : defaultValues; form.reset(values); }; applyReset(); - }, [initialValues, form]); + }, [defaultValues, form]); return form; } diff --git a/modules/core/src/web/hooks/use-money-dto.ts b/modules/core/src/web/hooks/use-money-dto.ts new file mode 100644 index 00000000..2fcb9f72 --- /dev/null +++ b/modules/core/src/web/hooks/use-money-dto.ts @@ -0,0 +1,101 @@ +import type { MoneyDTO } from "@erp/core/common"; +import type { Currency } from "dinero.js"; +import * as React from "react"; +import { useTranslation } from "../i18n"; + +/** + * Hook para manipular valores MoneyDTO con operaciones, + * formato, parseo y conversión seguras. + */ +export function useMoneyDTO(overrides?: { + locale?: string; + fallbackCurrency?: Currency; + defaultScale?: number; +}) { + const { i18n } = useTranslation(); + const locale = overrides?.locale || i18n.language || "es-ES"; + const fallbackCurrency: Currency = overrides?.fallbackCurrency ?? "EUR"; + const defaultScale = overrides?.defaultScale ?? 2; + + // Conversión + const toNumber = React.useCallback( + (dto?: MoneyDTO | null) => toNumberUnsafe(dto, defaultScale), + [defaultScale] + ); + + const fromNumber = React.useCallback( + (n: number, currency: Currency = fallbackCurrency, scale: number = defaultScale): MoneyDTO => + fromNumberUnsafe(n, currency, scale), + [fallbackCurrency, defaultScale] + ); + + // Operaciones + const add = React.useCallback( + (a: MoneyDTO, b: MoneyDTO): MoneyDTO => sumDTO([a, b], fallbackCurrency), + [fallbackCurrency] + ); + + const sub = React.useCallback( + (a: MoneyDTO, b: MoneyDTO): MoneyDTO => sumDTO([a, multiplyDTO(b, -1)], fallbackCurrency), + [fallbackCurrency] + ); + + const multiply = React.useCallback( + (dto: MoneyDTO, k: number, rounding: Dinero.RoundingMode = "HALF_EVEN") => + multiplyDTO(dto, k, rounding, fallbackCurrency), + [fallbackCurrency] + ); + + const percentage = React.useCallback( + (dto: MoneyDTO, p: number, rounding: Dinero.RoundingMode = "HALF_EVEN") => + percentageDTO(dto, p, rounding, fallbackCurrency), + [fallbackCurrency] + ); + + // Formatos + const formatCurrency = React.useCallback( + (dto: MoneyDTO, loc?: string) => formatDTO(dto, loc ?? locale), + [locale] + ); + + const parse = React.useCallback((text: string): number | null => parseLocaleNumber(text), []); + + // Estado + const isZero = React.useCallback((dto?: MoneyDTO | null) => toNumber(dto) === 0, [toNumber]); + + return React.useMemo( + () => ({ + toNumber, + fromNumber, + add, + sub, + multiply, + percentage, + formatCurrency, + parse, + isZero, + roundToScale, + stepNumber, + stripCurrencySymbols, + toDinero: (dto: MoneyDTO) => dineroFromDTO(dto, fallbackCurrency), + fromDinero: dtoFromDinero, + locale, + fallbackCurrency, + defaultScale, + }), + [ + toNumber, + fromNumber, + add, + sub, + multiply, + percentage, + formatCurrency, + parse, + isZero, + fallbackCurrency, + locale, + defaultScale, + ] + ); +} diff --git a/modules/core/src/web/hooks/use-money.ts b/modules/core/src/web/hooks/use-money.ts index c97e9733..c122b034 100644 --- a/modules/core/src/web/hooks/use-money.ts +++ b/modules/core/src/web/hooks/use-money.ts @@ -1,14 +1,6 @@ import type { MoneyDTO } from "@erp/core/common"; import type { Currency } from "dinero.js"; import * as React from "react"; -import { - dineroFromDTO, - dtoFromDinero, - formatDTO, - multiplyDTO, - percentageDTO, - sumDTO, -} from "../../common/helpers"; import { useTranslation } from "../i18n"; export type { Currency }; @@ -131,7 +123,7 @@ export function useMoney(overrides?: { [fallbackCurrency] ); - // Operaciones (dinero.js via helpers) + /* // Operaciones (dinero.js via helpers) const add = React.useCallback( (a: MoneyDTO, b: MoneyDTO): MoneyDTO => sumDTO([a, b], fallbackCurrency), [fallbackCurrency] @@ -149,7 +141,7 @@ export function useMoney(overrides?: { (dto: MoneyDTO, p: number, rounding: Dinero.RoundingMode = "HALF_EVEN") => percentageDTO(dto, p, rounding, fallbackCurrency), [fallbackCurrency] - ); + ); */ // Estado/Comparaciones const isZero = React.useCallback((dto?: MoneyDTO | null) => toNumber(dto) === 0, [toNumber]); @@ -183,10 +175,10 @@ export function useMoney(overrides?: { toApi, // Operaciones - add, - sub, - multiply, - percentage, + //add, + //sub, + //multiply, + //percentage, // Estado/ayudas isZero, @@ -202,8 +194,8 @@ export function useMoney(overrides?: { fallbackCurrency, defaultScale, // Factory Dinero si se necesita en algún punto de bajo nivel: - toDinero: (dto: MoneyDTO) => dineroFromDTO(dto, fallbackCurrency), - fromDinero: dtoFromDinero, + //toDinero: (dto: MoneyDTO) => dineroFromDTO(dto, fallbackCurrency), + //fromDinero: dtoFromDinero, }), [ toNumber, @@ -214,10 +206,10 @@ export function useMoney(overrides?: { parse, fromApi, toApi, - add, - sub, - multiply, - percentage, + //add, + //sub, + //multiply, + //percentage, isZero, sameCurrency, stepNumber, diff --git a/modules/core/src/web/hooks/use-percentage.ts b/modules/core/src/web/hooks/use-percentage.ts index b793ef40..f90fb976 100644 --- a/modules/core/src/web/hooks/use-percentage.ts +++ b/modules/core/src/web/hooks/use-percentage.ts @@ -21,5 +21,6 @@ export function usePercentage() { maximumFractionDigits: 2, })}%`; + // biome-ignore lint/correctness/useExhaustiveDependencies: return useMemo(() => ({ toNumber, fromNumber, format }), []); } diff --git a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts index 3bc1498a..2154d648 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts @@ -36,7 +36,6 @@ export class CustomerInvoiceFullPresenter extends Presenter< ); const invoiceTaxes = invoice.getTaxes().map((taxItem) => { - console.log(taxItem); return { tax_code: taxItem.tax.code, taxable_amount: taxItem.taxableAmount.toObjectString(), @@ -44,8 +43,6 @@ export class CustomerInvoiceFullPresenter extends Presenter< }; }); - console.log(invoiceTaxes); - return { id: invoice.id.toString(), company_id: invoice.companyId.toString(), diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts index 6a6b573b..f6c8b610 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -225,7 +225,12 @@ export class CustomerInvoice const itemTaxes = this.items.getTaxesAmountByTaxes(); for (const taxItem of itemTaxes) { - amount = amount.add(taxItem.taxesAmount); + amount = amount.add( + InvoiceAmount.create({ + value: taxItem.taxesAmount.convertScale(2).value, + currency_code: this.currencyCode.code, + }).data + ); } return amount; } diff --git a/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts index c07cc5e8..0f363b2d 100644 --- a/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts @@ -1,3 +1,4 @@ +import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core"; import { z } from "zod/v4"; export const UpdateCustomerInvoiceByIdParamsRequestSchema = z.object({ @@ -18,6 +19,20 @@ export const UpdateCustomerInvoiceByIdRequestSchema = z.object({ language_code: z.string().optional(), currency_code: z.string().optional(), + + items: z.array( + z.object({ + is_non_valued: z.string().optional(), + + description: z.string().optional(), + quantity: QuantitySchema.optional(), + unit_amount: MoneySchema.optional(), + + discount_percentage: PercentageSchema.optional(), + + tax_codes: z.array(z.string()).default([]), + }) + ), }); export type UpdateCustomerInvoiceByIdRequestDTO = Partial< diff --git a/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts index c4d25f1f..7ddb5cff 100644 --- a/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts @@ -57,7 +57,7 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({ items: z.array( z.object({ id: z.uuid(), - isNonValued: z.string(), + is_non_valued: z.string(), position: z.string(), description: z.string(), quantity: QuantitySchema, diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index 70373980..58721cea 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -114,6 +114,11 @@ "placeholder": "Select a date", "description": "Invoice operation date" }, + "reference": { + "label": "Reference", + "placeholder": "Reference of the invoice", + "description": "Reference of the invoice" + }, "description": { "label": "Description", "placeholder": "Description of the invoice", diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index bdaade63..5203e7e2 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -106,6 +106,12 @@ "placeholder": "Selecciona una fecha", "description": "Fecha de la operación de la factura" }, + "reference": { + "label": "Referencia", + "placeholder": "Referencia de la factura", + "description": "Referencia de la factura" + }, + "description": { "label": "Descripción", "placeholder": "Descripción de la factura", diff --git a/modules/customer-invoices/src/web/components/customer-invoices-layout.tsx b/modules/customer-invoices/src/web/components/customer-invoices-layout.tsx index 5fa65fad..fddb9644 100644 --- a/modules/customer-invoices/src/web/components/customer-invoices-layout.tsx +++ b/modules/customer-invoices/src/web/components/customer-invoices-layout.tsx @@ -1,6 +1,5 @@ import { PropsWithChildren } from "react"; -import { InvoiceProvider } from "../context"; export const CustomerInvoicesLayout = ({ children }: PropsWithChildren) => { - return {children}; + return
{children}
; }; diff --git a/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx b/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx index 93c939b3..8ed5ebd9 100644 --- a/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx +++ b/modules/customer-invoices/src/web/components/customer-invoices-list-grid.tsx @@ -10,9 +10,7 @@ import { import { useCallback, useMemo, useState } from "react"; -import { MoneyDTO } from "@erp/core"; import { formatDate } from "@erp/core/client"; -import { useMoney } from '@erp/core/hooks'; import { ErrorOverlay } from "@repo/rdx-ui/components"; import { Button } from "@repo/shadcn-ui/components"; import { AgGridReact } from "ag-grid-react"; @@ -26,7 +24,7 @@ import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge"; export const CustomerInvoicesListGrid = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const { formatCurrency } = useMoney(); + //const { formatCurrency } = useMoney(); const { data: invoices, @@ -96,10 +94,10 @@ export const CustomerInvoicesListGrid = () => { field: "taxable_amount", headerName: t("pages.list.grid_columns.taxable_amount"), type: "rightAligned", - valueFormatter: (params: ValueFormatterParams) => { + /*valueFormatter: (params: ValueFormatterParams) => { const raw: MoneyDTO | null = params.value; return raw ? formatCurrency(raw) : "—"; - }, + },*/ cellClass: "tabular-nums", minWidth: 130, }, @@ -107,10 +105,10 @@ export const CustomerInvoicesListGrid = () => { field: "taxes_amount", headerName: t("pages.list.grid_columns.taxes_amount"), type: "rightAligned", - valueFormatter: (params: ValueFormatterParams) => { + /*valueFormatter: (params: ValueFormatterParams) => { const raw: MoneyDTO | null = params.value; return raw ? formatCurrency(raw) : "—"; - }, + },*/ cellClass: "tabular-nums", minWidth: 130, }, @@ -118,10 +116,10 @@ export const CustomerInvoicesListGrid = () => { field: "total_amount", headerName: t("pages.list.grid_columns.total_amount"), type: "rightAligned", - valueFormatter: (params: ValueFormatterParams) => { + /*valueFormatter: (params: ValueFormatterParams) => { const raw: MoneyDTO | null = params.value; return raw ? formatCurrency(raw) : "—"; - }, + },*/ cellClass: "tabular-nums font-semibold", minWidth: 140, }, diff --git a/modules/customer-invoices/src/web/components/editor/customer-invoice-edit-form.tsx b/modules/customer-invoices/src/web/components/editor/customer-invoice-edit-form.tsx index 65c57758..6718a466 100644 --- a/modules/customer-invoices/src/web/components/editor/customer-invoice-edit-form.tsx +++ b/modules/customer-invoices/src/web/components/editor/customer-invoice-edit-form.tsx @@ -1,18 +1,19 @@ import { FieldErrors, useFormContext } from "react-hook-form"; import { FormDebug } from "@erp/core/components"; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@repo/shadcn-ui/components'; import { useTranslation } from "../../i18n"; -import { CustomerInvoiceFormData } from "../../schemas"; +import { InvoiceFormData } from "../../schemas"; import { InvoiceBasicInfoFields } from "./invoice-basic-info-fields"; -import { InvoiceItems } from "./invoice-items-editor"; +import { InvoiceNotes } from './invoice-tax-notes'; import { InvoiceTaxSummary } from "./invoice-tax-summary"; import { InvoiceTotals } from "./invoice-totals"; import { InvoiceRecipient } from "./recipient"; interface CustomerInvoiceFormProps { formId: string; - onSubmit: (data: CustomerInvoiceFormData) => void; - onError: (errors: FieldErrors) => void; + onSubmit: (data: InvoiceFormData) => void; + onError: (errors: FieldErrors) => void; className: string; } @@ -23,7 +24,7 @@ export const CustomerInvoiceEditForm = ({ className, }: CustomerInvoiceFormProps) => { const { t } = useTranslation(); - const form = useFormContext(); + const form = useFormContext(); return (
@@ -31,24 +32,35 @@ export const CustomerInvoiceEditForm = ({
-
-
- -
+
+ + + -
- -
+
+ + + -
- -
+
+
-
- -
-
- +
+
+ {/* */} +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/modules/customer-invoices/src/web/components/editor/invoice-basic-info-fields.tsx b/modules/customer-invoices/src/web/components/editor/invoice-basic-info-fields.tsx index 3e9755cd..985fb448 100644 --- a/modules/customer-invoices/src/web/components/editor/invoice-basic-info-fields.tsx +++ b/modules/customer-invoices/src/web/components/editor/invoice-basic-info-fields.tsx @@ -5,50 +5,38 @@ import { FieldGroup, Fieldset, Legend, - TextAreaField, - TextField, + TextField } from "@repo/rdx-ui/components"; import { FileTextIcon } from "lucide-react"; -import { useFormContext, useWatch } from "react-hook-form"; +import { ComponentProps } from 'react'; +import { useFormContext } from "react-hook-form"; import { useTranslation } from "../../i18n"; -import { CustomerInvoiceFormData } from "../../schemas"; +import { InvoiceFormData } from "../../schemas"; -export const InvoiceBasicInfoFields = () => { +export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { control } = useFormContext(); - const status = useWatch({ - control, - name: "status", - defaultValue: "", - }); return ( -
+
- {t("form_groups.basic_into.title")} + {t("form_groups.basic_into.title")} {t("form_groups.basic_into.description")} - - - - - + + + + + { /> - + + + + + { description={t("form_fields.operation_date.description")} /> - - + + + + + + + +
); diff --git a/modules/customer-invoices/src/web/components/editor/invoice-items-editor.tsx b/modules/customer-invoices/src/web/components/editor/invoice-items-editor.tsx index d32ffa51..888f14e1 100644 --- a/modules/customer-invoices/src/web/components/editor/invoice-items-editor.tsx +++ b/modules/customer-invoices/src/web/components/editor/invoice-items-editor.tsx @@ -1,19 +1,21 @@ import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components"; import { Package } from "lucide-react"; +import { cn } from '@repo/shadcn-ui/lib/utils'; +import { ComponentProps } from 'react'; import { useTranslation } from '../../i18n'; import { ItemsEditor } from "./items"; -export const InvoiceItems = () => { +export const InvoiceItems = ({ className, ...props }: ComponentProps<"div">) => { const { t } = useTranslation(); return ( - +
- + {t('form_groups.items.title')}
diff --git a/modules/customer-invoices/src/web/components/editor/invoice-tax-notes.tsx b/modules/customer-invoices/src/web/components/editor/invoice-tax-notes.tsx new file mode 100644 index 00000000..4498704c --- /dev/null +++ b/modules/customer-invoices/src/web/components/editor/invoice-tax-notes.tsx @@ -0,0 +1,32 @@ +import { Description, FieldGroup, Fieldset, Legend, TextAreaField } from "@repo/rdx-ui/components"; +import { StickyNoteIcon } from "lucide-react"; +import { ComponentProps } from 'react'; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "../../i18n"; +import { InvoiceFormData } from "../../schemas"; + +export const InvoiceNotes = (props: ComponentProps<"fieldset">) => { + const { t } = useTranslation(); + const { control } = useFormContext(); + + return ( +
+ + {t("form_groups.basic_into.title")} + + + {t("form_groups.basic_into.description")} + + + +
+ ); +}; diff --git a/modules/customer-invoices/src/web/components/editor/invoice-tax-summary.tsx b/modules/customer-invoices/src/web/components/editor/invoice-tax-summary.tsx index c322c20c..c04a1dbb 100644 --- a/modules/customer-invoices/src/web/components/editor/invoice-tax-summary.tsx +++ b/modules/customer-invoices/src/web/components/editor/invoice-tax-summary.tsx @@ -1,13 +1,14 @@ import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components"; import { Badge } from "@repo/shadcn-ui/components"; import { ReceiptIcon } from "lucide-react"; +import { ComponentProps } from 'react'; import { useFormContext, useWatch } from "react-hook-form"; import { useTranslation } from "../../i18n"; -import { CustomerInvoiceFormData } from "../../schemas"; +import { InvoiceFormData } from "../../schemas"; -export const InvoiceTaxSummary = () => { +export const InvoiceTaxSummary = (props: ComponentProps<"fieldset">) => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { control, getValues } = useFormContext(); const taxes = useWatch({ control, @@ -15,79 +16,43 @@ export const InvoiceTaxSummary = () => { defaultValue: [], }); - const formatCurrency = (amount: { - value: string; - scale: string; - currency_code: string; - }) => { - const { currency_code, value, scale } = amount; + console.log(getValues()); + const formatCurrency = (amount: number) => { return new Intl.NumberFormat("es-ES", { style: "currency", - currency: currency_code, - minimumFractionDigits: Number(scale), - maximumFractionDigits: Number(scale), - compactDisplay: "short", - currencyDisplay: "symbol", - }).format(Number(value) / 10 ** Number(scale)); + currency: "EUR", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); }; - // Mock tax data - const mockTaxes = [ - { - tax_code: "IVA 21%", - taxable_amount: { - value: "10000", - scale: "2", - currency_code: "EUR", - }, - taxes_amount: { - value: "21000", - scale: "2", - currency_code: "EUR", - }, - }, - { - tax_code: "IVA 10%", - taxable_amount: { - value: "50000", - scale: "2", - currency_code: "EUR", - }, - taxes_amount: { - value: "5000", - scale: "2", - currency_code: "EUR", - }, - }, - ]; - - const displayTaxes = taxes ? taxes : mockTaxes; + const displayTaxes = taxes || []; return ( -
+
- {t("form_groups.tax_resume.title")} + {t("form_groups.tax_resume.title")} {t("form_groups.tax_resume.description")}
{displayTaxes.map((tax, index) => ( -
-
+
+
- {tax.tax_code} + {tax.tax_label}
-
+
- Base Imponible: - {formatCurrency(tax.taxable_amount)} + Base para el impuesto: + {formatCurrency(tax.taxable_amount)}
- Importe Impuesto: - + Importe de impuesto: + {formatCurrency(tax.taxes_amount)}
diff --git a/modules/customer-invoices/src/web/components/editor/invoice-totals.tsx b/modules/customer-invoices/src/web/components/editor/invoice-totals.tsx index 8e47a9d8..e0999029 100644 --- a/modules/customer-invoices/src/web/components/editor/invoice-totals.tsx +++ b/modules/customer-invoices/src/web/components/editor/invoice-totals.tsx @@ -1,18 +1,18 @@ import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components"; import { Input, Label, Separator } from "@repo/shadcn-ui/components"; import { CalculatorIcon } from "lucide-react"; -import { useState } from "react"; -import { useFormContext } from "react-hook-form"; +import { ComponentProps } from 'react'; +import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "../../i18n"; -import { CustomerInvoiceFormData } from "../../schemas"; +import { InvoiceFormData } from "../../schemas"; -export const InvoiceTotals = () => { +export const InvoiceTotals = (props: ComponentProps<"fieldset">) => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { control, getValues } = useFormContext(); //const invoiceFormData = useWatch({ control }); - const [invoice, setInvoice] = useState({ + /*const [invoice, setInvoice] = useState({ items: [], subtotal_amount: 0, discount_percentage: 0, @@ -23,7 +23,7 @@ export const InvoiceTotals = () => { }); const updateDiscount = (value: number) => { - const subtotal = invoice.items.reduce( + const subtotal = getValues('items.reduce( (sum: number, item: any) => sum + item.subtotal_amount, 0 ); @@ -41,7 +41,7 @@ export const InvoiceTotals = () => { taxes_amount: taxesAmount, total_amount: totalAmount, }); - }; + };*/ const formatCurrency = (amount: number) => { return new Intl.NumberFormat("es-ES", { @@ -53,9 +53,9 @@ export const InvoiceTotals = () => { }; return ( -
+
- {t("form_groups.totals.title")} + {t("form_groups.totals.title")} {t("form_groups.totals.description")} @@ -63,48 +63,54 @@ export const InvoiceTotals = () => {
- {formatCurrency(invoice.subtotal_amount)} + {formatCurrency(getValues('subtotal_amount'))}
- +
- updateDiscount(Number.parseFloat(e.target.value) || 0)} - className='w-20 text-right' + ()} /> - %
- - - -{formatCurrency(invoice.discount_amount)} + + + -{formatCurrency(getValues("discount_amount"))}
- - {formatCurrency(invoice.taxable_amount)} + + {formatCurrency(getValues('taxable_amount'))}
- - {formatCurrency(invoice.taxes_amount)} + + {formatCurrency(getValues('taxes_amount'))}
- - - {formatCurrency(invoice.total_amount)} + + + {formatCurrency(getValues('total_amount'))}
diff --git a/modules/customer-invoices/src/web/components/editor/items/blocks-view.tsx b/modules/customer-invoices/src/web/components/editor/items/blocks-view.tsx index c92ee172..025878a2 100644 --- a/modules/customer-invoices/src/web/components/editor/items/blocks-view.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/blocks-view.tsx @@ -3,7 +3,7 @@ import { Trash2 } from "lucide-react"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "../../../i18n"; -import { CustomerInvoiceFormData } from "../../../schemas"; +import { InvoiceFormData } from "../../../schemas"; import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select'; import { CustomItemViewProps } from "./types"; @@ -20,7 +20,7 @@ const formatCurrency = (amount: number) => { export const BlocksView = ({ items, removeItem, updateItem }: BlocksViewProps) => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { control } = useFormContext(); return (
diff --git a/modules/customer-invoices/src/web/components/editor/items/item-row.tsx b/modules/customer-invoices/src/web/components/editor/items/item-row.tsx index 54e74c38..14d9af79 100644 --- a/modules/customer-invoices/src/web/components/editor/items/item-row.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/item-row.tsx @@ -2,7 +2,7 @@ import { Button, Checkbox, TableCell, TableRow, Tooltip, TooltipContent, Tooltip import { ArrowDownIcon, ArrowUpIcon, CopyIcon, Trash2Icon } from "lucide-react"; import { Control, Controller } from "react-hook-form"; import { useTranslation } from '../../../i18n'; -import { CustomerInvoiceItemFormData } from '../../../schemas'; +import { InvoiceItemFormData } from '../../../schemas'; import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select'; import { AmountDTOInputField } from './amount-dto-input-field'; import { HoverCardTotalsSummary } from './hover-card-total-summary'; @@ -11,7 +11,7 @@ import { QuantityDTOInputField } from './quantity-dto-input-field'; export type ItemRowProps = { control: Control, - item: CustomerInvoiceItemFormData; + item: InvoiceItemFormData; rowIndex: number; isSelected: boolean; isFirst: boolean; @@ -47,7 +47,7 @@ export const ItemRow = ({
void; + value?: InvoiceItemFormData[]; + onChange?: (items: InvoiceItemFormData[]) => void; readOnly?: boolean; } diff --git a/modules/customer-invoices/src/web/components/editor/items/table-view.tsx b/modules/customer-invoices/src/web/components/editor/items/table-view.tsx index 18eb5669..1fa7d766 100644 --- a/modules/customer-invoices/src/web/components/editor/items/table-view.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/table-view.tsx @@ -20,7 +20,7 @@ import { useMoney } from '@erp/core/hooks'; import { useEffect, useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { useTranslation } from '../../../i18n'; -import { CustomerInvoiceItemFormData } from '../../../schemas'; +import { InvoiceItemFormData } from '../../../schemas'; import { HoverCardTotalsSummary } from './hover-card-total-summary'; import { CustomItemViewProps } from "./types"; @@ -28,25 +28,25 @@ export interface TableViewProps extends CustomItemViewProps { } export const TableView = ({ items, actions }: TableViewProps) => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { control } = useFormContext(); const { format } = useMoney(); - const [lines, setLines] = useState(items); + const [lines, setLines] = useState(items); useEffect(() => { setLines(items) }, [items]) // Mantiene sincronía con el formulario padre - const updateItems = (updated: CustomerInvoiceItemFormData[]) => { + const updateItems = (updated: InvoiceItemFormData[]) => { setLines(updated); onItemsChange(updated); }; /** 🔹 Actualiza una fila con recalculo */ - const updateItem = (index: number, patch: Partial) => { + const updateItem = (index: number, patch: Partial) => { const newItems = [...lines]; const merged = { ...newItems[index], ...patch }; - newItems[index] = calculateItemAmounts(merged as CustomerInvoiceItemFormData); + newItems[index] = calculateItemAmounts(merged as InvoiceItemFormData); updateItems(newItems); }; @@ -79,8 +79,8 @@ export const TableView = ({ items, actions }: TableViewProps) => { /** 🔹 Añade una nueva línea vacía */ const addNewItem = () => { - const newItem: CustomerInvoiceItemFormData = { - isNonValued: false, + const newItem: InvoiceItemFormData = { + is_non_valued: false, description: "", quantity: { value: "0", scale: "2" }, unit_amount: { value: "0", scale: "2", currency_code: "EUR" }, diff --git a/modules/customer-invoices/src/web/components/editor/recipient/invoice-recipient.tsx b/modules/customer-invoices/src/web/components/editor/recipient/invoice-recipient.tsx index 827c1838..8da6a706 100644 --- a/modules/customer-invoices/src/web/components/editor/recipient/invoice-recipient.tsx +++ b/modules/customer-invoices/src/web/components/editor/recipient/invoice-recipient.tsx @@ -2,10 +2,11 @@ import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/componen import { useFormContext } from "react-hook-form"; import { UserIcon } from "lucide-react"; +import { ComponentProps } from 'react'; import { useTranslation } from "../../../i18n"; import { RecipientModalSelectorField } from "./recipient-modal-selector-field"; -export const InvoiceRecipient = () => { +export const InvoiceRecipient = (props: ComponentProps<"fieldset">) => { const { t } = useTranslation(); const { control, getValues } = useFormContext(); @@ -13,7 +14,7 @@ export const InvoiceRecipient = () => { const recipient = getValues('recipient'); return ( -
+
{t("form_groups.customer.title")} diff --git a/modules/customer-invoices/src/web/context/invoice-context.tsx b/modules/customer-invoices/src/web/context/invoice-context.tsx index 2e36b03e..7c24d9d2 100644 --- a/modules/customer-invoices/src/web/context/invoice-context.tsx +++ b/modules/customer-invoices/src/web/context/invoice-context.tsx @@ -2,6 +2,7 @@ import { PropsWithChildren, createContext, useCallback, useContext, useMemo, use export type InvoiceContextValue = { company_id: string; + status: string; currency_code: string; language_code: string; is_proforma: boolean; @@ -15,13 +16,14 @@ const InvoiceContext = createContext(null); export interface InvoiceProviderParams { company_id: string; + status: string; // default "draft" language_code?: string; // default "es" currency_code?: string; // default "EUR" is_proforma?: boolean; // default 'true' children: React.ReactNode; } -export const InvoiceProvider = ({ company_id, language_code: initialLang = "es", +export const InvoiceProvider = ({ company_id, status: initialStatus = "draft", language_code: initialLang = "es", currency_code: initialCurrency = "EUR", is_proforma: initialProforma = true, children }: PropsWithChildren) => { @@ -29,6 +31,7 @@ export const InvoiceProvider = ({ company_id, language_code: initialLang = "es", const [language_code, setLanguage] = useState(initialLang); const [currency_code, setCurrency] = useState(initialCurrency); const [is_proforma, setIsProforma] = useState(initialProforma); + const [status] = useState(initialStatus); // Callbacks memoizados const setLanguageMemo = useCallback((language_code: string) => setLanguage(language_code), []); @@ -39,6 +42,7 @@ export const InvoiceProvider = ({ company_id, language_code: initialLang = "es", return { company_id, + status, language_code, currency_code, is_proforma, diff --git a/modules/customer-invoices/src/web/customer-invoice-routes.tsx b/modules/customer-invoices/src/web/customer-invoice-routes.tsx index 6fc6ca23..c8288d64 100644 --- a/modules/customer-invoices/src/web/customer-invoice-routes.tsx +++ b/modules/customer-invoices/src/web/customer-invoice-routes.tsx @@ -15,7 +15,7 @@ const CustomerInvoiceAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate })) ); const CustomerInvoiceUpdate = lazy(() => - import("./pages").then((m) => ({ default: m.CustomerInvoiceUpdatePage })) + import("./pages").then((m) => ({ default: m.InvoiceUpdatePage })) ); export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => { diff --git a/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-items-totals.ts b/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-items-totals.ts index 7e3b4c9f..8bffab6f 100644 --- a/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-items-totals.ts +++ b/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-items-totals.ts @@ -1,7 +1,7 @@ import { MoneyDTO } from "@erp/core"; import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks"; import { useMemo } from "react"; -import { CustomerInvoiceItemFormData } from "../../schemas"; +import { InvoiceItemFormData } from "../../schemas"; /** * Calcula totales derivados de un ítem de factura @@ -30,7 +30,7 @@ export type InvoiceItemTotals = Readonly<{ /** * Calcula totales derivados de una línea de factura usando tus hooks de Money/Quantity/Percentage. */ -export function useCalcInvoiceItemTotals(item?: CustomerInvoiceItemFormData): InvoiceItemTotals { +export function useCalcInvoiceItemTotals(item?: InvoiceItemFormData): InvoiceItemTotals { const moneyHelper = useMoney(); const qtyHelper = useQuantity(); const pctHelper = usePercentage(); diff --git a/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-totals.ts b/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-totals.ts index 70759c5e..183d4aea 100644 --- a/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-totals.ts +++ b/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-totals.ts @@ -1,7 +1,7 @@ import { MoneyDTO } from "@erp/core"; import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks"; import { useMemo } from "react"; -import { CustomerInvoiceItemFormData } from "../../schemas"; +import { InvoiceItemFormData } from "../../schemas"; export type InvoiceTotals = Readonly<{ subtotal: number; @@ -23,9 +23,7 @@ export type InvoiceTotals = Readonly<{ /** * Calcula los totales generales de la factura a partir de sus líneas. */ -export function useCalcInvoiceTotals( - items: CustomerInvoiceItemFormData[] | undefined -): InvoiceTotals { +export function useCalcInvoiceTotals(items: InvoiceItemFormData[] | undefined): InvoiceTotals { const money = useMoney(); const qty = useQuantity(); const pct = usePercentage(); diff --git a/modules/customer-invoices/src/web/hooks/calcs/use-invoice-auto-recalc.ts b/modules/customer-invoices/src/web/hooks/calcs/use-invoice-auto-recalc.ts index d16959e0..e6c25c9b 100644 --- a/modules/customer-invoices/src/web/hooks/calcs/use-invoice-auto-recalc.ts +++ b/modules/customer-invoices/src/web/hooks/calcs/use-invoice-auto-recalc.ts @@ -2,13 +2,13 @@ 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"; +import { InvoiceFormData, InvoiceItemFormData } 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) { +export function useInvoiceAutoRecalc(form: UseFormReturn) { const { watch, setValue, @@ -22,7 +22,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn { + (item: InvoiceItemFormData) => { if (!item) { const zero = moneyHelper.fromNumber(0); return { @@ -65,7 +65,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn { + (items: InvoiceItemFormData[]) => { let subtotalDTO = moneyHelper.fromNumber(0); let discountTotalDTO = moneyHelper.fromNumber(0); let taxableBaseDTO = moneyHelper.fromNumber(0); @@ -106,7 +106,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn { if (!item) return; - const typedItem = item as CustomerInvoiceItemFormData; + const typedItem = item as InvoiceItemFormData; const totals = calculateItemTotals(typedItem); const current = getValues(`items.${i}.total_amount`); @@ -120,7 +120,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn { diff --git a/modules/customer-invoices/src/web/hooks/use-customer-invoice-query.ts b/modules/customer-invoices/src/web/hooks/use-invoice-query.ts similarity index 93% rename from modules/customer-invoices/src/web/hooks/use-customer-invoice-query.ts rename to modules/customer-invoices/src/web/hooks/use-invoice-query.ts index 7662284f..0e3a7c8d 100644 --- a/modules/customer-invoices/src/web/hooks/use-customer-invoice-query.ts +++ b/modules/customer-invoices/src/web/hooks/use-invoice-query.ts @@ -9,7 +9,7 @@ type CustomerInvoiceQueryOptions = { enabled?: boolean; }; -export function useCustomerInvoiceQuery(invoiceId?: string, options?: CustomerInvoiceQueryOptions) { +export function useInvoiceQuery(invoiceId?: string, options?: CustomerInvoiceQueryOptions) { const dataSource = useDataSource(); const enabled = (options?.enabled ?? true) && !!invoiceId; diff --git a/modules/customer-invoices/src/web/hooks/use-update-customer-invoice-mutation.ts b/modules/customer-invoices/src/web/hooks/use-update-customer-invoice-mutation.ts index 05b1789c..96d2a35a 100644 --- a/modules/customer-invoices/src/web/hooks/use-update-customer-invoice-mutation.ts +++ b/modules/customer-invoices/src/web/hooks/use-update-customer-invoice-mutation.ts @@ -5,8 +5,8 @@ import { UpdateCustomerInvoiceByIdRequestDTO, UpdateCustomerInvoiceByIdRequestSchema, } from "../../common"; -import { CustomerInvoiceFormData } from "../schemas"; -import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-customer-invoice-query"; +import { InvoiceFormData } from "../schemas"; +import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-invoice-query"; export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const; @@ -14,7 +14,7 @@ type UpdateCustomerInvoiceContext = {}; type UpdateCustomerInvoicePayload = { id: string; - data: Partial; + data: Partial; }; export function useUpdateCustomerInvoice() { @@ -23,7 +23,7 @@ export function useUpdateCustomerInvoice() { const schema = UpdateCustomerInvoiceByIdRequestSchema; return useMutation< - CustomerInvoiceFormData, + InvoiceFormData, Error, UpdateCustomerInvoicePayload, UpdateCustomerInvoiceContext @@ -53,9 +53,9 @@ export function useUpdateCustomerInvoice() { } const updated = await dataSource.updateOne("customer-invoices", invoiceId, data); - return updated as CustomerInvoiceFormData; + return updated as InvoiceFormData; }, - onSuccess: (updated: CustomerInvoiceFormData, variables) => { + onSuccess: (updated: InvoiceFormData, variables) => { const { id: invoiceId } = variables; // Refresca inmediatamente el detalle diff --git a/modules/customer-invoices/src/web/pages/update/index.ts b/modules/customer-invoices/src/web/pages/update/index.ts index de0f684c..de801651 100644 --- a/modules/customer-invoices/src/web/pages/update/index.ts +++ b/modules/customer-invoices/src/web/pages/update/index.ts @@ -1 +1 @@ -export * from "./customer-invoices-update-page"; +export * from "./invoice-update-page"; diff --git a/modules/customer-invoices/src/web/pages/update/customer-invoices-update-page.tsx b/modules/customer-invoices/src/web/pages/update/invoice-update-page.tsx similarity index 85% rename from modules/customer-invoices/src/web/pages/update/customer-invoices-update-page.tsx rename to modules/customer-invoices/src/web/pages/update/invoice-update-page.tsx index 5c1de30c..96617f57 100644 --- a/modules/customer-invoices/src/web/pages/update/customer-invoices-update-page.tsx +++ b/modules/customer-invoices/src/web/pages/update/invoice-update-page.tsx @@ -17,15 +17,16 @@ import { PageHeader, } from "../../components"; import { InvoiceProvider } from '../../context'; -import { useCustomerInvoiceQuery, useInvoiceAutoRecalc, useUpdateCustomerInvoice } from "../../hooks"; +import { useInvoiceQuery, useUpdateCustomerInvoice } from "../../hooks"; import { useTranslation } from "../../i18n"; import { - CustomerInvoiceFormData, - CustomerInvoiceFormSchema, - defaultCustomerInvoiceFormData + InvoiceFormData, + InvoiceFormSchema, + defaultCustomerInvoiceFormData, + invoiceDtoToFormAdapter } from "../../schemas"; -export const CustomerInvoiceUpdatePage = () => { +export const InvoiceUpdatePage = () => { const invoiceId = useUrlParamId(); const { t } = useTranslation(); const navigate = useNavigate(); @@ -36,7 +37,7 @@ export const CustomerInvoiceUpdatePage = () => { isLoading: isLoadingInvoice, isError: isLoadError, error: loadError, - } = useCustomerInvoiceQuery(invoiceId, { enabled: !!invoiceId }); + } = useInvoiceQuery(invoiceId, { enabled: !!invoiceId }); // 2) Estado de actualización (mutación) const { @@ -47,16 +48,17 @@ export const CustomerInvoiceUpdatePage = () => { } = useUpdateCustomerInvoice(); // 3) Form hook - const form = useHookForm({ - resolverSchema: CustomerInvoiceFormSchema, - initialValues: (invoiceData as unknown as CustomerInvoiceFormData) ?? defaultCustomerInvoiceFormData, + const form = useHookForm({ + resolverSchema: InvoiceFormSchema, + defaultValues: defaultCustomerInvoiceFormData, + values: invoiceData ? invoiceDtoToFormAdapter.fromDto(invoiceData) : undefined, disabled: isUpdating, }); // 4) Activa recálculo automático de los totales de la factura cuando hay algún cambio en importes - useInvoiceAutoRecalc(form); + // useInvoiceAutoRecalc(form); - const handleSubmit = (formData: CustomerInvoiceFormData) => { + const handleSubmit = (formData: InvoiceFormData) => { const { dirtyFields } = form.formState; if (!formHasAnyDirty(dirtyFields)) { @@ -74,7 +76,7 @@ export const CustomerInvoiceUpdatePage = () => { showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg")); // 🔹 limpiar el form e isDirty pasa a false - form.reset(data as unknown as CustomerInvoiceFormData); + form.reset(data as unknown as InvoiceFormData); }, onError(error) { showErrorToast(t("pages.update.errorTitle"), error.message); @@ -84,13 +86,13 @@ export const CustomerInvoiceUpdatePage = () => { }; const handleReset = () => - form.reset((invoiceData as unknown as CustomerInvoiceFormData) ?? defaultCustomerInvoiceFormData); + form.reset((invoiceData as unknown as InvoiceFormData) ?? defaultCustomerInvoiceFormData); const handleBack = () => { navigate(-1); }; - const handleError = (errors: FieldErrors) => { + const handleError = (errors: FieldErrors) => { console.error("Errores en el formulario:", errors); // Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario }; @@ -136,6 +138,7 @@ export const CustomerInvoiceUpdatePage = () => { return ( diff --git a/modules/customer-invoices/src/web/schemas/customer-invoices.form.schema.ts b/modules/customer-invoices/src/web/schemas/customer-invoices.form.schema.ts deleted file mode 100644 index eeec8522..00000000 --- a/modules/customer-invoices/src/web/schemas/customer-invoices.form.schema.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core"; -import { ArrayElement } from "@repo/rdx-utils"; -import { z } from "zod/v4"; - -export const CustomerInvoiceItemFormSchema = z.object({ - isNonValued: z.boolean().optional(), - - description: z.string().optional(), - quantity: QuantitySchema.optional(), - unit_amount: MoneySchema.optional(), - - subtotal_amount: MoneySchema.optional(), - discount_percentage: PercentageSchema.optional(), - discount_amount: MoneySchema.optional(), - taxable_amount: MoneySchema.optional(), - - tax_codes: z.array(z.string()).default([]), - taxes: z - .array( - z.object({ - label: z.string(), - percentage: z.number(), - amount: MoneySchema.optional(), - }) - ) - .optional(), - - taxes_amount: MoneySchema.optional(), - total_amount: MoneySchema.optional(), -}); - -export const CustomerInvoiceFormSchema = z.object({ - invoice_number: z.string().optional(), - status: z.string(), - series: z.string().optional(), - - invoice_date: z.string().optional(), - operation_date: z.string().optional(), - - customer_id: z.string().optional(), - - description: z.string().optional(), - notes: z.string().optional(), - - language_code: z - .string({ - error: "El idioma es obligatorio", - }) - .min(1, "Debe indicar un idioma") - .toUpperCase() // asegura mayúsculas - .default("es"), - - currency_code: z - .string({ - error: "La moneda es obligatoria", - }) - .min(1, "La moneda no puede estar vacía") - .toUpperCase() // asegura mayúsculas - .default("EUR"), - - /*taxes: z - .array( - z.object({ - tax_code: z.string(), - taxable_amount: MoneySchema, - taxes_amount: MoneySchema, - }) - ) - .optional(), -*/ - - items: z.array(CustomerInvoiceItemFormSchema).optional(), - - subtotal_amount: MoneySchema, - discount_percentage: PercentageSchema, - discount_amount: MoneySchema, - taxable_amount: MoneySchema, - taxes_amount: MoneySchema, - total_amount: MoneySchema, -}); - -export type CustomerInvoiceFormData = z.infer; -export type CustomerInvoiceItemFormData = ArrayElement; - -export const defaultCustomerInvoiceItemFormData: CustomerInvoiceItemFormData = { - description: "", - - quantity: { - value: "", - scale: "2", - }, - - unit_amount: { - currency_code: "EUR", - value: "", - scale: "4", - }, - - discount_percentage: { - value: "", - scale: "2", - }, - - tax_codes: ["iva_21"], - - total_amount: { - currency_code: "EUR", - value: "", - scale: "4", - }, -}; - -export const defaultCustomerInvoiceFormData: CustomerInvoiceFormData = { - invoice_number: "", - status: "draft", - series: "", - - invoice_date: "", - operation_date: "", - - description: "", - notes: "", - - language_code: "es", - currency_code: "EUR", - - //taxes: [], - - items: [], - - subtotal_amount: { - currency_code: "EUR", - value: "0", - scale: "2", - }, - discount_amount: { - currency_code: "EUR", - value: "0", - scale: "2", - }, - discount_percentage: { - value: "0", - scale: "2", - }, - taxable_amount: { - currency_code: "EUR", - value: "0", - scale: "2", - }, - taxes_amount: { - currency_code: "EUR", - value: "0", - scale: "2", - }, - total_amount: { - currency_code: "EUR", - value: "0", - scale: "2", - }, -}; diff --git a/modules/customer-invoices/src/web/schemas/index.ts b/modules/customer-invoices/src/web/schemas/index.ts index cda9c8dc..d37fdaf1 100644 --- a/modules/customer-invoices/src/web/schemas/index.ts +++ b/modules/customer-invoices/src/web/schemas/index.ts @@ -1,2 +1,3 @@ export * from "./customer-invoices.api.schema"; -export * from "./customer-invoices.form.schema"; +export * from "./invoice-dto.adapter"; +export * from "./invoice.form.schema"; diff --git a/modules/customer-invoices/src/web/schemas/invoice-dto.adapter.ts b/modules/customer-invoices/src/web/schemas/invoice-dto.adapter.ts new file mode 100644 index 00000000..ca58e3c2 --- /dev/null +++ b/modules/customer-invoices/src/web/schemas/invoice-dto.adapter.ts @@ -0,0 +1,96 @@ +import { + MoneyDTOHelper, + PercentageDTOHelper, + QuantityDTOHelper, + SpainTaxCatalogProvider, +} from "@erp/core"; +import { + GetCustomerInvoiceByIdResponseDTO, + UpdateCustomerInvoiceByIdRequestDTO, +} from "../../common"; +import { InvoiceContextValue } from "../context"; +import { InvoiceFormData } from "./invoice.form.schema"; + +/** + * Convierte el DTO completo de API a datos numéricos para el formulario. + */ +export const invoiceDtoToFormAdapter = { + fromDto(dto: GetCustomerInvoiceByIdResponseDTO): InvoiceFormData { + const taxCatalog = SpainTaxCatalogProvider(); + + return { + invoice_number: dto.invoice_number, + series: dto.series, + + invoice_date: dto.invoice_date, + operation_date: dto.operation_date, + + customer_id: dto.customer_id, + + reference: dto.reference ?? "", + description: dto.description ?? "", + notes: dto.notes ?? "", + + language_code: dto.language_code, + currency_code: dto.currency_code, + + subtotal_amount: MoneyDTOHelper.toNumber(dto.subtotal_amount), + discount_percentage: PercentageDTOHelper.toNumber(dto.discount_percentage), + discount_amount: MoneyDTOHelper.toNumber(dto.discount_amount), + taxable_amount: MoneyDTOHelper.toNumber(dto.taxable_amount), + taxes_amount: MoneyDTOHelper.toNumber(dto.taxes_amount), + total_amount: MoneyDTOHelper.toNumber(dto.total_amount), + + taxes: dto.taxes.map((taxItem) => ({ + tax_code: taxItem.tax_code, + tax_label: taxCatalog.findByCode(taxItem.tax_code).match( + (tax) => tax.name, + () => "" + ), + taxable_amount: MoneyDTOHelper.toNumber(taxItem.taxable_amount), + taxes_amount: MoneyDTOHelper.toNumber(taxItem.taxes_amount), + })), + + items: dto.items.map((item) => ({ + is_non_valued: item.is_non_valued === "true", + description: item.description ?? "", + quantity: QuantityDTOHelper.toNumericString(item.quantity), + unit_amount: MoneyDTOHelper.toNumericString(item.unit_amount), + subtotal_amount: MoneyDTOHelper.toNumber(item.subtotal_amount), + discount_percentage: PercentageDTOHelper.toNumericString(item.discount_percentage), + discount_amount: MoneyDTOHelper.toNumber(item.discount_amount), + taxable_amount: MoneyDTOHelper.toNumber(item.taxable_amount), + tax_codes: item.tax_codes ?? [], + taxes_amount: MoneyDTOHelper.toNumber(item.taxes_amount), + total_amount: MoneyDTOHelper.toNumber(item.total_amount), + })), + }; + }, + + toDto(form: InvoiceFormData, context: InvoiceContextValue): UpdateCustomerInvoiceByIdRequestDTO { + return { + series: form.series, + + invoice_date: form.invoice_date, + operation_date: form.operation_date, + + customer_id: form.customer_id, + + reference: form.reference, + description: form.description, + notes: form.notes, + + language_code: context.language_code, + currency_code: context.currency_code, + + items: form.items?.map((item) => ({ + is_non_valued: item.is_non_valued ? "true" : "false", + description: item.description, + quantity: QuantityDTOHelper.fromNumericString(item.quantity, 4), + unit_amount: MoneyDTOHelper.fromNumericString(item.unit_amount, context.currency_code, 4), + discount_percentage: PercentageDTOHelper.fromNumericString(item.discount_percentage, 2), + tax_codes: item.tax_codes, + })), + }; + }, +}; diff --git a/modules/customer-invoices/src/web/schemas/invoice.form.schema.ts b/modules/customer-invoices/src/web/schemas/invoice.form.schema.ts new file mode 100644 index 00000000..9b8c6264 --- /dev/null +++ b/modules/customer-invoices/src/web/schemas/invoice.form.schema.ts @@ -0,0 +1,124 @@ +import { NumericStringSchema } from "@erp/core"; +import { z } from "zod/v4"; + +export const InvoiceItemFormSchema = z.object({ + is_non_valued: z.boolean(), + + description: z.string().max(2000).optional().default(""), + quantity: NumericStringSchema.optional(), + unit_amount: NumericStringSchema.optional(), + + subtotal_amount: z.number(), + discount_percentage: NumericStringSchema.optional(), + discount_amount: z.number(), + taxable_amount: z.number(), + + tax_codes: z.array(z.string()).default([]), + + taxes_amount: z.number(), + total_amount: z.number(), +}); + +export const InvoiceFormSchema = z.object({ + invoice_number: z.string().optional(), + series: z.string().optional(), + + invoice_date: z.string().optional(), + operation_date: z.string().optional(), + + customer_id: z.string().optional(), + recipient: z + .object({ + id: z.string().optional(), + name: z.string().optional(), + tin: z.string().optional(), + street: z.string().optional(), + street2: z.string().optional(), + city: z.string().optional(), + province: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional(), + }) + .optional(), + + reference: z.string().optional(), + description: z.string().optional(), + notes: z.string().optional(), + + language_code: z + .string({ + error: "El idioma es obligatorio", + }) + .min(1, "Debe indicar un idioma") + .toUpperCase() // asegura mayúsculas + .default("es"), + + currency_code: z + .string({ + error: "La moneda es obligatoria", + }) + .min(1, "La moneda no puede estar vacía") + .toUpperCase() // asegura mayúsculas + .default("EUR"), + + taxes: z + .array( + z.object({ + tax_code: z.string(), + tax_label: z.string(), + taxable_amount: z.number(), + taxes_amount: z.number(), + }) + ) + .optional(), + + items: z.array(InvoiceItemFormSchema).optional(), + + subtotal_amount: z.number(), + discount_percentage: z.number(), + discount_amount: z.number(), + taxable_amount: z.number(), + taxes_amount: z.number(), + total_amount: z.number(), +}); + +export type InvoiceFormData = z.infer; +export type InvoiceItemFormData = z.infer; + +export const defaultCustomerInvoiceItemFormData: InvoiceItemFormData = { + is_non_valued: false, + description: "", + quantity: "", + unit_amount: "", + subtotal_amount: 0, + discount_percentage: "", + discount_amount: 0, + taxable_amount: 0, + tax_codes: ["iva_21"], + taxes_amount: 0, + total_amount: 0, +}; + +export const defaultCustomerInvoiceFormData: InvoiceFormData = { + invoice_number: "", + series: "", + + invoice_date: "", + operation_date: "", + + reference: "", + description: "", + notes: "", + + language_code: "es", + currency_code: "EUR", + + items: [], + + subtotal_amount: 0, + discount_amount: 0, + discount_percentage: 0, + taxable_amount: 0, + taxes_amount: 0, + total_amount: 0, +}; diff --git a/modules/customers/src/web/components/client-selector-modal.tsx b/modules/customers/src/web/components/client-selector-modal.tsx index 588c14c2..e9a82cd2 100644 --- a/modules/customers/src/web/components/client-selector-modal.tsx +++ b/modules/customers/src/web/components/client-selector-modal.tsx @@ -222,7 +222,7 @@ export const ClientSelectorModal = () => { - + Nuevo Cliente @@ -248,7 +248,7 @@ const CustomerCard = ({ customer }: { customer: Customer }) => (
- + {customer.name} {customer.status} diff --git a/modules/customers/src/web/components/customer-modal-selector/create-customer-form-dialog.tsx b/modules/customers/src/web/components/customer-modal-selector/create-customer-form-dialog.tsx index 137b2bf7..b837de86 100644 --- a/modules/customers/src/web/components/customer-modal-selector/create-customer-form-dialog.tsx +++ b/modules/customers/src/web/components/customer-modal-selector/create-customer-form-dialog.tsx @@ -31,7 +31,7 @@ export const CreateCustomerFormDialog = ({ - Agregar Nuevo Cliente + Agregar Nuevo Cliente Complete la información del cliente. Los campos marcados con * son obligatorios. diff --git a/modules/customers/src/web/components/customer-modal-selector/customer-search-dialog.tsx b/modules/customers/src/web/components/customer-modal-selector/customer-search-dialog.tsx index b3c1f176..a113c3d5 100644 --- a/modules/customers/src/web/components/customer-modal-selector/customer-search-dialog.tsx +++ b/modules/customers/src/web/components/customer-modal-selector/customer-search-dialog.tsx @@ -61,7 +61,7 @@ export const CustomerSearchDialog = ({ - + Seleccionar Cliente diff --git a/modules/customers/src/web/pages/view/customer-view-page.tsx b/modules/customers/src/web/pages/view/customer-view-page.tsx index 6e5d9bbe..bcebd12d 100644 --- a/modules/customers/src/web/pages/view/customer-view-page.tsx +++ b/modules/customers/src/web/pages/view/customer-view-page.tsx @@ -102,7 +102,7 @@ export const CustomerViewPage = () => { - + Información Básica @@ -136,7 +136,7 @@ export const CustomerViewPage = () => { - + Dirección @@ -180,7 +180,7 @@ export const CustomerViewPage = () => { - + Información de Contacto @@ -306,7 +306,7 @@ export const CustomerViewPage = () => { - + Preferencias diff --git a/modules/verifactu/src/common/dto/request/get-verifactu-record-by-id.response.dto.ts b/modules/verifactu/src/common/dto/request/get-verifactu-record-by-id.response.dto.ts index 0b485eb8..396ac37c 100644 --- a/modules/verifactu/src/common/dto/request/get-verifactu-record-by-id.response.dto.ts +++ b/modules/verifactu/src/common/dto/request/get-verifactu-record-by-id.response.dto.ts @@ -49,7 +49,7 @@ export const GetVerifactuRecordByIdResponseSchema = z.object({ items: z.array( z.object({ id: z.uuid(), - isNonValued: z.string(), + is_non_valued: z.string(), position: z.string(), description: z.string(), quantity: QuantitySchema, diff --git a/packages/rdx-ui/src/components/form/TextAreaField.tsx b/packages/rdx-ui/src/components/form/TextAreaField.tsx index 4e5140aa..e0e9095b 100644 --- a/packages/rdx-ui/src/components/form/TextAreaField.tsx +++ b/packages/rdx-ui/src/components/form/TextAreaField.tsx @@ -28,6 +28,8 @@ type TextAreaFieldProps = CommonInputProps & { /** Contador de caracteres (si usas maxLength) */ showCounter?: boolean; + maxLength?: number; + rows?: number; }; export function TextAreaField({ @@ -42,6 +44,7 @@ export function TextAreaField({ className, showCounter = false, maxLength, + rows = 3 }: TextAreaFieldProps) { const { t } = useTranslation(); const isDisabled = disabled || readOnly; @@ -57,7 +60,7 @@ export function TextAreaField({ control={control} name={name} render={({ field }) => ( - + {label && (
@@ -81,8 +84,11 @@ export function TextAreaField({