Facturas de cliente
This commit is contained in:
parent
efcae31500
commit
78cc422d9a
@ -35,7 +35,9 @@
|
||||
},
|
||||
"style": {
|
||||
"useImportType": "off",
|
||||
"noNonNullAssertion": "info"
|
||||
"noInferrableTypes": "off",
|
||||
"noNonNullAssertion": "info",
|
||||
"noUselessElse": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"useSemanticElements": "info"
|
||||
|
||||
@ -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";
|
||||
|
||||
145
modules/core/src/common/helpers/money-dto-helper.ts
Normal file
145
modules/core/src/common/helpers/money-dto-helper.ts
Normal file
@ -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<MoneyDTO> => {
|
||||
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);
|
||||
}
|
||||
56
modules/core/src/common/helpers/money-helper.ts
Normal file
56
modules/core/src/common/helpers/money-helper.ts
Normal file
@ -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);
|
||||
@ -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<MoneyDTO> {
|
||||
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);
|
||||
}
|
||||
60
modules/core/src/common/helpers/percentage-dto-helpers.ts
Normal file
60
modules/core/src/common/helpers/percentage-dto-helpers.ts
Normal file
@ -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,
|
||||
};
|
||||
60
modules/core/src/common/helpers/quantity-dto-helpers.ts
Normal file
60
modules/core/src/common/helpers/quantity-dto-helpers.ts
Normal file
@ -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,
|
||||
};
|
||||
@ -8,13 +8,15 @@ type UseHookFormProps<TFields extends FieldValues = FieldValues, TContext = any>
|
||||
TContext
|
||||
> & {
|
||||
resolverSchema: z4.$ZodType<TFields, any>;
|
||||
initialValues: UseFormProps<TFields>["defaultValues"];
|
||||
defaultValues: UseFormProps<TFields>["defaultValues"];
|
||||
values: UseFormProps<TFields>["values"];
|
||||
onDirtyChange?: (isDirty: boolean) => void;
|
||||
};
|
||||
|
||||
export function useHookForm<TFields extends FieldValues = FieldValues, TContext = any>({
|
||||
resolverSchema,
|
||||
initialValues,
|
||||
defaultValues,
|
||||
values,
|
||||
disabled,
|
||||
onDirtyChange,
|
||||
...rest
|
||||
@ -22,7 +24,8 @@ export function useHookForm<TFields extends FieldValues = FieldValues, TContext
|
||||
const form = useForm<TFields, TContext>({
|
||||
...rest,
|
||||
resolver: zodResolver(resolverSchema),
|
||||
defaultValues: initialValues,
|
||||
defaultValues,
|
||||
values,
|
||||
disabled,
|
||||
});
|
||||
|
||||
@ -36,12 +39,12 @@ export function useHookForm<TFields extends FieldValues = FieldValues, TContext
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
}
|
||||
|
||||
101
modules/core/src/web/hooks/use-money-dto.ts
Normal file
101
modules/core/src/web/hooks/use-money-dto.ts
Normal file
@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -21,5 +21,6 @@ export function usePercentage() {
|
||||
maximumFractionDigits: 2,
|
||||
})}%`;
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
return useMemo(() => ({ toNumber, fromNumber, format }), []);
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
import { InvoiceProvider } from "../context";
|
||||
|
||||
export const CustomerInvoicesLayout = ({ children }: PropsWithChildren) => {
|
||||
return <InvoiceProvider>{children}</InvoiceProvider>;
|
||||
return <section>{children}</section>;
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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<CustomerInvoiceFormData>) => void;
|
||||
onSubmit: (data: InvoiceFormData) => void;
|
||||
onError: (errors: FieldErrors<InvoiceFormData>) => void;
|
||||
className: string;
|
||||
}
|
||||
|
||||
@ -23,7 +24,7 @@ export const CustomerInvoiceEditForm = ({
|
||||
className,
|
||||
}: CustomerInvoiceFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext<CustomerInvoiceFormData>();
|
||||
const form = useFormContext<InvoiceFormData>();
|
||||
|
||||
return (
|
||||
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
||||
@ -31,24 +32,35 @@ export const CustomerInvoiceEditForm = ({
|
||||
<div className='w-full'>
|
||||
<FormDebug />
|
||||
</div>
|
||||
<div className='mx-auto grid w-full grid-cols-1 gap-6 lg:grid-flow-col-dense lg:grid-cols-2 items-stretch'>
|
||||
<div className='lg:col-start-1 space-y-6'>
|
||||
<InvoiceBasicInfoFields />
|
||||
</div>
|
||||
<div className="w-full gap-6 grid grid-cols-1 mx-auto">
|
||||
<ResizablePanelGroup direction="horizontal" className="mx-auto grid w-full grid-cols-1 gap-6 lg:grid-cols-3 items-stretch">
|
||||
<ResizablePanel className="lg:col-start-1 lg:col-span-2 h-full" defaultSize={65}>
|
||||
<InvoiceBasicInfoFields className="h-full flex flex-col" />
|
||||
|
||||
<div className='space-y-6 '>
|
||||
<InvoiceRecipient />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel className="lg:col-end-4 h-full" defaultSize={35}>
|
||||
<InvoiceRecipient className="h-full flex flex-col" />
|
||||
|
||||
<div className='lg:col-start-1 lg:col-span-2 space-y-6'>
|
||||
<InvoiceItems />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
<div className='lg:col-start-1 space-y-6'>
|
||||
<InvoiceTaxSummary />
|
||||
</div>
|
||||
<div className='space-y-6 '>
|
||||
<InvoiceTotals />
|
||||
<div className="mx-auto grid w-full grid-cols-1 gap-6 lg:grid-cols-3 items-stretch">
|
||||
<div className="lg:col-start-1 lg:col-span-full h-full">
|
||||
{/* <InvoiceItems className="h-full flex flex-col"/> */}
|
||||
</div>
|
||||
|
||||
<div className="lg:col-start-1 h-full">
|
||||
<InvoiceNotes className="h-full flex flex-col" />
|
||||
</div>
|
||||
|
||||
<div className="h-full">
|
||||
<InvoiceTaxSummary className="h-full flex flex-col" />
|
||||
</div>
|
||||
|
||||
<div className="h-full">
|
||||
<InvoiceTotals className="h-full flex flex-col" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -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<CustomerInvoiceFormData>();
|
||||
const { control } = useFormContext<InvoiceFormData>();
|
||||
|
||||
const status = useWatch({
|
||||
control,
|
||||
name: "status",
|
||||
defaultValue: "",
|
||||
});
|
||||
|
||||
return (
|
||||
<Fieldset>
|
||||
<Fieldset {...props}>
|
||||
<Legend className='flex items-center gap-2 text-foreground'>
|
||||
<FileTextIcon className='h-5 w-5' /> {t("form_groups.basic_into.title")}
|
||||
<FileTextIcon className='size-5' /> {t("form_groups.basic_into.title")}
|
||||
</Legend>
|
||||
|
||||
<Description>{t("form_groups.basic_into.description")}</Description>
|
||||
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-3'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='invoice_number'
|
||||
readOnly
|
||||
label={t("form_fields.invoice_number.label")}
|
||||
placeholder={t("form_fields.invoice_number.placeholder")}
|
||||
description={t("form_fields.invoice_number.description")}
|
||||
/>
|
||||
<TextField
|
||||
typePreset='text'
|
||||
control={control}
|
||||
name='series'
|
||||
label={t("form_fields.series.label")}
|
||||
placeholder={t("form_fields.series.placeholder")}
|
||||
description={t("form_fields.series.description")}
|
||||
/>
|
||||
|
||||
<Field className='lg:col-span-2 2xl:col-span-1'>
|
||||
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
|
||||
<Field >
|
||||
<TextField
|
||||
control={control}
|
||||
name='invoice_number'
|
||||
readOnly
|
||||
label={t("form_fields.invoice_number.label")}
|
||||
placeholder={t("form_fields.invoice_number.placeholder")}
|
||||
description={t("form_fields.invoice_number.description")}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<DatePickerInputField
|
||||
control={control}
|
||||
name='invoice_date'
|
||||
@ -60,7 +48,18 @@ export const InvoiceBasicInfoFields = () => {
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field className='lg:col-span-2 lg:col-start-1 2xl:col-auto'>
|
||||
<Field >
|
||||
<TextField
|
||||
typePreset='text'
|
||||
control={control}
|
||||
name='series'
|
||||
label={t("form_fields.series.label")}
|
||||
placeholder={t("form_fields.series.placeholder")}
|
||||
description={t("form_fields.series.description")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<DatePickerInputField
|
||||
control={control}
|
||||
numberOfMonths={2}
|
||||
@ -70,25 +69,30 @@ export const InvoiceBasicInfoFields = () => {
|
||||
description={t("form_fields.operation_date.description")}
|
||||
/>
|
||||
</Field>
|
||||
<TextField
|
||||
typePreset='text'
|
||||
maxLength={256}
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
name='description'
|
||||
label={t("form_fields.description.label")}
|
||||
placeholder={t("form_fields.description.placeholder")}
|
||||
description={t("form_fields.description.description")}
|
||||
/>
|
||||
<TextAreaField
|
||||
maxLength={1024}
|
||||
className='lg:col-span-full'
|
||||
control={control}
|
||||
name='notes'
|
||||
label={t("form_fields.notes.label")}
|
||||
placeholder={t("form_fields.notes.placeholder")}
|
||||
description={t("form_fields.notes.description")}
|
||||
/>
|
||||
|
||||
<Field className='lg:col-start-1 lg:col-span-1'>
|
||||
<TextField
|
||||
typePreset='text'
|
||||
maxLength={256}
|
||||
control={control}
|
||||
name='reference'
|
||||
label={t("form_fields.reference.label")}
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
description={t("form_fields.reference.description")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field className='lg:col-span-3'>
|
||||
<TextField
|
||||
typePreset='text'
|
||||
maxLength={256}
|
||||
control={control}
|
||||
name='description'
|
||||
label={t("form_fields.description.label")}
|
||||
placeholder={t("form_fields.description.placeholder")}
|
||||
description={t("form_fields.description.description")}
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Fieldset>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<Card className='border-none shadow-none'>
|
||||
<Card className={cn("border-none shadow-none", className)} {...props}>
|
||||
<CardHeader>
|
||||
<div className='flex items-center justify-between'>
|
||||
<CardTitle className='text-lg font-medium flex items-center gap-2'>
|
||||
<Package className='h-5 w-5' />
|
||||
<Package className='size-5' />
|
||||
{t('form_groups.items.title')}
|
||||
</CardTitle>
|
||||
</div>
|
||||
|
||||
@ -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<InvoiceFormData>();
|
||||
|
||||
return (
|
||||
<Fieldset {...props}>
|
||||
<Legend className='flex items-center gap-2 text-foreground'>
|
||||
<StickyNoteIcon className='size-5' /> {t("form_groups.basic_into.title")}
|
||||
</Legend>
|
||||
|
||||
<Description>{t("form_groups.basic_into.description")}</Description>
|
||||
<FieldGroup className='grid grid-cols-1 gap-x-6 h-full min-h-0'>
|
||||
<TextAreaField
|
||||
maxLength={1024}
|
||||
className='lg:col-span-full h-full'
|
||||
control={control}
|
||||
name='notes'
|
||||
label={t("form_fields.notes.label")}
|
||||
placeholder={t("form_fields.notes.placeholder")}
|
||||
description={t("form_fields.notes.description")}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
@ -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<CustomerInvoiceFormData>();
|
||||
const { control, getValues } = useFormContext<InvoiceFormData>();
|
||||
|
||||
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 (
|
||||
<Fieldset>
|
||||
<Fieldset {...props}>
|
||||
<Legend className='flex items-center gap-2 text-foreground'>
|
||||
<ReceiptIcon className='h-5 w-5' /> {t("form_groups.tax_resume.title")}
|
||||
<ReceiptIcon className='size-5' /> {t("form_groups.tax_resume.title")}
|
||||
</Legend>
|
||||
|
||||
<Description>{t("form_groups.tax_resume.description")}</Description>
|
||||
<FieldGroup className='grid grid-cols-1'>
|
||||
<div className='space-y-3'>
|
||||
{displayTaxes.map((tax, index) => (
|
||||
<div key={`${tax.tax_code}-${index}`} className='border rounded-lg p-3'>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<div key={`${tax.tax_code}-${index}`} className='border rounded-lg p-3 space-y-2'>
|
||||
<div className='flex items-center justify-between mb-2 '>
|
||||
<Badge variant='secondary' className='text-xs'>
|
||||
{tax.tax_code}
|
||||
{tax.tax_label}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className='space-y-1 text-sm'>
|
||||
<div className='space-y-2 text-sm'>
|
||||
<div className='flex justify-between'>
|
||||
<span className='text-muted-foreground'>Base Imponible:</span>
|
||||
<span className='font-medium'>{formatCurrency(tax.taxable_amount)}</span>
|
||||
<span className='text-muted-foreground'>Base para el impuesto:</span>
|
||||
<span className='font-medium tabular-nums'>{formatCurrency(tax.taxable_amount)}</span>
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<span className='text-muted-foreground'>Importe Impuesto:</span>
|
||||
<span className='font-medium text-primary'>
|
||||
<span className='text-muted-foreground'>Importe de impuesto:</span>
|
||||
<span className='font-medium text-primary tabular-nums'>
|
||||
{formatCurrency(tax.taxes_amount)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -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<CustomerInvoiceFormData>();
|
||||
const { control, getValues } = useFormContext<InvoiceFormData>();
|
||||
|
||||
//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 (
|
||||
<Fieldset>
|
||||
<Fieldset {...props}>
|
||||
<Legend className='flex items-center gap-2 text-foreground'>
|
||||
<CalculatorIcon className='h-5 w-5' /> {t("form_groups.totals.title")}
|
||||
<CalculatorIcon className='size-5' /> {t("form_groups.totals.title")}
|
||||
</Legend>
|
||||
|
||||
<Description>{t("form_groups.totals.description")}</Description>
|
||||
@ -63,48 +63,54 @@ export const InvoiceTotals = () => {
|
||||
<div className='space-y-3'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-sm text-muted-foreground'>Subtotal</Label>
|
||||
<span className='font-medium'>{formatCurrency(invoice.subtotal_amount)}</span>
|
||||
<span className='font-medium tabular-nums'>{formatCurrency(getValues('subtotal_amount'))}</span>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between items-center gap-4'>
|
||||
<Label className='text-sm text-muted-foreground'>Descuento Global</Label>
|
||||
<Label className='text-sm text-muted-foreground'>Descuento (%)</Label>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
type='number'
|
||||
step='0.01'
|
||||
value={invoice.discount_percentage}
|
||||
onChange={(e) => updateDiscount(Number.parseFloat(e.target.value) || 0)}
|
||||
className='w-20 text-right'
|
||||
<Controller
|
||||
control={control}
|
||||
name={"discount_percentage"}
|
||||
render={({
|
||||
field, fieldState
|
||||
}) => (<Input
|
||||
readOnly={false}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
disabled={fieldState.isValidating}
|
||||
onBlur={field.onBlur}
|
||||
className='w-20 text-right'
|
||||
/>)}
|
||||
/>
|
||||
<span className='text-sm text-muted-foreground'>%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-sm text-muted-foreground'>Importe Descuento</Label>
|
||||
<span className='font-medium text-destructive'>
|
||||
-{formatCurrency(invoice.discount_amount)}
|
||||
<Label className='text-sm text-muted-foreground'>Importe del descuento</Label>
|
||||
<span className='font-medium text-destructive tabular-nums'>
|
||||
-{formatCurrency(getValues("discount_amount"))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-sm text-muted-foreground'>Base Imponible</Label>
|
||||
<span className='font-medium'>{formatCurrency(invoice.taxable_amount)}</span>
|
||||
<Label className='text-sm text-muted-foreground'>Base imponible</Label>
|
||||
<span className='font-medium tabular-nums'>{formatCurrency(getValues('taxable_amount'))}</span>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-sm text-muted-foreground'>Total Impuestos</Label>
|
||||
<span className='font-medium'>{formatCurrency(invoice.taxes_amount)}</span>
|
||||
<Label className='text-sm text-muted-foreground'>Total de impuestos</Label>
|
||||
<span className='font-medium tabular-nums'>{formatCurrency(getValues('taxes_amount'))}</span>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-lg font-semibold'>Total Factura</Label>
|
||||
<span className='text-xl font-bold text-primary'>
|
||||
{formatCurrency(invoice.total_amount)}
|
||||
<Label className='text-lg font-semibold'>Total de la factura</Label>
|
||||
<span className='text-xl font-bold text-primary tabular-nums'>
|
||||
{formatCurrency(getValues('total_amount'))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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<CustomerInvoiceFormData>();
|
||||
const { control } = useFormContext<InvoiceFormData>();
|
||||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
|
||||
@ -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 = ({
|
||||
<div className='h-5'>
|
||||
<Checkbox
|
||||
aria-label={`Seleccionar fila ${rowIndex + 1}`}
|
||||
className="block h-5 w-5 leading-none align-middle"
|
||||
className="block size-5 leading-none align-middle"
|
||||
checked={isSelected}
|
||||
onCheckedChange={onToggleSelect}
|
||||
disabled={readOnly}
|
||||
|
||||
@ -4,14 +4,14 @@ import * as React from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useItemsTableNavigation } from '../../../hooks';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { CustomerInvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||
import { InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||
import { ItemRow } from './item-row';
|
||||
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
||||
import { LastCellTabHook } from './last-cell-tab-hook';
|
||||
|
||||
interface ItemsEditorProps {
|
||||
value?: CustomerInvoiceItemFormData[];
|
||||
onChange?: (items: CustomerInvoiceItemFormData[]) => void;
|
||||
value?: InvoiceItemFormData[];
|
||||
onChange?: (items: InvoiceItemFormData[]) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@ -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<CustomerInvoiceItemFormData>();
|
||||
const { control } = useFormContext<InvoiceItemFormData>();
|
||||
const { format } = useMoney();
|
||||
const [lines, setLines] = useState<CustomerInvoiceItemFormData[]>(items);
|
||||
const [lines, setLines] = useState<InvoiceItemFormData[]>(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<CustomerInvoiceItemFormData>) => {
|
||||
const updateItem = (index: number, patch: Partial<InvoiceItemFormData>) => {
|
||||
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" },
|
||||
|
||||
@ -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 (
|
||||
<Fieldset>
|
||||
<Fieldset {...props}>
|
||||
<Legend className='flex items-center gap-2 text-foreground'>
|
||||
<UserIcon className='size-5' /> {t("form_groups.customer.title")}
|
||||
</Legend>
|
||||
|
||||
@ -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<InvoiceContextValue | null>(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<InvoiceProviderParams>) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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[] => {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<CustomerInvoiceFormData>) {
|
||||
export function useInvoiceAutoRecalc(form: UseFormReturn<InvoiceFormData>) {
|
||||
const {
|
||||
watch,
|
||||
setValue,
|
||||
@ -22,7 +22,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
||||
|
||||
// Cálculo de una línea
|
||||
const calculateItemTotals = React.useCallback(
|
||||
(item: CustomerInvoiceItemFormData) => {
|
||||
(item: InvoiceItemFormData) => {
|
||||
if (!item) {
|
||||
const zero = moneyHelper.fromNumber(0);
|
||||
return {
|
||||
@ -65,7 +65,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
||||
|
||||
// Cálculo de los totales de la factura a partir de los conceptos
|
||||
const calculateInvoiceTotals = React.useCallback(
|
||||
(items: CustomerInvoiceItemFormData[]) => {
|
||||
(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<CustomerInvoiceFormData
|
||||
formData.items.forEach((item, i) => {
|
||||
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<CustomerInvoiceFormData
|
||||
|
||||
// Recalcular importes totales de la factura y
|
||||
// actualizar valores calculados.
|
||||
const typedItems = formData.items as CustomerInvoiceItemFormData[];
|
||||
const typedItems = formData.items as InvoiceItemFormData[];
|
||||
const totalsGlobal = calculateInvoiceTotals(typedItems);
|
||||
|
||||
setValue("subtotal_amount", totalsGlobal.subtotalDTO);
|
||||
@ -136,7 +136,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
||||
const fieldName = name.split(".")[2];
|
||||
|
||||
if (["quantity", "unit_amount", "discount_percentage"].includes(fieldName)) {
|
||||
const typedItem = formData.items[index] as CustomerInvoiceItemFormData;
|
||||
const typedItem = formData.items[index] as InvoiceItemFormData;
|
||||
if (!typedItem) return;
|
||||
|
||||
// Recalcular línea
|
||||
@ -152,7 +152,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
||||
|
||||
// Recalcular importes totales de la factura y
|
||||
// actualizar valores calculados.
|
||||
const typedItems = formData.items as CustomerInvoiceItemFormData[];
|
||||
const typedItems = formData.items as InvoiceItemFormData[];
|
||||
const totalsGlobal = calculateInvoiceTotals(typedItems);
|
||||
|
||||
setValue("subtotal_amount", totalsGlobal.subtotalDTO);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export * from "./calcs";
|
||||
export * from "./use-create-customer-invoice-mutation";
|
||||
export * from "./use-customer-invoice-query";
|
||||
export * from "./use-customer-invoices-query";
|
||||
export * from "./use-detail-columns";
|
||||
export * from "./use-invoice-query";
|
||||
export * from "./use-items-table-navigation";
|
||||
export * from "./use-update-customer-invoice-mutation";
|
||||
|
||||
@ -2,10 +2,10 @@ import { useDataSource } from "@erp/core/hooks";
|
||||
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { CreateCustomerInvoiceRequestSchema } from "../../common";
|
||||
import { CustomerInvoice, CustomerInvoiceFormData } from "../schemas";
|
||||
import { CustomerInvoice, InvoiceFormData } from "../schemas";
|
||||
|
||||
type CreateCustomerInvoicePayload = {
|
||||
data: CustomerInvoiceFormData;
|
||||
data: InvoiceFormData;
|
||||
};
|
||||
|
||||
export const useCreateCustomerInvoiceMutation = () => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<CustomerInvoiceFormData>;
|
||||
data: Partial<InvoiceFormData>;
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from "./customer-invoices-update-page";
|
||||
export * from "./invoice-update-page";
|
||||
|
||||
@ -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<CustomerInvoiceFormData>({
|
||||
resolverSchema: CustomerInvoiceFormSchema,
|
||||
initialValues: (invoiceData as unknown as CustomerInvoiceFormData) ?? defaultCustomerInvoiceFormData,
|
||||
const form = useHookForm<InvoiceFormData>({
|
||||
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<CustomerInvoiceFormData>) => {
|
||||
const handleError = (errors: FieldErrors<InvoiceFormData>) => {
|
||||
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 (
|
||||
<InvoiceProvider
|
||||
company_id={invoiceData.company_id}
|
||||
status={invoiceData.status}
|
||||
language_code={invoiceData.language_code}
|
||||
currency_code={invoiceData.currency_code}
|
||||
>
|
||||
@ -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<typeof CustomerInvoiceFormSchema>;
|
||||
export type CustomerInvoiceItemFormData = ArrayElement<CustomerInvoiceFormData["items"]>;
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
})),
|
||||
};
|
||||
},
|
||||
};
|
||||
124
modules/customer-invoices/src/web/schemas/invoice.form.schema.ts
Normal file
124
modules/customer-invoices/src/web/schemas/invoice.form.schema.ts
Normal file
@ -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<typeof InvoiceFormSchema>;
|
||||
export type InvoiceItemFormData = z.infer<typeof InvoiceItemFormSchema>;
|
||||
|
||||
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,
|
||||
};
|
||||
@ -222,7 +222,7 @@ export const ClientSelectorModal = () => {
|
||||
<DialogContent className='max-w-md'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='flex items-center gap-2'>
|
||||
<Plus className='h-5 w-5' />
|
||||
<Plus className='size-5' />
|
||||
Nuevo Cliente
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -248,7 +248,7 @@ const CustomerCard = ({ customer }: { customer: Customer }) => (
|
||||
<Card>
|
||||
<CardContent className='p-4 space-y-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<User className='h-5 w-5' />
|
||||
<User className='size-5' />
|
||||
<span className='font-semibold'>{customer.name}</span>
|
||||
<Badge variant={customer.status === "Activo" ? "default" : "secondary"}>
|
||||
{customer.status}
|
||||
|
||||
@ -31,7 +31,7 @@ export const CreateCustomerFormDialog = ({
|
||||
<DialogContent className='sm:max-w-[500px] bg-card border-border'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='flex items-center gap-2'>
|
||||
<Plus className='h-5 w-5' /> Agregar Nuevo Cliente
|
||||
<Plus className='size-5' /> Agregar Nuevo Cliente
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Complete la información del cliente. Los campos marcados con * son obligatorios.
|
||||
|
||||
@ -61,7 +61,7 @@ export const CustomerSearchDialog = ({
|
||||
<DialogHeader className='px-6 pt-6 pb-4'>
|
||||
<DialogTitle className='flex items-center justify-between'>
|
||||
<span className='flex items-center gap-2'>
|
||||
<User className='h-5 w-5' />
|
||||
<User className='size-5' />
|
||||
Seleccionar Cliente
|
||||
</span>
|
||||
</DialogTitle>
|
||||
|
||||
@ -102,7 +102,7 @@ export const CustomerViewPage = () => {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||
<FileText className='h-5 w-5 text-primary' />
|
||||
<FileText className='size-5 text-primary' />
|
||||
Información Básica
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@ -136,7 +136,7 @@ export const CustomerViewPage = () => {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||
<MapPin className='h-5 w-5 text-primary' />
|
||||
<MapPin className='size-5 text-primary' />
|
||||
Dirección
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@ -180,7 +180,7 @@ export const CustomerViewPage = () => {
|
||||
<Card className='md:col-span-2'>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||
<Mail className='h-5 w-5 text-primary' />
|
||||
<Mail className='size-5 text-primary' />
|
||||
Información de Contacto
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@ -306,7 +306,7 @@ export const CustomerViewPage = () => {
|
||||
<Card className='md:col-span-2'>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||
<Languages className='h-5 w-5 text-primary' />
|
||||
<Languages className='size-5 text-primary' />
|
||||
Preferencias
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -28,6 +28,8 @@ type TextAreaFieldProps<TFormValues extends FieldValues> = CommonInputProps & {
|
||||
|
||||
/** Contador de caracteres (si usas maxLength) */
|
||||
showCounter?: boolean;
|
||||
maxLength?: number;
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
export function TextAreaField<TFormValues extends FieldValues>({
|
||||
@ -42,6 +44,7 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
||||
className,
|
||||
showCounter = false,
|
||||
maxLength,
|
||||
rows = 3
|
||||
}: TextAreaFieldProps<TFormValues>) {
|
||||
const { t } = useTranslation();
|
||||
const isDisabled = disabled || readOnly;
|
||||
@ -57,7 +60,7 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className={cn("space-y-0", className)}>
|
||||
<FormItem className={cn("space-y-0 flex flex-col ", className)}>
|
||||
{label && (
|
||||
<div className='mb-1 flex justify-between gap-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
@ -81,8 +84,11 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
||||
<Textarea
|
||||
disabled={isDisabled}
|
||||
placeholder={placeholder}
|
||||
className={"placeholder:font-normal placeholder:italic bg-background"}
|
||||
className={"placeholder:font-normal placeholder:italic bg-background flex flex-1 min-h-0 h-full"}
|
||||
maxLength={maxLength}
|
||||
spellCheck={true}
|
||||
rows={rows}
|
||||
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -407,7 +407,7 @@ export function DataTable({
|
||||
Past Performance{" "}
|
||||
<Badge
|
||||
variant='secondary'
|
||||
className='flex h-5 w-5 items-center justify-center rounded-full bg-muted-foreground/30'
|
||||
className='flex size-5 items-center justify-center rounded-full bg-muted-foreground/30'
|
||||
>
|
||||
3
|
||||
</Badge>
|
||||
@ -416,7 +416,7 @@ export function DataTable({
|
||||
Key Personnel{" "}
|
||||
<Badge
|
||||
variant='secondary'
|
||||
className='flex h-5 w-5 items-center justify-center rounded-full bg-muted-foreground/30'
|
||||
className='flex size-5 items-center justify-center rounded-full bg-muted-foreground/30'
|
||||
>
|
||||
2
|
||||
</Badge>
|
||||
|
||||
@ -24,6 +24,18 @@ export class Collection<T> {
|
||||
this.totalItems = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrega un nuevo elemento a la colección.
|
||||
* @param item - Elemento a agregar.
|
||||
*/
|
||||
addCollection(collection: Collection<T>): boolean {
|
||||
this.items.push(...collection.items);
|
||||
if (this.totalItems !== null) {
|
||||
this.totalItems = this.totalItems + collection.totalItems;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrega un nuevo elemento a la colección.
|
||||
* @param item - Elemento a agregar.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user