From 79e90ec00f276b41c088344624dbd78c5e8b6929 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 29 Apr 2026 17:02:08 +0200 Subject: [PATCH] =?UTF-8?q?Edici=C3=B3n=20de=20detalles=20de=20factura?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/package.json | 2 +- apps/web/package.json | 2 +- modules/core/package.json | 2 +- .../src/common/helpers/dto-compare-helper.ts | 16 - modules/core/src/common/helpers/index.ts | 4 - .../core/src/common/helpers/money-helper.ts | 66 -- .../core/src/common/helpers/number-helper.ts | 7 - modules/core/src/web/hooks/index.ts | 6 - .../use-datasource/datasource-context.tsx | 3 +- .../src/web/hooks/use-datasource/use-list.ts | 87 -- .../use-datasource/use-loading-overtime.ts | 101 -- modules/core/src/web/hooks/use-money-dto.ts | 101 -- modules/core/src/web/hooks/use-money.ts | 226 ----- .../src/web/hooks/use-pagination/index.ts | 2 - .../use-pagination-sync-with-location.tsx | 54 -- .../use-pagination/use-pagination.test.ts | 76 -- .../hooks/use-pagination/use-pagination.tsx | 52 -- modules/core/src/web/hooks/use-percentage.ts | 26 - modules/core/src/web/hooks/use-quantity.ts | 206 ----- .../core/src/web/hooks/use-query-key/index.ts | 5 - .../web/hooks/use-query-key/key-builder.ts | 181 ---- modules/core/src/web/hooks/use-toggle.ts | 15 - modules/customer-invoices/package.json | 2 +- .../get-issued-invoice-by-id.controller.ts | 4 +- .../list-issued-invoices.controller.ts | 4 +- .../report-issued-invoice.controller.ts | 4 +- .../issued-invoices-api-error-mapper.ts | 2 +- .../update-proforma-by-id.request.dto.ts | 33 +- .../src/web/hooks/calcs/index.ts | 1 - .../calcs/use-calc-invoice-items-totals.ts | 77 -- .../hooks/calcs/use-calc-invoice-totals.ts | 87 -- .../use-customer-invoice-item-summary.ts | 102 -- .../hooks/calcs/use-proforma-auto-recalc.ts | 191 ---- .../adapters/list-issued-invoice.adapter.ts | 13 +- .../create-customer-invoice-edit-form.tsx | 874 ------------------ .../create/create-customer-invoice-page.tsx | 131 --- .../pages/create/customer-invoice.schema.ts | 41 - .../src/web/pages/create/index.ts | 1 - .../src/web/pages/create/utils.ts | 41 - .../customer-invoices/src/web/pages/index.ts | 4 - .../src/web/pages/list/index.ts | 1 - .../web/pages/list/invoice-preview-panel.tsx | 204 ---- .../src/web/pages/list/invoices-list-page.tsx | 102 -- .../helpers/proforma-status-ui.ts | 12 +- .../blocks/proformas-grid/proformas-grid.tsx | 37 +- .../use-proforma-grid-columns.tsx | 11 +- .../ui/components/proforma-status-badge.tsx | 4 +- .../list/ui/pages/list-proformas-page.tsx | 12 +- .../proformas/list/ui/pages/orders-table.tsx | 362 ++++++++ .../update/ui/blocks/proforma-totals.tsx | 3 +- .../shared/adapters/list-proformas.adapter.ts | 14 +- .../entities/proforma-list-row.entity.ts | 1 - .../shared/hooks/to-validation-errors.ts | 13 +- .../src/web/proformas/ui/blocks/index.ts | 1 - .../ui/blocks/proforma-tax-summary.tsx | 78 -- .../ui/components/amount-input-field.tsx | 56 -- .../proformas/ui/components/amount-input.tsx | 233 ----- .../src/web/proformas/ui/components/index.ts | 3 - .../proformas/ui/components/input-utils.ts | 73 -- .../ui/components/percentage-input-field.tsx | 56 -- .../ui/components/percentage-input.tsx | 254 ----- .../ui/components/quantity-input-field.tsx | 56 -- .../ui/components/quantity-input.tsx | 269 ------ .../src/web/proformas/ui/index.ts | 2 - .../use-update-proforma-controller.ts | 16 +- .../use-update-proforma-items-controller.ts | 87 +- .../proforma-item-update-form.entity.ts | 2 +- .../proforma-item-update-form.schema.ts | 33 +- .../web/proformas/update/ui/blocks/index.ts | 2 +- .../update/ui/blocks/line-editor.tsx | 240 +++++ .../update/ui/blocks/proforma-line-editor.tsx | 145 +++ .../editors/proforma-update-items-editor.tsx | 52 +- .../focus-first-proforma-update-form-error.ts | 14 - .../src/web/proformas/update/utils/index.ts | 1 - .../customer-stats-section.tsx | 8 +- .../use-customer-update.controller.ts | 12 +- .../focus-first-customer-update-form-error.ts | 14 - .../customers/src/web/update/utils/index.ts | 1 - packages/rdx-ddd/package.json | 2 +- packages/rdx-ui/package.json | 6 +- .../datatable/data-table-toolbar.tsx | 7 +- .../src/components/datatable/data-table.tsx | 235 +++-- .../rdx-ui/src/components/datatable/types.ts | 9 + .../rdx-ui/src/components/error-overlay.tsx | 2 +- .../src/components/form/NumberField.tsx | 2 +- .../src/components/form/checkbox-field.tsx | 6 +- .../src/components/form/date-picker-field.tsx | 6 +- .../form/decimal-field/decimal-field.tsx | 13 +- .../form/decimal-field/decimal-field.types.ts | 1 + .../form/decimal-field/decimal-field.utils.ts | 11 +- .../src/components/form/form-section-card.tsx | 47 +- .../src/components/form/form-section-grid.tsx | 6 +- .../components/form/multi-select-field.tsx | 9 +- .../src/components/form/radio-group-field.tsx | 9 +- .../src/components/form/select-field.tsx | 7 +- .../src/components/form/text-area-field.tsx | 6 +- .../rdx-ui/src/components/form/text-field.tsx | 6 +- packages/rdx-ui/src/components/grid/cell.tsx | 2 +- packages/rdx-ui/src/components/grid/grid.tsx | 2 +- .../loading-overlay/loading-spin-icon.tsx | 2 +- .../lookup-dialog/lookup-dialog.tsx | 2 +- .../src/components/multiple-selector.tsx | 6 +- .../helpers/focus-first-input-form-error.ts | 12 + packages/rdx-ui/src/helpers/index.ts | 1 + .../rdx-utils/src}/helpers/date-helper.ts | 0 packages/rdx-utils/src/helpers/index.ts | 3 + .../rdx-utils/src/helpers/money-helper.ts | 32 + .../rdx-utils/src/helpers/number-helper.ts | 60 ++ packages/rdx-utils/src/index.ts | 1 - packages/rdx-utils/src/types.ts | 3 - packages/rdx-utils/tsconfig.json | 5 +- packages/shadcn-ui/src/components/index.tsx | 9 +- pnpm-lock.yaml | 32 +- 113 files changed, 1387 insertions(+), 4511 deletions(-) delete mode 100644 modules/core/src/common/helpers/dto-compare-helper.ts delete mode 100644 modules/core/src/common/helpers/money-helper.ts delete mode 100644 modules/core/src/common/helpers/number-helper.ts delete mode 100644 modules/core/src/web/hooks/use-datasource/use-list.ts delete mode 100644 modules/core/src/web/hooks/use-datasource/use-loading-overtime.ts delete mode 100644 modules/core/src/web/hooks/use-money-dto.ts delete mode 100644 modules/core/src/web/hooks/use-money.ts delete mode 100644 modules/core/src/web/hooks/use-pagination/index.ts delete mode 100644 modules/core/src/web/hooks/use-pagination/use-pagination-sync-with-location.tsx delete mode 100644 modules/core/src/web/hooks/use-pagination/use-pagination.test.ts delete mode 100644 modules/core/src/web/hooks/use-pagination/use-pagination.tsx delete mode 100644 modules/core/src/web/hooks/use-percentage.ts delete mode 100644 modules/core/src/web/hooks/use-quantity.ts delete mode 100644 modules/core/src/web/hooks/use-query-key/index.ts delete mode 100644 modules/core/src/web/hooks/use-query-key/key-builder.ts delete mode 100644 modules/core/src/web/hooks/use-toggle.ts delete mode 100644 modules/customer-invoices/src/web/hooks/calcs/index.ts delete mode 100644 modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-items-totals.ts delete mode 100644 modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-totals.ts delete mode 100644 modules/customer-invoices/src/web/hooks/calcs/use-customer-invoice-item-summary.ts delete mode 100644 modules/customer-invoices/src/web/hooks/calcs/use-proforma-auto-recalc.ts delete mode 100644 modules/customer-invoices/src/web/pages/create/create-customer-invoice-edit-form.tsx delete mode 100644 modules/customer-invoices/src/web/pages/create/create-customer-invoice-page.tsx delete mode 100644 modules/customer-invoices/src/web/pages/create/customer-invoice.schema.ts delete mode 100644 modules/customer-invoices/src/web/pages/create/index.ts delete mode 100644 modules/customer-invoices/src/web/pages/create/utils.ts delete mode 100644 modules/customer-invoices/src/web/pages/index.ts delete mode 100644 modules/customer-invoices/src/web/pages/list/index.ts delete mode 100644 modules/customer-invoices/src/web/pages/list/invoice-preview-panel.tsx delete mode 100644 modules/customer-invoices/src/web/pages/list/invoices-list-page.tsx create mode 100644 modules/customer-invoices/src/web/proformas/list/ui/pages/orders-table.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/ui/blocks/index.ts delete mode 100644 modules/customer-invoices/src/web/proformas/ui/blocks/proforma-tax-summary.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/ui/components/amount-input-field.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/ui/components/amount-input.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/ui/components/index.ts delete mode 100644 modules/customer-invoices/src/web/proformas/ui/components/input-utils.ts delete mode 100644 modules/customer-invoices/src/web/proformas/ui/components/percentage-input-field.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/ui/components/percentage-input.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/ui/components/quantity-input-field.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/ui/components/quantity-input.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/ui/index.ts create mode 100644 modules/customer-invoices/src/web/proformas/update/ui/blocks/line-editor.tsx create mode 100644 modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/update/utils/focus-first-proforma-update-form-error.ts delete mode 100644 modules/customers/src/web/update/utils/focus-first-customer-update-form-error.ts create mode 100644 packages/rdx-ui/src/components/datatable/types.ts create mode 100644 packages/rdx-ui/src/helpers/focus-first-input-form-error.ts rename {modules/core/src/common => packages/rdx-utils/src}/helpers/date-helper.ts (100%) create mode 100644 packages/rdx-utils/src/helpers/money-helper.ts create mode 100644 packages/rdx-utils/src/helpers/number-helper.ts delete mode 100644 packages/rdx-utils/src/types.ts diff --git a/apps/server/package.json b/apps/server/package.json index e745ec48..42478500 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -16,7 +16,7 @@ "@repo/typescript-config": "workspace:*", "@types/bcrypt": "^6.0.0", "@types/cors": "^2.8.19", - "@types/dinero.js": "^2.0.0", + "@types/dinero.js": "^1.9.1", "@types/express": "^4.17.21", "@types/glob": "^9.0.0", "@types/jsonwebtoken": "^9.0.10", diff --git a/apps/web/package.json b/apps/web/package.json index 94c8e10c..cf0cea65 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,7 +18,7 @@ "@biomejs/biome": "^2.4.11", "@tanstack/react-query-devtools": "^5.98.0", "@tailwindcss/postcss": "^4.1.5", - "@types/dinero.js": "^2.0.0", + "@types/dinero.js": "1.9.1", "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/modules/core/package.json b/modules/core/package.json index eb6c49ca..5f9a9086 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "@hookform/devtools": "^4.4.0", - "@types/dinero.js": "^2.0.0", + "@types/dinero.js": "1.9.1", "@types/express": "^4.17.21", "@types/mime-types": "^3.0.1", "@types/react": "^19.2.14", diff --git a/modules/core/src/common/helpers/dto-compare-helper.ts b/modules/core/src/common/helpers/dto-compare-helper.ts deleted file mode 100644 index 5f336600..00000000 --- a/modules/core/src/common/helpers/dto-compare-helper.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { MoneyDTO, PercentageDTO, QuantityDTO } from "../dto"; - -/** MoneyDTO: { value, scale, currency_code } */ -export function areMoneyDTOEqual(a?: MoneyDTO | null, b?: MoneyDTO | null): boolean { - return a?.value === b?.value && a?.scale === b?.scale && a?.currency_code === b?.currency_code; -} - -/** QuantityDTO: { value, scale } */ -export function areQuantityDTOEqual(a?: QuantityDTO | null, b?: QuantityDTO | null): boolean { - return a?.value === b?.value && a?.scale === b?.scale; -} - -/** PercentageDTO: { value, scale } */ -export function arePercentageDTOEqual(a?: PercentageDTO | null, b?: PercentageDTO | null): boolean { - return a?.value === b?.value && a?.scale === b?.scale; -} diff --git a/modules/core/src/common/helpers/index.ts b/modules/core/src/common/helpers/index.ts index 0c35a1d5..cf7e21a3 100644 --- a/modules/core/src/common/helpers/index.ts +++ b/modules/core/src/common/helpers/index.ts @@ -1,8 +1,4 @@ export * from "./apply-validation-error-collection"; -export * from "./date-helper"; -export * from "./dto-compare-helper"; export * from "./money-dto-helper"; -export * from "./money-helper"; -export * from "./number-helper"; export * from "./percentage-dto-helper"; export * from "./quantity-dto-helper"; diff --git a/modules/core/src/common/helpers/money-helper.ts b/modules/core/src/common/helpers/money-helper.ts deleted file mode 100644 index c2099eab..00000000 --- a/modules/core/src/common/helpers/money-helper.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Funciones para manipular valores monetarios numéricos. - */ - -export const formatCurrency = (amount: number, scale = 2, currency = "EUR", locale = "es-ES") => { - return new Intl.NumberFormat(locale, { - style: "currency", - currency, - maximumFractionDigits: scale, - minimumFractionDigits: Number.isInteger(amount) ? 0 : scale, - useGrouping: true, - }).format(amount); -}; - -/** - * 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/number-helper.ts b/modules/core/src/common/helpers/number-helper.ts deleted file mode 100644 index eba54410..00000000 --- a/modules/core/src/common/helpers/number-helper.ts +++ /dev/null @@ -1,7 +0,0 @@ -const toSafeNumber = (value: string | number | null | undefined): number => { - return Number(value ?? 0); -}; - -export const NumberHelper = { - toSafeNumber, -}; diff --git a/modules/core/src/web/hooks/index.ts b/modules/core/src/web/hooks/index.ts index 406831fa..23e4fe07 100644 --- a/modules/core/src/web/hooks/index.ts +++ b/modules/core/src/web/hooks/index.ts @@ -1,11 +1,5 @@ export * from "./use-datasource"; export * from "./use-hook-form"; -export * from "./use-money"; -export * from "./use-pagination"; -export * from "./use-percentage"; -export * from "./use-quantity"; -export * from "./use-query-key"; export * from "./use-rhf-error-focus"; -export * from "./use-toggle"; export * from "./use-unsaved-changes-notifier"; export * from "./use-url-param-id"; diff --git a/modules/core/src/web/hooks/use-datasource/datasource-context.tsx b/modules/core/src/web/hooks/use-datasource/datasource-context.tsx index 4a1dd6ea..8b5d999f 100644 --- a/modules/core/src/web/hooks/use-datasource/datasource-context.tsx +++ b/modules/core/src/web/hooks/use-datasource/datasource-context.tsx @@ -1,5 +1,6 @@ import { createContext, useContext } from "react"; -import { IDataSource } from "../../lib/data-source/datasource.interface"; + +import type { IDataSource } from "../../lib/data-source/datasource.interface"; const DataSourceContext = createContext(null); diff --git a/modules/core/src/web/hooks/use-datasource/use-list.ts b/modules/core/src/web/hooks/use-datasource/use-list.ts deleted file mode 100644 index 95a109cd..00000000 --- a/modules/core/src/web/hooks/use-datasource/use-list.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - QueryFunctionContext, - QueryKey, - UseQueryOptions, - UseQueryResult, - keepPreviousData, - useQuery, -} from "@tanstack/react-query"; - -import { isResponseAListDTO } from "../../../common/dto"; -import { - UseLoadingOvertimeOptionsProps, - UseLoadingOvertimeReturnType, - useLoadingOvertime, -} from "./use-loading-overtime"; - -const DEFAULT_REFETCH_INTERVAL = 2 * 60 * 1000; // 2 minutes -const DEFAULT_STALE_TIME = 60 * 1000; // 1 minute - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export type UseListQueryOptions = { - queryKey: QueryKey; - queryFn: (context: QueryFunctionContext) => Promise; - enabled?: boolean; - refetchInterval?: number | false; - select?: (data: TUseListQueryData) => TUseListQueryData; - queryOptions?: Partial>; -} & UseLoadingOvertimeOptionsProps; - -export type UseListQueryResult = UseQueryResult< - TUseListQueryData, - TUseListQueryError -> & { - isEmpty: boolean; -} & UseLoadingOvertimeReturnType; - -/** - * Hook para manejar consultas de listas con React Query, - * incluye detección de listas vacías y control de sobretiempo de carga. - */ -export const useList = ({ - queryKey, - queryFn, - enabled, - refetchInterval, - select, - queryOptions = {}, - overtimeOptions, -}: UseListQueryOptions): UseListQueryResult< - TUseListQueryData, - TUseListQueryError -> => { - if (!queryFn) { - console.error("queryFn es requerido en useList"); - throw new Error("queryFn es requerido en useList"); - } - - const queryResponse = useQuery({ - queryKey, - queryFn, - placeholderData: keepPreviousData, - staleTime: DEFAULT_STALE_TIME, - refetchInterval: refetchInterval ?? DEFAULT_REFETCH_INTERVAL, - refetchOnWindowFocus: true, - enabled: enabled && !!queryFn, - select, - ...queryOptions, - }); - - const { elapsedTime } = useLoadingOvertime({ - isPending: queryResponse.isFetching, - interval: overtimeOptions?.interval, - onInterval: overtimeOptions?.onInterval, - }); - - const isEmpty = - queryResponse.isSuccess && - isResponseAListDTO(queryResponse.data) && - queryResponse.data.total_items === 0; - - const result = { - ...queryResponse, - overtime: { elapsedTime }, - isEmpty, - }; - return result; -}; diff --git a/modules/core/src/web/hooks/use-datasource/use-loading-overtime.ts b/modules/core/src/web/hooks/use-datasource/use-loading-overtime.ts deleted file mode 100644 index b23487cb..00000000 --- a/modules/core/src/web/hooks/use-datasource/use-loading-overtime.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { useEffect, useState } from "react"; - -export type UseLoadingOvertimeRefineContext = Omit< - UseLoadingOvertimeCoreProps, - "isPending" | "interval" -> & - Required>; - -export type UseLoadingOvertimeOptionsProps = { - overtimeOptions?: UseLoadingOvertimeCoreOptions; -}; - -export type UseLoadingOvertimeReturnType = { - overtime: { - elapsedTime?: number; - }; -}; - -type UseLoadingOvertimeCoreOptions = Omit; - -type UseLoadingOvertimeCoreReturnType = { - elapsedTime?: number; -}; - -export type UseLoadingOvertimeCoreProps = { - /** - * The pengind state. If true, the elapsed time will be calculated. - */ - isPending: boolean; - - /** - * The interval in milliseconds. If the pending time exceeds this time, the `onInterval` callback will be called. - * If not specified, the `interval` value from the `overtime` option of the `RefineProvider` will be used. - * - * @default: 1000 (1 second) - */ - interval?: number; - - /** - * The callback function that will be called when the pending time exceeds the specified time. - * If not specified, the `onInterval` value from the `overtime` option of the `RefineProvider` will be used. - * - * @param elapsedInterval The elapsed time in milliseconds. - */ - onInterval?: (elapsedInterval: number) => void; -}; - -/** - * if you need to do something when the loading time exceeds the specified time, refine provides the `useLoadingOvertime` hook. - * It returns the elapsed time in milliseconds. - * - * @example - * const { elapsedTime } = useLoadingOvertime({ - * isLoading, - * interval: 1000, - * onInterval(elapsedInterval) { - * console.log("loading overtime", elapsedInterval); - * }, - * }); - */ -export const useLoadingOvertime = ({ - isPending, - interval = 1000, - onInterval, -}: UseLoadingOvertimeCoreProps): UseLoadingOvertimeCoreReturnType => { - const [elapsedTime, setElapsedTime] = useState(undefined); - - useEffect(() => { - let intervalFn: ReturnType; - - if (isPending) { - intervalFn = setInterval(() => { - // increase elapsed time - setElapsedTime((prevElapsedTime) => { - if (prevElapsedTime === undefined) { - return interval; - } - - return prevElapsedTime + interval; - }); - }, interval); - } - - return () => { - clearInterval(intervalFn); - // reset elapsed time - setElapsedTime(undefined); - }; - }, [isPending, interval]); - - useEffect(() => { - // call onInterval callback - if (onInterval && elapsedTime) { - onInterval(elapsedTime); - } - }, [onInterval, elapsedTime]); - - return { - elapsedTime, - }; -}; diff --git a/modules/core/src/web/hooks/use-money-dto.ts b/modules/core/src/web/hooks/use-money-dto.ts deleted file mode 100644 index 2fcb9f72..00000000 --- a/modules/core/src/web/hooks/use-money-dto.ts +++ /dev/null @@ -1,101 +0,0 @@ -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 deleted file mode 100644 index 3e80fbfe..00000000 --- a/modules/core/src/web/hooks/use-money.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { type MoneyDTO, MoneyDTOHelper } from "@erp/core/common"; -import type { Currency } from "dinero.js"; -import * as React from "react"; - -import { useTranslation } from "../i18n"; - -export type { Currency }; - -// --- Utils locales (edición texto → número) --- - -// Quita símbolos de moneda/letras, conserva dígitos, signo y , . -const stripCurrencySymbols = (s: string) => - s - .replace(/[^\d.,-]/g, "") - .replace(/\s+/g, " ") - .trim(); - -// Heurística robusta: determina decimal por última ocurrencia de , o . -const parseLocaleNumber = (raw: string, locale = "es-ES"): number | null => { - if (!raw) return null; - - const s = stripCurrencySymbols(raw); - if (!s) return null; - - // Obtener formato local - const example = Intl.NumberFormat(locale).format(1000.1); - const group = example.charAt(1); // normalmente "." o "," - const decimal = example.charAt(example.length - 2); // normalmente "," o "." - - // Normalizar: eliminar separador de miles y reemplazar decimal por "." - const normalized = s - .replace(new RegExp(`\\${group}`, "g"), "") - .replace(new RegExp(`\\${decimal}`), "."); - - const n = Number(normalized); - return Number.isFinite(n) ? n : null; -}; - -// Redondeo a escala (por defecto 2) -const roundToScale = (n: number, scale = 2) => { - const f = 10 ** scale; - return Math.round(n * f) / f; -}; - -// DTO vacío (API puede mandar "", "") -const isEmptyMoneyDTO = (m?: MoneyDTO | null) => - !m || m.value?.trim?.() === "" || m.scale?.trim?.() === ""; - -// Convierte DTO→número sin instanciar dinero.js (solo lectura) -const toNumberUnsafe = (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 número→DTO (sin costo extra) -const fromNumberUnsafe = (n: number, currency: Currency = "EUR", scale = 2): MoneyDTO => ({ - value: String(Math.round(n * 10 ** scale)), - scale: String(scale), - currency_code: currency, -}); - -// --- Hook --- - -export function useMoney(overrides?: { - locale?: string; // e.g. "es-ES" (si no, i18n.language) - fallbackCurrency?: Currency; // por defecto "EUR" - defaultScale?: number; // por defecto 2 -}) { - const { i18n } = useTranslation(); - const locale = overrides?.locale || i18n.language || "es-ES"; - const fallbackCurrency: Currency = overrides?.fallbackCurrency ?? "EUR"; - const defaultScale = overrides?.defaultScale ?? 2; - - // Conversión básica - 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] - ); - - // Reescala manteniendo magnitud - const withScale = React.useCallback( - (dto: MoneyDTO, scale: number) => { - const curr = toNumber(dto); - return fromNumber(curr, (dto.currency_code as Currency) || fallbackCurrency, scale); - }, - [toNumber, fromNumber, fallbackCurrency] - ); - - // Formateos - const formatCurrency = React.useCallback( - (dto: MoneyDTO, loc?: string) => MoneyDTOHelper.format(dto, loc ?? locale), - [locale] - ); - - const formatPlain = React.useCallback( - (dto: MoneyDTO, loc?: string) => { - const n = toNumber(dto); - const dec = Number(dto?.scale || defaultScale); - return new Intl.NumberFormat(loc ?? locale, { - maximumFractionDigits: dec, - minimumFractionDigits: Number.isInteger(n) ? 0 : 0, - useGrouping: true, - }).format(n); - }, - [locale, toNumber, defaultScale] - ); - - const parse = React.useCallback( - (text: string): number | null => parseLocaleNumber(text, locale), - [locale] - ); - - // Adaptadores API - const fromApi = React.useCallback( - (m?: MoneyDTO | null): MoneyDTO | null => (m == null || isEmptyMoneyDTO(m) ? null : m), - [] - ); - const toApi = React.useCallback( - (m: MoneyDTO | null, currency: Currency = fallbackCurrency): MoneyDTO => - m ? m : { value: "", scale: "", currency_code: currency }, - [fallbackCurrency] - ); - - /* // Operaciones (dinero.js via helpers) - 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] - ); */ - - // Estado/Comparaciones - const isZero = React.useCallback((dto?: MoneyDTO | null) => toNumber(dto) === 0, [toNumber]); - const sameCurrency = React.useCallback( - (a?: MoneyDTO | null, b?: MoneyDTO | null) => - (a?.currency_code || fallbackCurrency) === (b?.currency_code || fallbackCurrency), - [fallbackCurrency] - ); - - // Stepping teclado con redondeo a escala - const stepNumber = React.useCallback( - (base: number, step = 0.01, scale = defaultScale) => roundToScale(base + step, scale), - [defaultScale] - ); - - return React.useMemo( - () => ({ - // Conversión - toNumber, - fromNumber, - withScale, - - // Formateo/parseo - formatCurrency, - formatPlain, - parse, - - // DTO vacío / adaptadores - isEmptyMoneyDTO, - fromApi, - toApi, - - // Operaciones - //add, - //sub, - //multiply, - //percentage, - - // Estado/ayudas - isZero, - sameCurrency, - stepNumber, - roundToScale, - - // Utils UI - stripCurrencySymbols, - - // Config efectiva - locale, - fallbackCurrency, - defaultScale, - // Factory Dinero si se necesita en algún punto de bajo nivel: - //toDinero: (dto: MoneyDTO) => dineroFromDTO(dto, fallbackCurrency), - //fromDinero: dtoFromDinero, - }), - [ - toNumber, - fromNumber, - withScale, - formatCurrency, - formatPlain, - parse, - fromApi, - toApi, - //add, - //sub, - //multiply, - //percentage, - isZero, - sameCurrency, - stepNumber, - locale, - fallbackCurrency, - defaultScale, - ] - ); -} diff --git a/modules/core/src/web/hooks/use-pagination/index.ts b/modules/core/src/web/hooks/use-pagination/index.ts deleted file mode 100644 index ea7fd9a6..00000000 --- a/modules/core/src/web/hooks/use-pagination/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./use-pagination"; -export * from "./use-pagination-sync-with-location"; diff --git a/modules/core/src/web/hooks/use-pagination/use-pagination-sync-with-location.tsx b/modules/core/src/web/hooks/use-pagination/use-pagination-sync-with-location.tsx deleted file mode 100644 index b361b803..00000000 --- a/modules/core/src/web/hooks/use-pagination/use-pagination-sync-with-location.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { - INITIAL_PAGE_INDEX, - INITIAL_PAGE_SIZE, - MAX_PAGE_SIZE, - MIN_PAGE_SIZE, -} from "@repo/rdx-criteria"; - -import { useMemo } from "react"; -import { useSearchParams } from "react-router-dom"; -import { usePagination } from "./use-pagination"; - -export const usePaginationSyncWithLocation = ( - initialPageIndex: number = INITIAL_PAGE_INDEX, - initialPageSize: number = INITIAL_PAGE_SIZE -) => { - const [urlSearchParams, setUrlSearchParams] = useSearchParams(); - - const urlParamPageIndex: string | null = urlSearchParams.get("page_index"); - const urlParamPageSize: string | null = urlSearchParams.get("page_size"); - - const calculatedPageIndex = useMemo(() => { - const parsedPageIndex = Number.parseInt(urlParamPageIndex ?? "", 10); - let result = !Number.isNaN(parsedPageIndex) ? parsedPageIndex : initialPageIndex; - - if (result < initialPageIndex) { - result = initialPageIndex; - } - - return result; - }, [urlParamPageIndex, initialPageIndex]); - - const calculatedPageSize = useMemo(() => { - const parsedPageSize = Number.parseInt(urlParamPageSize ?? "", 10); - let result = !Number.isNaN(parsedPageSize) ? parsedPageSize : initialPageSize; - if (result < MIN_PAGE_SIZE || result > MAX_PAGE_SIZE) { - result = initialPageSize; - } - return result; - }, [urlParamPageSize, initialPageSize]); - - const [pagination, setPagination] = usePagination(calculatedPageIndex, calculatedPageSize); - - const updatePagination = (newPagination: any) => { - const _validatedPagination = setPagination(newPagination); - - setUrlSearchParams({ - //...actualSearchParam, - page_index: String(_validatedPagination.pageIndex), - page_size: String(_validatedPagination.pageSize), - }); - }; - - return [pagination, updatePagination] as const; -}; diff --git a/modules/core/src/web/hooks/use-pagination/use-pagination.test.ts b/modules/core/src/web/hooks/use-pagination/use-pagination.test.ts deleted file mode 100644 index 6d869ff5..00000000 --- a/modules/core/src/web/hooks/use-pagination/use-pagination.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - INITIAL_PAGE_INDEX, - INITIAL_PAGE_SIZE, - MAX_PAGE_SIZE, - MIN_PAGE_SIZE, -} from "@repo/rdx-criteria"; - -import { act, renderHook } from "@testing-library/react-hooks"; - -import { PaginationState, usePagination } from "./use-pagination"; - -describe("usePagination", () => { - it("should initialize with default values", () => { - const { result } = renderHook(() => usePagination()); - const [pagination] = result.current; - - expect(pagination.pageIndex).toBe(INITIAL_PAGE_INDEX); - expect(pagination.pageSize).toBe(INITIAL_PAGE_SIZE); - }); - - it("should initialize with custom values", () => { - const customPageIndex = 2; - const customPageSize = 20; - const { result } = renderHook(() => usePagination(customPageIndex, customPageSize)); - const [pagination] = result.current; - - expect(pagination.pageIndex).toBe(customPageIndex); - expect(pagination.pageSize).toBe(customPageSize); - }); - - it("should update pagination state correctly", () => { - const { result } = renderHook(() => usePagination()); - const [pagination, updatePagination] = result.current; - - const newPagination: PaginationState = { pageIndex: 1, pageSize: 25 }; - - act(() => { - updatePagination(newPagination); - }); - - expect(pagination.pageIndex).toBe(newPagination.pageIndex); - expect(pagination.pageSize).toBe(newPagination.pageSize); - }); - - it("should not update pageIndex below INITIAL_PAGE_INDEX", () => { - const { result } = renderHook(() => usePagination()); - const [, updatePagination] = result.current; - - act(() => { - updatePagination({ pageIndex: -1, pageSize: INITIAL_PAGE_SIZE }); - }); - - const [pagination] = result.current; - - expect(pagination.pageIndex).toBe(INITIAL_PAGE_INDEX); - }); - - it("should not update pageSize below MIN_PAGE_SIZE or above MAX_PAGE_SIZE", () => { - const { result } = renderHook(() => usePagination()); - const [, updatePagination] = result.current; - - act(() => { - updatePagination({ pageIndex: INITIAL_PAGE_INDEX, pageSize: MIN_PAGE_SIZE - 1 }); - }); - - let [pagination] = result.current; - expect(pagination.pageSize).toBe(INITIAL_PAGE_SIZE); // No cambia porque pageSize es menor que el mínimo - - act(() => { - updatePagination({ pageIndex: INITIAL_PAGE_INDEX, pageSize: MAX_PAGE_SIZE + 1 }); - }); - - [pagination] = result.current; - expect(pagination.pageSize).toBe(INITIAL_PAGE_SIZE); // No cambia porque pageSize es mayor que el máximo - }); -}); diff --git a/modules/core/src/web/hooks/use-pagination/use-pagination.tsx b/modules/core/src/web/hooks/use-pagination/use-pagination.tsx deleted file mode 100644 index decd87dd..00000000 --- a/modules/core/src/web/hooks/use-pagination/use-pagination.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - INITIAL_PAGE_INDEX, - INITIAL_PAGE_SIZE, - MAX_PAGE_SIZE, - MIN_PAGE_SIZE, -} from "@repo/rdx-criteria"; -import { useState } from "react"; - -export const DEFAULT_PAGE_SIZES = [5, 10, 15, 30, 50, 75, 100]; - -export interface PaginationState { - pageIndex: number; - pageSize: number; -} - -export const defaultPaginationState = { - pageIndex: INITIAL_PAGE_INDEX, - pageSize: INITIAL_PAGE_SIZE, -}; - -export const usePagination = ( - initialPageIndex: number = INITIAL_PAGE_INDEX, - initialPageSize: number = INITIAL_PAGE_SIZE -) => { - const [pagination, setPagination] = useState({ - pageIndex: initialPageIndex, - pageSize: initialPageSize, - }); - - const updatePagination = (newPagination: PaginationState) => { - // Realiza comprobaciones antes de actualizar el estado - const validatedPagination = newPagination; - - if (validatedPagination.pageIndex < INITIAL_PAGE_INDEX) { - validatedPagination.pageIndex = INITIAL_PAGE_INDEX; - } - - if (newPagination.pageSize < MIN_PAGE_SIZE || newPagination.pageSize > MAX_PAGE_SIZE) { - validatedPagination.pageSize = MIN_PAGE_SIZE; - } - - setPagination((oldPagination) => ({ - ...oldPagination, - pageIndex: newPagination.pageIndex, - pageSize: newPagination.pageSize, - })); - - return validatedPagination; - }; - - return [pagination, updatePagination] as const; -}; diff --git a/modules/core/src/web/hooks/use-percentage.ts b/modules/core/src/web/hooks/use-percentage.ts deleted file mode 100644 index f90fb976..00000000 --- a/modules/core/src/web/hooks/use-percentage.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useMemo } from "react"; -import { PercentageDTO } from "../../common"; - -/** - * Hook para porcentajes escalados (value+scale). - */ -export function usePercentage() { - const toNumber = (p?: PercentageDTO | null): number => { - if (!p?.value || !p.scale) return 0; - return Number(p.value) / 10 ** Number(p.scale); - }; - - const fromNumber = (num: number, scale = 2): PercentageDTO => ({ - value: Math.round(num * 10 ** scale).toString(), - scale: scale.toString(), - }); - - const format = (p: PercentageDTO): string => - `${toNumber(p).toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}%`; - - // biome-ignore lint/correctness/useExhaustiveDependencies: - return useMemo(() => ({ toNumber, fromNumber, format }), []); -} diff --git a/modules/core/src/web/hooks/use-quantity.ts b/modules/core/src/web/hooks/use-quantity.ts deleted file mode 100644 index 4f167ea9..00000000 --- a/modules/core/src/web/hooks/use-quantity.ts +++ /dev/null @@ -1,206 +0,0 @@ -import * as React from "react"; -import { QuantityDTO } from "../../common"; - -/** - * Hook para manipular cantidades escaladas (value+scale). - * Ejemplo: { value:"1500", scale:"2" } → 15.00 unidades - */ -export const isEmptyQuantityDTO = (q?: QuantityDTO | null) => - !q || q.value.trim() === "" || q.scale.trim() === ""; - -// Redondeo a escala (por defecto 2) -const roundToScale = (n: number, scale = 2) => { - const f = 10 ** scale; - return Math.round(n * f) / f; -}; - -// Quita caracteres no numéricos salvo signos y separadores -const stripNumberish = (s: string) => s.replace(/[^\d.,\-]/g, "").trim(); - -// Parse tolerante: “1.234,5” | “1,234.5” | “1234.5” → número JS -const parseLocaleNumber = (raw: string): number | null => { - if (!raw) return null; - const s = stripNumberish(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(",", "."); - const n = Number(normalized); - return Number.isFinite(n) ? n : null; -}; - -export function useQuantity(overrides?: { - defaultScale?: number; // por defecto 2 - min?: number; // clamp opcional (ej. 0) - max?: number; // clamp opcional -}) { - const defaultScale = overrides?.defaultScale ?? 2; - const min = overrides?.min; - const max = overrides?.max; - - // DTO → número (ej. {100, "2"} → 1) - const toNumber = React.useCallback( - (q?: QuantityDTO | null): number => { - if (isEmptyQuantityDTO(q)) return 0; - const scale = Number(q!.scale || defaultScale); - return Number(q!.value || 0) / 10 ** scale; - }, - [defaultScale] - ); - - // número → DTO (manteniendo escala deseada) - const fromNumber = React.useCallback( - (n: number, scale: number = defaultScale): QuantityDTO => ({ - value: String(Math.round(n * 10 ** scale)), - scale: String(scale), - }), - [defaultScale] - ); - - // Reescala manteniendo magnitud - const withScale = React.useCallback( - (q: QuantityDTO, scale: number) => { - const curr = toNumber(q); - return fromNumber(curr, scale); - }, - [toNumber, fromNumber] - ); - - // Formateo sin relleno de ceros (máx. escala) - const formatPlain = React.useCallback( - (q: QuantityDTO) => { - const n = toNumber(q); - const dec = Number(q.scale || defaultScale); - return new Intl.NumberFormat(undefined, { - maximumFractionDigits: dec, - minimumFractionDigits: Number.isInteger(n) ? 0 : 0, - useGrouping: false, - }).format(n); - }, - [toNumber, defaultScale] - ); - - // Parse texto → número (tolerante ,/.) - const parse = React.useCallback((text: string): number | null => parseLocaleNumber(text), []); - - // DTO vacío ↔ null (adaptadores) - const fromApi = React.useCallback( - (q?: QuantityDTO | null): QuantityDTO | null => (q && !isEmptyQuantityDTO(q) ? q : null), - [] - ); - const toApi = React.useCallback( - (q: QuantityDTO | null): QuantityDTO => (q ? q : { value: "", scale: "" }), - [] - ); - - // Operaciones aritméticas simples (misma escala resultado) - const add = React.useCallback( - (a: QuantityDTO, b: QuantityDTO): QuantityDTO => { - const scale = Math.max(Number(a.scale || defaultScale), Number(b.scale || defaultScale)); - const av = withScale(a, scale); - const bv = withScale(b, scale); - return { value: String(Number(av.value) + Number(bv.value)), scale: String(scale) }; - }, - [withScale, defaultScale] - ); - - const sub = React.useCallback( - (a: QuantityDTO, b: QuantityDTO): QuantityDTO => { - const scale = Math.max(Number(a.scale || defaultScale), Number(b.scale || defaultScale)); - const av = withScale(a, scale); - const bv = withScale(b, scale); - return { value: String(Number(av.value) - Number(bv.value)), scale: String(scale) }; - }, - [withScale, defaultScale] - ); - - const multiply = React.useCallback( - (q: QuantityDTO, k: number, outScale?: number): QuantityDTO => { - const sc = outScale ?? Number(q.scale || defaultScale); - const n = toNumber(q) * k; - return fromNumber(roundToScale(n, sc), sc); - }, - [toNumber, fromNumber, defaultScale] - ); - - // Stepping teclado (p.ej. ArrowUp/Down) - const stepNumber = React.useCallback( - (base: number, step = 1, scale: number = defaultScale) => { - let next = base + step; - if (min !== undefined) next = Math.max(next, min); - if (max !== undefined) next = Math.min(next, max); - return roundToScale(next, scale); - }, - [defaultScale, min, max] - ); - - // Estado/ayudas - const clamp = React.useCallback( - (n: number, s: number = defaultScale) => { - let v = n; - if (min !== undefined) v = Math.max(v, min); - if (max !== undefined) v = Math.min(v, max); - return roundToScale(v, s); - }, - [min, max, defaultScale] - ); - - const isZero = React.useCallback((q?: QuantityDTO | null) => toNumber(q) === 0, [toNumber]); - - return React.useMemo( - () => ({ - // Conversión - toNumber, - fromNumber, - withScale, - - // Formateo/parseo - formatPlain, - parse, - - // DTO vacío / adaptadores - isEmptyQuantityDTO, - fromApi, - toApi, - - // Operaciones - add, - sub, - multiply, - - // Teclado/estado - stepNumber, - roundToScale, - clamp, - isZero, - - // Utils UI - stripNumberish, - - // Config efectiva - defaultScale, - min, - max, - }), - [ - toNumber, - fromNumber, - withScale, - formatPlain, - parse, - add, - sub, - multiply, - stepNumber, - clamp, - isZero, - defaultScale, - min, - max, - ] - ); -} diff --git a/modules/core/src/web/hooks/use-query-key/index.ts b/modules/core/src/web/hooks/use-query-key/index.ts deleted file mode 100644 index ae7a8b41..00000000 --- a/modules/core/src/web/hooks/use-query-key/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { keys } from "./key-builder"; - -export const useQueryKey = () => { - return keys; -}; diff --git a/modules/core/src/web/hooks/use-query-key/key-builder.ts b/modules/core/src/web/hooks/use-query-key/key-builder.ts deleted file mode 100644 index 5f674bda..00000000 --- a/modules/core/src/web/hooks/use-query-key/key-builder.ts +++ /dev/null @@ -1,181 +0,0 @@ -type BaseKey = string | number; - -type ParametrizedDataActions = "list" | "infinite"; -type IdRequiredDataActions = "one" | "report" | "upload"; -type IdsRequiredDataActions = "many"; -type DataMutationActions = - | "custom" - | "customMutation" - | "create" - | "createMany" - | "update" - | "updateMany" - | "delete" - | "deleteMany"; - -type AuthActionType = - | "login" - | "logout" - | "profile" - | "register" - | "forgotPassword" - | "check" - | "onError" - | "permissions" - | "updatePassword"; - -type AuditActionType = "list" | "log" | "rename"; - -type IdType = BaseKey; -type IdsType = IdType[]; - -type ParamsType = any; - -type KeySegment = string | IdType | IdsType | ParamsType; - -export function arrayFindIndex(array: T[], slice: T[]): number { - return array.findIndex( - (_, index) => - index <= array.length - slice.length && - slice.every((sliceItem, sliceIndex) => array[index + sliceIndex] === sliceItem) - ); -} - -export function arrayReplace(array: T[], partToBeReplaced: T[], newPart: T[]): T[] { - const newArray: T[] = [...array]; - const startIndex = arrayFindIndex(array, partToBeReplaced); - - if (startIndex !== -1) { - newArray.splice(startIndex, partToBeReplaced.length, ...newPart); - } - - return newArray; -} - -export function stripUndefined(segments: KeySegment[]) { - return segments.filter((segment) => segment !== undefined); -} - -class BaseKeyBuilder { - segments: KeySegment[] = []; - - constructor(segments: KeySegment[] = []) { - this.segments = segments; - } - - key() { - return this.segments; - } - - get() { - return this.segments; - } -} - -class ParamsKeyBuilder extends BaseKeyBuilder { - params(paramsValue?: ParamsType) { - return new BaseKeyBuilder([...this.segments, paramsValue]); - } -} - -class DataIdRequiringKeyBuilder extends BaseKeyBuilder { - id(idValue?: IdType) { - return new ParamsKeyBuilder([...this.segments, idValue ? String(idValue) : undefined]); - } -} - -class DataIdsRequiringKeyBuilder extends BaseKeyBuilder { - ids(...idsValue: IdsType) { - return new ParamsKeyBuilder([ - ...this.segments, - ...(idsValue.length ? [idsValue.map((el) => String(el))] : []), - ]); - } -} - -class DataResourceKeyBuilder extends BaseKeyBuilder { - action(actionType: ParametrizedDataActions): ParamsKeyBuilder; - action(actionType: IdRequiredDataActions): DataIdRequiringKeyBuilder; - action(actionType: IdsRequiredDataActions): DataIdsRequiringKeyBuilder; - action( - actionType: ParametrizedDataActions | IdRequiredDataActions | IdsRequiredDataActions - ): ParamsKeyBuilder | DataIdRequiringKeyBuilder | DataIdsRequiringKeyBuilder { - if (["one", "report"].includes(actionType)) { - return new DataIdRequiringKeyBuilder([...this.segments, actionType]); - } - if (actionType === "many") { - return new DataIdsRequiringKeyBuilder([...this.segments, actionType]); - } - if (["list", "infinite"].includes(actionType)) { - return new ParamsKeyBuilder([...this.segments, actionType]); - } - throw new Error("Invalid action type"); - } -} - -class DataKeyBuilder extends BaseKeyBuilder { - resource(resourceName?: string) { - return new DataResourceKeyBuilder([...this.segments, resourceName]); - } - - mutation(mutationName: DataMutationActions) { - return new ParamsKeyBuilder([ - ...(mutationName === "custom" ? this.segments : [this.segments[0]]), - mutationName, - ]); - } -} - -class AuthKeyBuilder extends BaseKeyBuilder { - action(actionType: AuthActionType) { - return new ParamsKeyBuilder([...this.segments, actionType]); - } -} - -class AccessResourceKeyBuilder extends BaseKeyBuilder { - action(resourceName: string) { - return new ParamsKeyBuilder([...this.segments, resourceName]); - } -} - -class AccessKeyBuilder extends BaseKeyBuilder { - resource(resourceName?: string) { - return new AccessResourceKeyBuilder([...this.segments, resourceName]); - } -} - -class AuditActionKeyBuilder extends BaseKeyBuilder { - action(actionType: Extract) { - return new ParamsKeyBuilder([...this.segments, actionType]); - } -} - -class AuditKeyBuilder extends BaseKeyBuilder { - resource(resourceName?: string) { - return new AuditActionKeyBuilder([...this.segments, resourceName]); - } - - action(actionType: Extract) { - return new ParamsKeyBuilder([...this.segments, actionType]); - } -} - -export class KeyBuilder extends BaseKeyBuilder { - data(name?: string) { - return new DataKeyBuilder(["data", name || "default"]); - } - - auth() { - return new AuthKeyBuilder(["auth"]); - } - - access() { - return new AccessKeyBuilder(["access"]); - } - - audit() { - return new AuditKeyBuilder(["audit"]); - } -} - -export const keys = () => new KeyBuilder([]); diff --git a/modules/core/src/web/hooks/use-toggle.ts b/modules/core/src/web/hooks/use-toggle.ts deleted file mode 100644 index 6705ce2b..00000000 --- a/modules/core/src/web/hooks/use-toggle.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useState } from "react"; - -// https://github.com/wojtekmaj/react-hooks/blob/main/src/useToggle.ts - -/** - * Returns a flag and a function to toggle it. - * - * @param {boolean} defaultValue Default value - * @returns {[boolean, () => void]} Flag and toggle function - */ -export default function useToggle(defaultValue = false): [boolean, () => void] { - const [value, setValue] = useState(defaultValue); - const toggleValue = () => setValue((prevValue) => !prevValue); - return [value, toggleValue]; -} diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index 2eb446f5..5d76875b 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@hookform/devtools": "^4.4.0", - "@types/dinero.js": "^2.0.0", + "@types/dinero.js": "1.9.1", "@types/express": "^4.17.21", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/get-issued-invoice-by-id.controller.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/get-issued-invoice-by-id.controller.ts index e7198c74..9cfa21e5 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/get-issued-invoice-by-id.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/get-issued-invoice-by-id.controller.ts @@ -7,12 +7,12 @@ import { import { GetIssuedInvoiceByIdResponseSchema } from "../../../../../common/index.ts"; import type { GetIssuedInvoiceByIdUseCase } from "../../../../application/issued-invoices/index.ts"; -import { proformasApiErrorMapper } from "../../../proformas/express/proformas-api-error-mapper.ts"; +import { issuedInvoicesApiErrorMapper } from "../issued-invoices-api-error-mapper.ts"; export class GetIssuedInvoiceByIdController extends ExpressController { public constructor(private readonly useCase: GetIssuedInvoiceByIdUseCase) { super(); - this.errorMapper = proformasApiErrorMapper; + this.errorMapper = issuedInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query this.registerGuards( diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/list-issued-invoices.controller.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/list-issued-invoices.controller.ts index e4bdd2aa..645bb012 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/list-issued-invoices.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/list-issued-invoices.controller.ts @@ -8,12 +8,12 @@ import { Criteria } from "@repo/rdx-criteria/server"; import { ListIssuedInvoicesResponseSchema } from "../../../../../common/index.ts"; import type { ListIssuedInvoicesUseCase } from "../../../../application/issued-invoices/index.ts"; -import { proformasApiErrorMapper } from "../../../proformas/express/proformas-api-error-mapper.ts"; +import { issuedInvoicesApiErrorMapper } from "../issued-invoices-api-error-mapper.ts"; export class ListIssuedInvoicesController extends ExpressController { public constructor(private readonly useCase: ListIssuedInvoicesUseCase) { super(); - this.errorMapper = proformasApiErrorMapper; + this.errorMapper = issuedInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query this.registerGuards( diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/report-issued-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/report-issued-invoice.controller.ts index ebeed65b..debe2b1b 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/report-issued-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/controllers/report-issued-invoice.controller.ts @@ -8,12 +8,12 @@ import { import type { ReportIssueInvoiceByIdQueryRequestDTO } from "../../../../../common"; import type { ReportIssuedInvoiceUseCase } from "../../../../application/index.ts"; -import { proformasApiErrorMapper } from "../../../proformas/express/proformas-api-error-mapper.ts"; +import { issuedInvoicesApiErrorMapper } from "../issued-invoices-api-error-mapper.ts"; export class ReportIssuedInvoiceController extends ExpressController { public constructor(private readonly useCase: ReportIssuedInvoiceUseCase) { super(); - this.errorMapper = proformasApiErrorMapper; + this.errorMapper = issuedInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query this.registerGuards( diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices-api-error-mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices-api-error-mapper.ts index 3b721e87..ee3c898e 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices-api-error-mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices-api-error-mapper.ts @@ -37,6 +37,6 @@ const issuedinvoiceItemMismatchError: ErrorToApiRule = { }; // Cómo aplicarla: crea una nueva instancia del mapper con la regla extra -export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() +export const issuedInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() .register(invoiceDuplicateRule) .register(issuedinvoiceItemMismatchError); diff --git a/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts index f7c6e9f1..fbcd3520 100644 --- a/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts @@ -9,34 +9,19 @@ import { z } from "zod/v4"; import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared"; -export const UpdateProformaItemRequestSchema = z - .object({ - position: ItemPositionSchema, - is_valued: z.boolean(), +export const UpdateProformaItemRequestSchema = z.object({ + position: ItemPositionSchema, + is_valued: z.boolean(), - description: z.string().nullable(), + description: z.string().nullable(), - quantity: NumericStringSchema.nullable(), - unit_amount: NumericStringSchema.nullable(), + quantity: NumericStringSchema.nullable(), + unit_amount: NumericStringSchema.nullable(), - item_discount_percentage: PercentageSchema.nullable(), + item_discount_percentage: PercentageSchema.nullable(), - taxes: TaxCombinationCodeSchema, - }) - .refine( - (item) => { - if (!item.is_valued) { - return item.quantity === null && item.unit_amount === null; - } - - return item.quantity !== null && item.unit_amount !== null; - }, - { - message: - "quantity and unit_amount must be null when is_valued is false and non-null when is_valued is true", - path: ["is_valued"], - } - ); + taxes: TaxCombinationCodeSchema, +}); export const UpdateProformaByIdParamsRequestSchema = z.object({ proforma_id: z.uuid(), diff --git a/modules/customer-invoices/src/web/hooks/calcs/index.ts b/modules/customer-invoices/src/web/hooks/calcs/index.ts deleted file mode 100644 index d16f97a4..00000000 --- a/modules/customer-invoices/src/web/hooks/calcs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./use-proforma-auto-recalc"; 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 deleted file mode 100644 index 8bffab6f..00000000 --- a/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-items-totals.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { MoneyDTO } from "@erp/core"; -import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks"; -import { useMemo } from "react"; -import { InvoiceItemFormData } from "../../schemas"; - -/** - * Calcula totales derivados de un ítem de factura - */ - -export type InvoiceItemTotals = Readonly<{ - // valores base ya normalizados a number - quantity: number; - unitAmount: number; - discountPercent: number; - - // desgloses numéricos - subtotal: number; // qty * unit - discountAmount: number; // subtotal * (discountPercent/100) - taxableBase: number; // subtotal - discountAmount - taxes: number; // por ahora 0 (o calcula según tax_codes si lo necesitas) - total: number; // taxableBase + taxes - - // equivalentes en MoneyDTO (misma divisa/escala que useMoney) - subtotalDTO: MoneyDTO; - discountAmountDTO: MoneyDTO; - taxableBaseDTO: MoneyDTO; - taxesDTO: MoneyDTO; - totalDTO: MoneyDTO; -}>; -/** - * Calcula totales derivados de una línea de factura usando tus hooks de Money/Quantity/Percentage. - */ -export function useCalcInvoiceItemTotals(item?: InvoiceItemFormData): InvoiceItemTotals { - const moneyHelper = useMoney(); - const qtyHelper = useQuantity(); - const pctHelper = usePercentage(); - - return useMemo(() => { - // valores base - const quantity = item ? qtyHelper.toNumber(item.quantity) : 0; - const unitAmount = item ? moneyHelper.toNumber(item.unit_amount) : 0; - const discountPercent = item ? pctHelper.toNumber(item.discount_percentage) : 0; - - // cálculos - const subtotal = quantity * unitAmount; - const discountAmount = subtotal * (discountPercent / 100); - const taxableBase = subtotal - discountAmount; - - // impuestos (ajústalo si quieres aplicar tax_codes) - const taxes = 0; - - const total = taxableBase + taxes; - - // DTOs - const subtotalDTO = moneyHelper.fromNumber(subtotal); - const discountAmountDTO = moneyHelper.fromNumber(discountAmount); - const taxableBaseDTO = moneyHelper.fromNumber(taxableBase); - const taxesDTO = moneyHelper.fromNumber(taxes); - const totalDTO = moneyHelper.fromNumber(total); - - return { - quantity, - unitAmount, - discountPercent, - subtotal, - discountAmount, - taxableBase, - taxes, - total, - subtotalDTO, - discountAmountDTO, - taxableBaseDTO, - taxesDTO, - totalDTO, - }; - }, [item, moneyHelper, qtyHelper, pctHelper]); -} 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 deleted file mode 100644 index 183d4aea..00000000 --- a/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-totals.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { MoneyDTO } from "@erp/core"; -import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks"; -import { useMemo } from "react"; -import { InvoiceItemFormData } from "../../schemas"; - -export type InvoiceTotals = Readonly<{ - subtotal: number; - discountTotal: number; - taxableBase: number; - taxes: number; - total: number; - - subtotalDTO: MoneyDTO; - discountTotalDTO: MoneyDTO; - taxableBaseDTO: MoneyDTO; - taxesDTO: MoneyDTO; - totalDTO: MoneyDTO; - - // número de líneas válidas consideradas - itemCount: number; -}>; - -/** - * Calcula los totales generales de la factura a partir de sus líneas. - */ -export function useCalcInvoiceTotals(items: InvoiceItemFormData[] | undefined): InvoiceTotals { - const money = useMoney(); - const qty = useQuantity(); - const pct = usePercentage(); - - return useMemo(() => { - if (!items?.length) { - const zero = money.fromNumber(0); - return { - subtotal: 0, - discountTotal: 0, - taxableBase: 0, - taxes: 0, - total: 0, - subtotalDTO: zero, - discountTotalDTO: zero, - taxableBaseDTO: zero, - taxesDTO: zero, - totalDTO: zero, - itemCount: 0, - }; - } - - let subtotal = 0; - let discountTotal = 0; - let taxableBase = 0; - let taxes = 0; - let total = 0; - - for (const item of items) { - const quantity = qty.toNumber(item.quantity); - const unit = money.toNumber(item.unit_amount); - const discountPct = pct.toNumber(item.discount_percentage); - - const lineSubtotal = quantity * unit; - const lineDiscount = lineSubtotal * (discountPct / 100); - const lineTaxable = lineSubtotal - lineDiscount; - const lineTaxes = 0; // ← ajusta si aplicas IVA o impuestos reales - const lineTotal = lineTaxable + lineTaxes; - - subtotal += lineSubtotal; - discountTotal += lineDiscount; - taxableBase += lineTaxable; - taxes += lineTaxes; - total += lineTotal; - } - - return { - subtotal, - discountTotal, - taxableBase, - taxes, - total, - subtotalDTO: money.fromNumber(subtotal), - discountTotalDTO: money.fromNumber(discountTotal), - taxableBaseDTO: money.fromNumber(taxableBase), - taxesDTO: money.fromNumber(taxes), - totalDTO: money.fromNumber(total), - itemCount: items.length, - }; - }, [items, money, qty, pct]); -} diff --git a/modules/customer-invoices/src/web/hooks/calcs/use-customer-invoice-item-summary.ts b/modules/customer-invoices/src/web/hooks/calcs/use-customer-invoice-item-summary.ts deleted file mode 100644 index e2da1177..00000000 --- a/modules/customer-invoices/src/web/hooks/calcs/use-customer-invoice-item-summary.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { MoneyDTO, PercentageDTO, QuantityDTO, SpainTaxCatalogProvider } from "@erp/core"; -import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks"; -import { useMemo } from "react"; - -/** - * Calcula subtotal, descuento, base imponible, impuestos y total de una línea de factura. - * Trabaja con DTOs escalados (value+scale) como los del backend. - */ - -type ItemShape = { - quantity: QuantityDTO | null | undefined; - unit_amount: MoneyDTO | null | undefined; - discount_percentage?: PercentageDTO | null; - tax_codes?: string[] | null; -}; - -// ⚠️ Devuelve todo en MoneyDTO manteniendo moneda/escala del unit_amount -export function useInvoiceItemSummary(item: ItemShape) { - const { - add, - sub, - multiply, - percentage: moneyPct, - fromNumber, - isEmptyMoneyDTO, - fallbackCurrency, - } = useMoney(); - - const { toNumber: qtyToNumber } = useQuantity(); - const { toNumber: pctToNumber } = usePercentage(); - - // Cero monetario con la misma divisa/escala del unit_amount (fallback EUR/2) - const zero = useMemo(() => { - const cur = item.unit_amount?.currency_code ?? fallbackCurrency; - const sc = Number(item.unit_amount?.scale ?? 2); - return fromNumber(0, cur as any, sc); - }, [item.unit_amount?.currency_code, item.unit_amount?.scale, fromNumber, fallbackCurrency]); - - const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []); - - return useMemo(() => { - // 1) Cantidad - const qty = qtyToNumber(item.quantity); // 0 si null/DTO vacío - - // 2) Subtotal = quantity × unit_amount - const unit = item.unit_amount && !isEmptyMoneyDTO(item.unit_amount) ? item.unit_amount : zero; - const subtotal = multiply(unit, qty); // usa dinero.js, respeta escala - - // 3) Descuento - const pctDTO = item.discount_percentage ?? ({ value: "", scale: "" } as PercentageDTO); - const pct = pctToNumber(pctDTO); // 0 si vacío - const discountAmount = pct !== 0 ? moneyPct(subtotal, pct) : zero; - - // 4) Base imponible = subtotal - descuento - const baseAmount = sub(subtotal, discountAmount); - - // 5) Impuestos (cada código es un % sobre base; soporta negativos) - const taxesBreakdown = - item.tax_codes?.map((code) => { - const maybe = taxCatalog.findByCode(code); - if (maybe.isNone()) return { label: code, percentage: 0, amount: zero }; - - const tax = maybe.unwrap()!; // { name, value, scale } - const p = pctToNumber({ value: tax.value, scale: tax.scale }); // ej. 21 → 21% - return { - label: tax.name, - percentage: p, - amount: moneyPct(baseAmount, p), - }; - }) ?? []; - - // 6) Total impuestos = suma amounts - const taxesTotal = taxesBreakdown.reduce((acc, t) => add(acc, t.amount), zero); - - // 7) Total línea = base + impuestos - const total = add(baseAmount, taxesTotal); - - return { - qty, - subtotal, - discountAmount, - baseAmount, - taxesBreakdown, // [{label, percentage, amount}] - taxesTotal, - total, - }; - }, [ - item.quantity, - item.unit_amount, - item.discount_percentage, - item.tax_codes, - qtyToNumber, - pctToNumber, - add, - sub, - multiply, - moneyPct, - isEmptyMoneyDTO, - taxCatalog, - zero, - ]); -} diff --git a/modules/customer-invoices/src/web/hooks/calcs/use-proforma-auto-recalc.ts b/modules/customer-invoices/src/web/hooks/calcs/use-proforma-auto-recalc.ts deleted file mode 100644 index 5095077e..00000000 --- a/modules/customer-invoices/src/web/hooks/calcs/use-proforma-auto-recalc.ts +++ /dev/null @@ -1,191 +0,0 @@ -import type { TaxCatalogProvider } from "@erp/core"; -import React from "react"; -import { type UseFormReturn, useWatch } from "react-hook-form"; - -import { - type InvoiceItemCalcResult, - calculateInvoiceHeaderAmounts, - calculateInvoiceItemAmounts, -} from "../../domain"; -import type { ProformaFormData } from "../../proformas/types"; -import type { InvoiceFormData, InvoiceItemFormData } from "../../schemas"; - -export type UseProformaAutoRecalcParams = { - currency_code: string; - taxCatalog: TaxCatalogProvider; - debug?: boolean; -}; - -/** - * Hook que recalcula automáticamente los totales de cada línea - * y los totales generales de la factura cuando cambian los valores relevantes. - * Adaptado a formulario con números planos (no DTOs). - * Evita renders innecesarios (debounce + useDeferredValue). - */ -export function useProformaAutoRecalc( - form: UseFormReturn, - { currency_code, taxCatalog, debug = true }: UseProformaAutoRecalcParams -) { - const { trigger, control } = form; - - // Observa los ítems y el descuento global - const watchedItems = useWatch({ control, name: "items" }) ?? []; - const watchedDiscount = useWatch({ control, name: "discount_percentage", defaultValue: 0 }); // <- descuento global - - // Diferir valores pesados para reducir renders (React 19) - const deferredItems = React.useDeferredValue(watchedItems); - const deferredDiscount = React.useDeferredValue(watchedDiscount); - - // Cache para evitar recálculos redundantes - const [prevDiscount, setPrevDiscount] = React.useState(watchedDiscount); - const itemCache = React.useRef>(new Map()); - - // Debounce para agrupar recalculados rápidos - const debounceTimer = React.useRef | null>(null); - - // Cálculo de una línea individual - const calculateItemTotals = React.useCallback( - (item: InvoiceItemFormData) => { - const sanitize = (v?: number | string) => (v && !Number.isNaN(Number(v)) ? String(v) : "0"); - - return calculateInvoiceItemAmounts( - { - quantity: sanitize(item.quantity), - unit_amount: sanitize(item.unit_amount), - discount_percentage: sanitize(item.discount_percentage), - tax_codes: item.tax_codes, - }, - currency_code, - taxCatalog - ); - }, - [taxCatalog, currency_code] - ); - - // Cálculo global de factura - const calculateInvoiceTotals = React.useCallback( - (items: InvoiceItemFormData[], header_discount_percentage: number) => { - const lines = items - //.filter((i) => i.is_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, - taxes_summary: totals.taxes_summary, - }; - }); - - return calculateInvoiceHeaderAmounts(lines, header_discount_percentage, currency_code); - }, - [calculateItemTotals, currency_code] - ); - - // Observamos el formulario esperando cualquier cambio - React.useEffect(() => { - if (!deferredItems.length) return; - - if (debounceTimer.current) clearTimeout(debounceTimer.current); - - debounceTimer.current = setTimeout(() => { - let shouldUpdateHeader = false; - - if (prevDiscount !== deferredDiscount) { - shouldUpdateHeader = true; - setPrevDiscount(deferredDiscount); - } - - deferredItems.forEach((item, idx) => { - const prev = itemCache.current.get(idx); - const next = calculateItemTotals(item); - - const itemHasChanges = - !prev || - prev.subtotal_amount !== next.subtotal_amount || - prev.total_amount !== next.total_amount || - prev.taxes_amount !== next.taxes_amount; - - //if (!prev || JSON.stringify(prev) !== JSON.stringify(next)) { <-- Costoso y poco preciso - if (itemHasChanges) { - shouldUpdateHeader = true; - itemCache.current.set(idx, next); - setInvoiceItemTotals(form, idx, next); - if (debug) console.log(`💡 Recalc line ${idx + 1}`, next.total_amount); - } - }); - - if (shouldUpdateHeader) { - const totals = calculateInvoiceTotals(deferredItems, deferredDiscount); - setInvoiceTotals(form, totals); - if (debug) console.log("📊 Recalc invoice totals", totals.subtotal_amount); - - trigger([ - "subtotal_amount", - "discount_amount", - "taxable_amount", - "taxes_amount", - "total_amount", - ]); - } - }, 100); // <-- debounce de 100ms, ajustable - - return () => { - if (debounceTimer.current) clearTimeout(debounceTimer.current); - }; - }, [ - deferredItems, - deferredDiscount, - calculateItemTotals, - calculateInvoiceTotals, - form, - trigger, - debug, - prevDiscount, - ]); -} - -// Ayudante para rellenar los importes de una línea -function setInvoiceItemTotals( - form: UseFormReturn, - index: number, - totals: InvoiceItemCalcResult -) { - const { setValue } = form; - const opts = { shouldDirty: true, shouldValidate: false } as const; - - setValue(`items.${index}.subtotal_amount`, totals.subtotal_amount, opts); - setValue(`items.${index}.discount_amount`, totals.discount_amount, opts); - setValue(`items.${index}.taxable_amount`, totals.taxable_amount, opts); - setValue(`items.${index}.taxes_amount`, totals.taxes_amount, opts); - setValue(`items.${index}.total_amount`, totals.total_amount, opts); -} - -// Ayudante para actualizar los importes de la cabecera -function setInvoiceTotals( - form: UseFormReturn, - totals: ReturnType -) { - const { setValue } = form; - const opts = { shouldDirty: true, shouldValidate: false } as const; - - setValue("subtotal_amount", totals.subtotal_amount, opts); - setValue("items_discount_amount", totals.items_discount_amount, opts); - setValue("discount_amount", totals.discount_amount, opts); - setValue("taxable_amount", totals.taxable_amount, opts); - setValue("taxes_amount", totals.taxes_amount, opts); - setValue("total_amount", totals.total_amount, opts); - - setValue( - "taxes", - totals.taxes_summary.map((t) => ({ - tax_code: t.code, - tax_label: t.name, - taxable_amount: t.taxable_amount, - taxes_amount: t.taxes_amount, - })), - opts - ); -} diff --git a/modules/customer-invoices/src/web/issued-invoices/shared/adapters/list-issued-invoice.adapter.ts b/modules/customer-invoices/src/web/issued-invoices/shared/adapters/list-issued-invoice.adapter.ts index 41fad62c..33ed1f20 100644 --- a/modules/customer-invoices/src/web/issued-invoices/shared/adapters/list-issued-invoice.adapter.ts +++ b/modules/customer-invoices/src/web/issued-invoices/shared/adapters/list-issued-invoice.adapter.ts @@ -1,4 +1,5 @@ -import { MoneyDTOHelper, formatCurrency } from "@erp/core"; +import { MoneyDTOHelper } from "@erp/core"; +import { MoneyHelper } from "@repo/rdx-utils"; import type { ListIssuedInvoicesResponseDTO } from "../../../../common"; import type { IssuedInvoiceList, IssuedInvoiceListRow, IssuedInvoiceStatus } from "../entities"; @@ -50,7 +51,7 @@ const IssuedInvoiceListRowAdapter = { }, subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount), - subtotalAmountFmt: formatCurrency( + subtotalAmountFmt: MoneyHelper.formatCurrency( MoneyDTOHelper.toNumber(dto.subtotal_amount), Number(dto.total_amount.scale || 2), dto.currency_code, @@ -58,7 +59,7 @@ const IssuedInvoiceListRowAdapter = { ), totalDiscountAmount: MoneyDTOHelper.toNumber(dto.total_discount_amount), - totalDiscountAmountFmt: formatCurrency( + totalDiscountAmountFmt: MoneyHelper.formatCurrency( MoneyDTOHelper.toNumber(dto.total_discount_amount), Number(dto.total_amount.scale || 2), dto.currency_code, @@ -66,7 +67,7 @@ const IssuedInvoiceListRowAdapter = { ), taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount), - taxableAmountFmt: formatCurrency( + taxableAmountFmt: MoneyHelper.formatCurrency( MoneyDTOHelper.toNumber(dto.taxable_amount), Number(dto.total_amount.scale || 2), dto.currency_code, @@ -74,7 +75,7 @@ const IssuedInvoiceListRowAdapter = { ), taxesAmount: MoneyDTOHelper.toNumber(dto.taxes_amount), - taxesAmountFmt: formatCurrency( + taxesAmountFmt: MoneyHelper.formatCurrency( MoneyDTOHelper.toNumber(dto.taxes_amount), Number(dto.total_amount.scale || 2), dto.currency_code, @@ -82,7 +83,7 @@ const IssuedInvoiceListRowAdapter = { ), totalAmount: MoneyDTOHelper.toNumber(dto.total_amount), - totalAmountFmt: formatCurrency( + totalAmountFmt: MoneyHelper.formatCurrency( MoneyDTOHelper.toNumber(dto.total_amount), Number(dto.total_amount.scale || 2), dto.currency_code, diff --git a/modules/customer-invoices/src/web/pages/create/create-customer-invoice-edit-form.tsx b/modules/customer-invoices/src/web/pages/create/create-customer-invoice-edit-form.tsx deleted file mode 100644 index 1b7c38a2..00000000 --- a/modules/customer-invoices/src/web/pages/create/create-customer-invoice-edit-form.tsx +++ /dev/null @@ -1,874 +0,0 @@ -import { CustomerModalSelector } from "@erp/customers/components"; -import { DevTool } from "@hookform/devtools"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { TextAreaField, TextField } from "@repo/rdx-ui/components"; -import { - Button, - Calendar, - Card, - CardAction, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - Input, - Label, - Popover, - PopoverContent, - PopoverTrigger, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Separator, - Textarea, -} from "@repo/shadcn-ui/components"; -import { format } from "date-fns"; -import { es } from "date-fns/locale"; -import { CalendarIcon, PlusIcon, Save, Trash2Icon, X } from "lucide-react"; -import { useFieldArray, useForm } from "react-hook-form"; -import * as z from "zod"; - -import { useTranslation } from "../../i18n"; -import { CustomerInvoicePricesCard } from "../../proformas/shared/ui/components"; -import { CustomerInvoiceItemsCardEditor } from "../../proformas/shared/ui/components/items"; - -import type { CustomerInvoiceData } from "./customer-invoice.schema"; -import { formatCurrency } from "./utils"; - -const invoiceFormSchema = z.object({ - id: z.string(), - invoice_status: z.string(), - invoice_number: z.string().min(1, "Número de factura requerido"), - invoice_series: z.string().min(1, "Serie requerida"), - invoice_date: z.string(), - operation_date: z.string(), - language_code: z.string(), - currency: z.string(), - customer_id: z.string().min(1, "ID de cliente requerido"), - items: z - .array( - z.object({ - description: z.string().optional(), - quantity: z - .object({ - amount: z.number().nullable(), - scale: z.number(), - }) - .optional(), - unit_price: z - .object({ - amount: z.number().nullable(), - scale: z.number(), - currency_code: z.string(), - }) - .optional(), - subtotal_price: z - .object({ - amount: z.number().nullable(), - scale: z.number(), - currency_code: z.string(), - }) - .optional(), - discount: z - .object({ - amount: z.number().min(0).max(100).nullable(), - scale: z.number(), - }) - .optional(), - discount_price: z - .object({ - amount: z.number().nullable(), - scale: z.number(), - currency_code: z.string(), - }) - .optional(), - total_price: z - .object({ - amount: z.number().nullable(), - scale: z.number(), - currency_code: z.string(), - }) - .optional(), - }) - ) - .min(1, "Al menos un item es requerido"), - subtotal_price: z.object({ - amount: z.number().nullable(), - scale: z.number(), - currency_code: z.string(), - }), - discount: z.object({ - amount: z.number().nullable(), - scale: z.number(), - }), - discount_price: z.object({ - amount: z.number().nullable(), - scale: z.number(), - currency_code: z.string(), - }), - before_tax_price: z.object({ - amount: z.number().nullable(), - scale: z.number(), - currency_code: z.string(), - }), - tax: z.object({ - amount: z.number().nullable(), - scale: z.number(), - }), - tax_price: z.object({ - amount: z.number().nullable(), - scale: z.number(), - currency_code: z.string(), - }), - total_price: z.object({ - amount: z.number().nullable(), - scale: z.number(), - currency_code: z.string(), - }), - metadata: z.object({ - entity: z.string(), - }), -}); - -const defaultInvoiceData = { - id: "34ae34af-1ffc-4de5-b0a8-c2cf203ef011", - invoice_status: "draft", - invoice_number: "1", - invoice_series: "A", - invoice_date: "2025-04-30T00:00:00.000Z", - operation_date: "2025-04-30T00:00:00.000Z", - description: "", - language_code: "ES", - currency: "EUR", - customer_id: "5e4dc5b3-96b9-4968-9490-14bd032fec5f", - items: [ - { - description: "", - quantity: { - amount: 100, - scale: 2, - }, - - unit_price: { - amount: 100, - scale: 2, - currency_code: "EUR", - }, - subtotal_price: { - amount: 100, - scale: 2, - currency_code: "EUR", - }, - discount: { - amount: 0, - scale: 2, - }, - discount_price: { - amount: 0, - scale: 2, - currency_code: "EUR", - }, - total_price: { - amount: 100, - scale: 2, - currency_code: "EUR", - }, - }, - ], - subtotal_price: { - amount: 0, - scale: 2, - currency_code: "EUR", - }, - discount: { - amount: 0, - scale: 0, - }, - discount_price: { - amount: 0, - scale: 0, - currency_code: "EUR", - }, - before_tax_price: { - amount: 0, - scale: 2, - currency_code: "EUR", - }, - tax: { - amount: 2100, - scale: 2, - }, - tax_price: { - amount: 0, - scale: 2, - currency_code: "EUR", - }, - total_price: { - amount: 0, - scale: 2, - currency_code: "EUR", - }, -}; - -interface InvoiceFormProps { - initialData?: CustomerInvoiceData; - isPending?: boolean; - /** - * Callback function to handle form submission. - * @param data - The invoice data submitted by the form. - */ - onSubmit?: (data: CustomerInvoiceData) => void; -} - -export const CreateCustomerInvoiceEditForm = ({ - initialData = defaultInvoiceData, - onSubmit, - isPending, -}: InvoiceFormProps) => { - const { t } = useTranslation(); - - const form = useForm({ - resolver: zodResolver(invoiceFormSchema), - defaultValues: initialData, - }); - - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: "items", - }); - - const watchedItems = form.watch("items"); - const watchedTaxRate = form.watch("tax.amount"); - - const addItem = () => { - append({ - id_article: "", - description: "", - quantity: { amount: 100, scale: 2 }, - unit_price: { amount: 0, scale: 2, currency_code: form.getValues("currency") }, - subtotal_price: { amount: 0, scale: 2, currency_code: form.getValues("currency") }, - discount: { amount: 0, scale: 2 }, - discount_price: { amount: 0, scale: 2, currency_code: form.getValues("currency") }, - total_price: { amount: 0, scale: 2, currency_code: form.getValues("currency") }, - }); - }; - - const handleSubmit = (data: CustomerInvoiceData) => { - console.log("Datos del formulario:", data); - onSubmit?.(data); - }; - - const handleError = (errors: any) => { - console.error("Errores en el formulario:", errors); - // Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario - }; - - const handleCancel = () => { - form.reset(initialData); - }; - - return ( -
- - - - Cliente - Description - - - - - - - - -
-
-

Radix Primitives

-

- An open-source UI component library. -

-
- -
-
Blog
- -
Docs
- -
Source
-
-
-
- - - - {" "} -
- - {/* Información básica */} - - - Información Básica - Detalles generales de la factura - - -
- - - - - -
-
- -
-
- -
-
-
- - {/* Cliente */} - - - Cliente - - - - - - - - {/*Items */} - - - {/* Items */} - - -
- Artículos - Lista de productos o servicios facturados -
- -
- - {fields.map((field, index) => ( - -
-
-

Item {index + 1}

-
- {fields.length > 1 && ( - - )} -
- -
- ( - - Código Artículo - - - - - - )} - /> - - ( - - Descripción - -