Facturas de cliente
This commit is contained in:
parent
efcae31500
commit
78cc422d9a
@ -35,7 +35,9 @@
|
|||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"useImportType": "off",
|
"useImportType": "off",
|
||||||
"noNonNullAssertion": "info"
|
"noInferrableTypes": "off",
|
||||||
|
"noNonNullAssertion": "info",
|
||||||
|
"noUselessElse": "off"
|
||||||
},
|
},
|
||||||
"a11y": {
|
"a11y": {
|
||||||
"useSemanticElements": "info"
|
"useSemanticElements": "info"
|
||||||
|
|||||||
@ -1,2 +1,5 @@
|
|||||||
export * from "./dto-compare-helper";
|
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
|
TContext
|
||||||
> & {
|
> & {
|
||||||
resolverSchema: z4.$ZodType<TFields, any>;
|
resolverSchema: z4.$ZodType<TFields, any>;
|
||||||
initialValues: UseFormProps<TFields>["defaultValues"];
|
defaultValues: UseFormProps<TFields>["defaultValues"];
|
||||||
|
values: UseFormProps<TFields>["values"];
|
||||||
onDirtyChange?: (isDirty: boolean) => void;
|
onDirtyChange?: (isDirty: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useHookForm<TFields extends FieldValues = FieldValues, TContext = any>({
|
export function useHookForm<TFields extends FieldValues = FieldValues, TContext = any>({
|
||||||
resolverSchema,
|
resolverSchema,
|
||||||
initialValues,
|
defaultValues,
|
||||||
|
values,
|
||||||
disabled,
|
disabled,
|
||||||
onDirtyChange,
|
onDirtyChange,
|
||||||
...rest
|
...rest
|
||||||
@ -22,7 +24,8 @@ export function useHookForm<TFields extends FieldValues = FieldValues, TContext
|
|||||||
const form = useForm<TFields, TContext>({
|
const form = useForm<TFields, TContext>({
|
||||||
...rest,
|
...rest,
|
||||||
resolver: zodResolver(resolverSchema),
|
resolver: zodResolver(resolverSchema),
|
||||||
defaultValues: initialValues,
|
defaultValues,
|
||||||
|
values,
|
||||||
disabled,
|
disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -36,12 +39,12 @@ export function useHookForm<TFields extends FieldValues = FieldValues, TContext
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const applyReset = async () => {
|
const applyReset = async () => {
|
||||||
const values = typeof initialValues === "function" ? await initialValues() : initialValues;
|
const values = typeof defaultValues === "function" ? await defaultValues() : defaultValues;
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
};
|
};
|
||||||
applyReset();
|
applyReset();
|
||||||
}, [initialValues, form]);
|
}, [defaultValues, form]);
|
||||||
|
|
||||||
return 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 { MoneyDTO } from "@erp/core/common";
|
||||||
import type { Currency } from "dinero.js";
|
import type { Currency } from "dinero.js";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
|
||||||
dineroFromDTO,
|
|
||||||
dtoFromDinero,
|
|
||||||
formatDTO,
|
|
||||||
multiplyDTO,
|
|
||||||
percentageDTO,
|
|
||||||
sumDTO,
|
|
||||||
} from "../../common/helpers";
|
|
||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
|
|
||||||
export type { Currency };
|
export type { Currency };
|
||||||
@ -131,7 +123,7 @@ export function useMoney(overrides?: {
|
|||||||
[fallbackCurrency]
|
[fallbackCurrency]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Operaciones (dinero.js via helpers)
|
/* // Operaciones (dinero.js via helpers)
|
||||||
const add = React.useCallback(
|
const add = React.useCallback(
|
||||||
(a: MoneyDTO, b: MoneyDTO): MoneyDTO => sumDTO([a, b], fallbackCurrency),
|
(a: MoneyDTO, b: MoneyDTO): MoneyDTO => sumDTO([a, b], fallbackCurrency),
|
||||||
[fallbackCurrency]
|
[fallbackCurrency]
|
||||||
@ -149,7 +141,7 @@ export function useMoney(overrides?: {
|
|||||||
(dto: MoneyDTO, p: number, rounding: Dinero.RoundingMode = "HALF_EVEN") =>
|
(dto: MoneyDTO, p: number, rounding: Dinero.RoundingMode = "HALF_EVEN") =>
|
||||||
percentageDTO(dto, p, rounding, fallbackCurrency),
|
percentageDTO(dto, p, rounding, fallbackCurrency),
|
||||||
[fallbackCurrency]
|
[fallbackCurrency]
|
||||||
);
|
); */
|
||||||
|
|
||||||
// Estado/Comparaciones
|
// Estado/Comparaciones
|
||||||
const isZero = React.useCallback((dto?: MoneyDTO | null) => toNumber(dto) === 0, [toNumber]);
|
const isZero = React.useCallback((dto?: MoneyDTO | null) => toNumber(dto) === 0, [toNumber]);
|
||||||
@ -183,10 +175,10 @@ export function useMoney(overrides?: {
|
|||||||
toApi,
|
toApi,
|
||||||
|
|
||||||
// Operaciones
|
// Operaciones
|
||||||
add,
|
//add,
|
||||||
sub,
|
//sub,
|
||||||
multiply,
|
//multiply,
|
||||||
percentage,
|
//percentage,
|
||||||
|
|
||||||
// Estado/ayudas
|
// Estado/ayudas
|
||||||
isZero,
|
isZero,
|
||||||
@ -202,8 +194,8 @@ export function useMoney(overrides?: {
|
|||||||
fallbackCurrency,
|
fallbackCurrency,
|
||||||
defaultScale,
|
defaultScale,
|
||||||
// Factory Dinero si se necesita en algún punto de bajo nivel:
|
// Factory Dinero si se necesita en algún punto de bajo nivel:
|
||||||
toDinero: (dto: MoneyDTO) => dineroFromDTO(dto, fallbackCurrency),
|
//toDinero: (dto: MoneyDTO) => dineroFromDTO(dto, fallbackCurrency),
|
||||||
fromDinero: dtoFromDinero,
|
//fromDinero: dtoFromDinero,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
toNumber,
|
toNumber,
|
||||||
@ -214,10 +206,10 @@ export function useMoney(overrides?: {
|
|||||||
parse,
|
parse,
|
||||||
fromApi,
|
fromApi,
|
||||||
toApi,
|
toApi,
|
||||||
add,
|
//add,
|
||||||
sub,
|
//sub,
|
||||||
multiply,
|
//multiply,
|
||||||
percentage,
|
//percentage,
|
||||||
isZero,
|
isZero,
|
||||||
sameCurrency,
|
sameCurrency,
|
||||||
stepNumber,
|
stepNumber,
|
||||||
|
|||||||
@ -21,5 +21,6 @@ export function usePercentage() {
|
|||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
})}%`;
|
})}%`;
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||||
return useMemo(() => ({ toNumber, fromNumber, format }), []);
|
return useMemo(() => ({ toNumber, fromNumber, format }), []);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,6 @@ export class CustomerInvoiceFullPresenter extends Presenter<
|
|||||||
);
|
);
|
||||||
|
|
||||||
const invoiceTaxes = invoice.getTaxes().map((taxItem) => {
|
const invoiceTaxes = invoice.getTaxes().map((taxItem) => {
|
||||||
console.log(taxItem);
|
|
||||||
return {
|
return {
|
||||||
tax_code: taxItem.tax.code,
|
tax_code: taxItem.tax.code,
|
||||||
taxable_amount: taxItem.taxableAmount.toObjectString(),
|
taxable_amount: taxItem.taxableAmount.toObjectString(),
|
||||||
@ -44,8 +43,6 @@ export class CustomerInvoiceFullPresenter extends Presenter<
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(invoiceTaxes);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: invoice.id.toString(),
|
id: invoice.id.toString(),
|
||||||
company_id: invoice.companyId.toString(),
|
company_id: invoice.companyId.toString(),
|
||||||
|
|||||||
@ -225,7 +225,12 @@ export class CustomerInvoice
|
|||||||
const itemTaxes = this.items.getTaxesAmountByTaxes();
|
const itemTaxes = this.items.getTaxesAmountByTaxes();
|
||||||
|
|
||||||
for (const taxItem of itemTaxes) {
|
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;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export const UpdateCustomerInvoiceByIdParamsRequestSchema = z.object({
|
export const UpdateCustomerInvoiceByIdParamsRequestSchema = z.object({
|
||||||
@ -18,6 +19,20 @@ export const UpdateCustomerInvoiceByIdRequestSchema = z.object({
|
|||||||
|
|
||||||
language_code: z.string().optional(),
|
language_code: z.string().optional(),
|
||||||
currency_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<
|
export type UpdateCustomerInvoiceByIdRequestDTO = Partial<
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
|
|||||||
items: z.array(
|
items: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
isNonValued: z.string(),
|
is_non_valued: z.string(),
|
||||||
position: z.string(),
|
position: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
quantity: QuantitySchema,
|
quantity: QuantitySchema,
|
||||||
|
|||||||
@ -114,6 +114,11 @@
|
|||||||
"placeholder": "Select a date",
|
"placeholder": "Select a date",
|
||||||
"description": "Invoice operation date"
|
"description": "Invoice operation date"
|
||||||
},
|
},
|
||||||
|
"reference": {
|
||||||
|
"label": "Reference",
|
||||||
|
"placeholder": "Reference of the invoice",
|
||||||
|
"description": "Reference of the invoice"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"label": "Description",
|
"label": "Description",
|
||||||
"placeholder": "Description of the invoice",
|
"placeholder": "Description of the invoice",
|
||||||
|
|||||||
@ -106,6 +106,12 @@
|
|||||||
"placeholder": "Selecciona una fecha",
|
"placeholder": "Selecciona una fecha",
|
||||||
"description": "Fecha de la operación de la factura"
|
"description": "Fecha de la operación de la factura"
|
||||||
},
|
},
|
||||||
|
"reference": {
|
||||||
|
"label": "Referencia",
|
||||||
|
"placeholder": "Referencia de la factura",
|
||||||
|
"description": "Referencia de la factura"
|
||||||
|
},
|
||||||
|
|
||||||
"description": {
|
"description": {
|
||||||
"label": "Descripción",
|
"label": "Descripción",
|
||||||
"placeholder": "Descripción de la factura",
|
"placeholder": "Descripción de la factura",
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
import { InvoiceProvider } from "../context";
|
|
||||||
|
|
||||||
export const CustomerInvoicesLayout = ({ children }: PropsWithChildren) => {
|
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 { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
import { MoneyDTO } from "@erp/core";
|
|
||||||
import { formatDate } from "@erp/core/client";
|
import { formatDate } from "@erp/core/client";
|
||||||
import { useMoney } from '@erp/core/hooks';
|
|
||||||
import { ErrorOverlay } from "@repo/rdx-ui/components";
|
import { ErrorOverlay } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
import { AgGridReact } from "ag-grid-react";
|
import { AgGridReact } from "ag-grid-react";
|
||||||
@ -26,7 +24,7 @@ import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge";
|
|||||||
export const CustomerInvoicesListGrid = () => {
|
export const CustomerInvoicesListGrid = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { formatCurrency } = useMoney();
|
//const { formatCurrency } = useMoney();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: invoices,
|
data: invoices,
|
||||||
@ -96,10 +94,10 @@ export const CustomerInvoicesListGrid = () => {
|
|||||||
field: "taxable_amount",
|
field: "taxable_amount",
|
||||||
headerName: t("pages.list.grid_columns.taxable_amount"),
|
headerName: t("pages.list.grid_columns.taxable_amount"),
|
||||||
type: "rightAligned",
|
type: "rightAligned",
|
||||||
valueFormatter: (params: ValueFormatterParams) => {
|
/*valueFormatter: (params: ValueFormatterParams) => {
|
||||||
const raw: MoneyDTO | null = params.value;
|
const raw: MoneyDTO | null = params.value;
|
||||||
return raw ? formatCurrency(raw) : "—";
|
return raw ? formatCurrency(raw) : "—";
|
||||||
},
|
},*/
|
||||||
cellClass: "tabular-nums",
|
cellClass: "tabular-nums",
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
},
|
},
|
||||||
@ -107,10 +105,10 @@ export const CustomerInvoicesListGrid = () => {
|
|||||||
field: "taxes_amount",
|
field: "taxes_amount",
|
||||||
headerName: t("pages.list.grid_columns.taxes_amount"),
|
headerName: t("pages.list.grid_columns.taxes_amount"),
|
||||||
type: "rightAligned",
|
type: "rightAligned",
|
||||||
valueFormatter: (params: ValueFormatterParams) => {
|
/*valueFormatter: (params: ValueFormatterParams) => {
|
||||||
const raw: MoneyDTO | null = params.value;
|
const raw: MoneyDTO | null = params.value;
|
||||||
return raw ? formatCurrency(raw) : "—";
|
return raw ? formatCurrency(raw) : "—";
|
||||||
},
|
},*/
|
||||||
cellClass: "tabular-nums",
|
cellClass: "tabular-nums",
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
},
|
},
|
||||||
@ -118,10 +116,10 @@ export const CustomerInvoicesListGrid = () => {
|
|||||||
field: "total_amount",
|
field: "total_amount",
|
||||||
headerName: t("pages.list.grid_columns.total_amount"),
|
headerName: t("pages.list.grid_columns.total_amount"),
|
||||||
type: "rightAligned",
|
type: "rightAligned",
|
||||||
valueFormatter: (params: ValueFormatterParams) => {
|
/*valueFormatter: (params: ValueFormatterParams) => {
|
||||||
const raw: MoneyDTO | null = params.value;
|
const raw: MoneyDTO | null = params.value;
|
||||||
return raw ? formatCurrency(raw) : "—";
|
return raw ? formatCurrency(raw) : "—";
|
||||||
},
|
},*/
|
||||||
cellClass: "tabular-nums font-semibold",
|
cellClass: "tabular-nums font-semibold",
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
import { FieldErrors, useFormContext } from "react-hook-form";
|
import { FieldErrors, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { FormDebug } from "@erp/core/components";
|
import { FormDebug } from "@erp/core/components";
|
||||||
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@repo/shadcn-ui/components';
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerInvoiceFormData } from "../../schemas";
|
import { InvoiceFormData } from "../../schemas";
|
||||||
import { InvoiceBasicInfoFields } from "./invoice-basic-info-fields";
|
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 { InvoiceTaxSummary } from "./invoice-tax-summary";
|
||||||
import { InvoiceTotals } from "./invoice-totals";
|
import { InvoiceTotals } from "./invoice-totals";
|
||||||
import { InvoiceRecipient } from "./recipient";
|
import { InvoiceRecipient } from "./recipient";
|
||||||
|
|
||||||
interface CustomerInvoiceFormProps {
|
interface CustomerInvoiceFormProps {
|
||||||
formId: string;
|
formId: string;
|
||||||
onSubmit: (data: CustomerInvoiceFormData) => void;
|
onSubmit: (data: InvoiceFormData) => void;
|
||||||
onError: (errors: FieldErrors<CustomerInvoiceFormData>) => void;
|
onError: (errors: FieldErrors<InvoiceFormData>) => void;
|
||||||
className: string;
|
className: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ export const CustomerInvoiceEditForm = ({
|
|||||||
className,
|
className,
|
||||||
}: CustomerInvoiceFormProps) => {
|
}: CustomerInvoiceFormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = useFormContext<CustomerInvoiceFormData>();
|
const form = useFormContext<InvoiceFormData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
||||||
@ -31,24 +32,35 @@ export const CustomerInvoiceEditForm = ({
|
|||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<FormDebug />
|
<FormDebug />
|
||||||
</div>
|
</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="w-full gap-6 grid grid-cols-1 mx-auto">
|
||||||
<div className='lg:col-start-1 space-y-6'>
|
<ResizablePanelGroup direction="horizontal" className="mx-auto grid w-full grid-cols-1 gap-6 lg:grid-cols-3 items-stretch">
|
||||||
<InvoiceBasicInfoFields />
|
<ResizablePanel className="lg:col-start-1 lg:col-span-2 h-full" defaultSize={65}>
|
||||||
</div>
|
<InvoiceBasicInfoFields className="h-full flex flex-col" />
|
||||||
|
|
||||||
<div className='space-y-6 '>
|
</ResizablePanel>
|
||||||
<InvoiceRecipient />
|
<ResizableHandle withHandle />
|
||||||
</div>
|
<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'>
|
</ResizablePanel>
|
||||||
<InvoiceItems />
|
</ResizablePanelGroup>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='lg:col-start-1 space-y-6'>
|
<div className="mx-auto grid w-full grid-cols-1 gap-6 lg:grid-cols-3 items-stretch">
|
||||||
<InvoiceTaxSummary />
|
<div className="lg:col-start-1 lg:col-span-full h-full">
|
||||||
</div>
|
{/* <InvoiceItems className="h-full flex flex-col"/> */}
|
||||||
<div className='space-y-6 '>
|
</div>
|
||||||
<InvoiceTotals />
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -5,50 +5,38 @@ import {
|
|||||||
FieldGroup,
|
FieldGroup,
|
||||||
Fieldset,
|
Fieldset,
|
||||||
Legend,
|
Legend,
|
||||||
TextAreaField,
|
TextField
|
||||||
TextField,
|
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import { FileTextIcon } from "lucide-react";
|
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 { useTranslation } from "../../i18n";
|
||||||
import { CustomerInvoiceFormData } from "../../schemas";
|
import { InvoiceFormData } from "../../schemas";
|
||||||
|
|
||||||
export const InvoiceBasicInfoFields = () => {
|
export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerInvoiceFormData>();
|
const { control } = useFormContext<InvoiceFormData>();
|
||||||
|
|
||||||
const status = useWatch({
|
|
||||||
control,
|
|
||||||
name: "status",
|
|
||||||
defaultValue: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset>
|
<Fieldset {...props}>
|
||||||
<Legend className='flex items-center gap-2 text-foreground'>
|
<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>
|
</Legend>
|
||||||
|
|
||||||
<Description>{t("form_groups.basic_into.description")}</Description>
|
<Description>{t("form_groups.basic_into.description")}</Description>
|
||||||
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-3'>
|
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
|
||||||
<TextField
|
<Field >
|
||||||
control={control}
|
<TextField
|
||||||
name='invoice_number'
|
control={control}
|
||||||
readOnly
|
name='invoice_number'
|
||||||
label={t("form_fields.invoice_number.label")}
|
readOnly
|
||||||
placeholder={t("form_fields.invoice_number.placeholder")}
|
label={t("form_fields.invoice_number.label")}
|
||||||
description={t("form_fields.invoice_number.description")}
|
placeholder={t("form_fields.invoice_number.placeholder")}
|
||||||
/>
|
description={t("form_fields.invoice_number.description")}
|
||||||
<TextField
|
/>
|
||||||
typePreset='text'
|
</Field>
|
||||||
control={control}
|
<Field>
|
||||||
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'>
|
|
||||||
<DatePickerInputField
|
<DatePickerInputField
|
||||||
control={control}
|
control={control}
|
||||||
name='invoice_date'
|
name='invoice_date'
|
||||||
@ -60,7 +48,18 @@ export const InvoiceBasicInfoFields = () => {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</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
|
<DatePickerInputField
|
||||||
control={control}
|
control={control}
|
||||||
numberOfMonths={2}
|
numberOfMonths={2}
|
||||||
@ -70,25 +69,30 @@ export const InvoiceBasicInfoFields = () => {
|
|||||||
description={t("form_fields.operation_date.description")}
|
description={t("form_fields.operation_date.description")}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<TextField
|
|
||||||
typePreset='text'
|
<Field className='lg:col-start-1 lg:col-span-1'>
|
||||||
maxLength={256}
|
<TextField
|
||||||
className='lg:col-span-2'
|
typePreset='text'
|
||||||
control={control}
|
maxLength={256}
|
||||||
name='description'
|
control={control}
|
||||||
label={t("form_fields.description.label")}
|
name='reference'
|
||||||
placeholder={t("form_fields.description.placeholder")}
|
label={t("form_fields.reference.label")}
|
||||||
description={t("form_fields.description.description")}
|
placeholder={t("form_fields.reference.placeholder")}
|
||||||
/>
|
description={t("form_fields.reference.description")}
|
||||||
<TextAreaField
|
/>
|
||||||
maxLength={1024}
|
</Field>
|
||||||
className='lg:col-span-full'
|
|
||||||
control={control}
|
<Field className='lg:col-span-3'>
|
||||||
name='notes'
|
<TextField
|
||||||
label={t("form_fields.notes.label")}
|
typePreset='text'
|
||||||
placeholder={t("form_fields.notes.placeholder")}
|
maxLength={256}
|
||||||
description={t("form_fields.notes.description")}
|
control={control}
|
||||||
/>
|
name='description'
|
||||||
|
label={t("form_fields.description.label")}
|
||||||
|
placeholder={t("form_fields.description.placeholder")}
|
||||||
|
description={t("form_fields.description.description")}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||||
import { Package } from "lucide-react";
|
import { Package } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { ComponentProps } from 'react';
|
||||||
import { useTranslation } from '../../i18n';
|
import { useTranslation } from '../../i18n';
|
||||||
import { ItemsEditor } from "./items";
|
import { ItemsEditor } from "./items";
|
||||||
|
|
||||||
|
|
||||||
export const InvoiceItems = () => {
|
export const InvoiceItems = ({ className, ...props }: ComponentProps<"div">) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='border-none shadow-none'>
|
<Card className={cn("border-none shadow-none", className)} {...props}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<CardTitle className='text-lg font-medium flex items-center gap-2'>
|
<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')}
|
{t('form_groups.items.title')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</div>
|
</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 { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
|
||||||
import { Badge } from "@repo/shadcn-ui/components";
|
import { Badge } from "@repo/shadcn-ui/components";
|
||||||
import { ReceiptIcon } from "lucide-react";
|
import { ReceiptIcon } from "lucide-react";
|
||||||
|
import { ComponentProps } from 'react';
|
||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
import { useTranslation } from "../../i18n";
|
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 { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerInvoiceFormData>();
|
const { control, getValues } = useFormContext<InvoiceFormData>();
|
||||||
|
|
||||||
const taxes = useWatch({
|
const taxes = useWatch({
|
||||||
control,
|
control,
|
||||||
@ -15,79 +16,43 @@ export const InvoiceTaxSummary = () => {
|
|||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatCurrency = (amount: {
|
console.log(getValues());
|
||||||
value: string;
|
|
||||||
scale: string;
|
|
||||||
currency_code: string;
|
|
||||||
}) => {
|
|
||||||
const { currency_code, value, scale } = amount;
|
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number) => {
|
||||||
return new Intl.NumberFormat("es-ES", {
|
return new Intl.NumberFormat("es-ES", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: currency_code,
|
currency: "EUR",
|
||||||
minimumFractionDigits: Number(scale),
|
minimumFractionDigits: 2,
|
||||||
maximumFractionDigits: Number(scale),
|
maximumFractionDigits: 2,
|
||||||
compactDisplay: "short",
|
}).format(amount);
|
||||||
currencyDisplay: "symbol",
|
|
||||||
}).format(Number(value) / 10 ** Number(scale));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock tax data
|
const displayTaxes = taxes || [];
|
||||||
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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset>
|
<Fieldset {...props}>
|
||||||
<Legend className='flex items-center gap-2 text-foreground'>
|
<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>
|
</Legend>
|
||||||
|
|
||||||
<Description>{t("form_groups.tax_resume.description")}</Description>
|
<Description>{t("form_groups.tax_resume.description")}</Description>
|
||||||
<FieldGroup className='grid grid-cols-1'>
|
<FieldGroup className='grid grid-cols-1'>
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
{displayTaxes.map((tax, index) => (
|
{displayTaxes.map((tax, index) => (
|
||||||
<div key={`${tax.tax_code}-${index}`} className='border rounded-lg p-3'>
|
<div key={`${tax.tax_code}-${index}`} className='border rounded-lg p-3 space-y-2'>
|
||||||
<div className='flex items-center justify-between mb-2'>
|
<div className='flex items-center justify-between mb-2 '>
|
||||||
<Badge variant='secondary' className='text-xs'>
|
<Badge variant='secondary' className='text-xs'>
|
||||||
{tax.tax_code}
|
{tax.tax_label}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-1 text-sm'>
|
<div className='space-y-2 text-sm'>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
<span className='text-muted-foreground'>Base Imponible:</span>
|
<span className='text-muted-foreground'>Base para el impuesto:</span>
|
||||||
<span className='font-medium'>{formatCurrency(tax.taxable_amount)}</span>
|
<span className='font-medium tabular-nums'>{formatCurrency(tax.taxable_amount)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
<span className='text-muted-foreground'>Importe Impuesto:</span>
|
<span className='text-muted-foreground'>Importe de impuesto:</span>
|
||||||
<span className='font-medium text-primary'>
|
<span className='font-medium text-primary tabular-nums'>
|
||||||
{formatCurrency(tax.taxes_amount)}
|
{formatCurrency(tax.taxes_amount)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
|
import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
|
||||||
import { Input, Label, Separator } from "@repo/shadcn-ui/components";
|
import { Input, Label, Separator } from "@repo/shadcn-ui/components";
|
||||||
import { CalculatorIcon } from "lucide-react";
|
import { CalculatorIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { ComponentProps } from 'react';
|
||||||
import { useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import { useTranslation } from "../../i18n";
|
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 { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerInvoiceFormData>();
|
const { control, getValues } = useFormContext<InvoiceFormData>();
|
||||||
|
|
||||||
//const invoiceFormData = useWatch({ control });
|
//const invoiceFormData = useWatch({ control });
|
||||||
|
|
||||||
const [invoice, setInvoice] = useState({
|
/*const [invoice, setInvoice] = useState({
|
||||||
items: [],
|
items: [],
|
||||||
subtotal_amount: 0,
|
subtotal_amount: 0,
|
||||||
discount_percentage: 0,
|
discount_percentage: 0,
|
||||||
@ -23,7 +23,7 @@ export const InvoiceTotals = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updateDiscount = (value: number) => {
|
const updateDiscount = (value: number) => {
|
||||||
const subtotal = invoice.items.reduce(
|
const subtotal = getValues('items.reduce(
|
||||||
(sum: number, item: any) => sum + item.subtotal_amount,
|
(sum: number, item: any) => sum + item.subtotal_amount,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
@ -41,7 +41,7 @@ export const InvoiceTotals = () => {
|
|||||||
taxes_amount: taxesAmount,
|
taxes_amount: taxesAmount,
|
||||||
total_amount: totalAmount,
|
total_amount: totalAmount,
|
||||||
});
|
});
|
||||||
};
|
};*/
|
||||||
|
|
||||||
const formatCurrency = (amount: number) => {
|
const formatCurrency = (amount: number) => {
|
||||||
return new Intl.NumberFormat("es-ES", {
|
return new Intl.NumberFormat("es-ES", {
|
||||||
@ -53,9 +53,9 @@ export const InvoiceTotals = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset>
|
<Fieldset {...props}>
|
||||||
<Legend className='flex items-center gap-2 text-foreground'>
|
<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>
|
</Legend>
|
||||||
|
|
||||||
<Description>{t("form_groups.totals.description")}</Description>
|
<Description>{t("form_groups.totals.description")}</Description>
|
||||||
@ -63,48 +63,54 @@ export const InvoiceTotals = () => {
|
|||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<Label className='text-sm text-muted-foreground'>Subtotal</Label>
|
<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>
|
||||||
|
|
||||||
<div className='flex justify-between items-center gap-4'>
|
<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'>
|
<div className='flex items-center gap-2'>
|
||||||
<Input
|
<Controller
|
||||||
type='number'
|
control={control}
|
||||||
step='0.01'
|
name={"discount_percentage"}
|
||||||
value={invoice.discount_percentage}
|
render={({
|
||||||
onChange={(e) => updateDiscount(Number.parseFloat(e.target.value) || 0)}
|
field, fieldState
|
||||||
className='w-20 text-right'
|
}) => (<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>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<Label className='text-sm text-muted-foreground'>Importe Descuento</Label>
|
<Label className='text-sm text-muted-foreground'>Importe del descuento</Label>
|
||||||
<span className='font-medium text-destructive'>
|
<span className='font-medium text-destructive tabular-nums'>
|
||||||
-{formatCurrency(invoice.discount_amount)}
|
-{formatCurrency(getValues("discount_amount"))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<Label className='text-sm text-muted-foreground'>Base Imponible</Label>
|
<Label className='text-sm text-muted-foreground'>Base imponible</Label>
|
||||||
<span className='font-medium'>{formatCurrency(invoice.taxable_amount)}</span>
|
<span className='font-medium tabular-nums'>{formatCurrency(getValues('taxable_amount'))}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<Label className='text-sm text-muted-foreground'>Total Impuestos</Label>
|
<Label className='text-sm text-muted-foreground'>Total de impuestos</Label>
|
||||||
<span className='font-medium'>{formatCurrency(invoice.taxes_amount)}</span>
|
<span className='font-medium tabular-nums'>{formatCurrency(getValues('taxes_amount'))}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<Label className='text-lg font-semibold'>Total Factura</Label>
|
<Label className='text-lg font-semibold'>Total de la factura</Label>
|
||||||
<span className='text-xl font-bold text-primary'>
|
<span className='text-xl font-bold text-primary tabular-nums'>
|
||||||
{formatCurrency(invoice.total_amount)}
|
{formatCurrency(getValues('total_amount'))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Trash2 } from "lucide-react";
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { CustomerInvoiceFormData } from "../../../schemas";
|
import { InvoiceFormData } from "../../../schemas";
|
||||||
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
||||||
import { CustomItemViewProps } from "./types";
|
import { CustomItemViewProps } from "./types";
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ const formatCurrency = (amount: number) => {
|
|||||||
|
|
||||||
export const BlocksView = ({ items, removeItem, updateItem }: BlocksViewProps) => {
|
export const BlocksView = ({ items, removeItem, updateItem }: BlocksViewProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerInvoiceFormData>();
|
const { control } = useFormContext<InvoiceFormData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-4'>
|
<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 { ArrowDownIcon, ArrowUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||||
import { Control, Controller } from "react-hook-form";
|
import { Control, Controller } from "react-hook-form";
|
||||||
import { useTranslation } from '../../../i18n';
|
import { useTranslation } from '../../../i18n';
|
||||||
import { CustomerInvoiceItemFormData } from '../../../schemas';
|
import { InvoiceItemFormData } from '../../../schemas';
|
||||||
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
||||||
import { AmountDTOInputField } from './amount-dto-input-field';
|
import { AmountDTOInputField } from './amount-dto-input-field';
|
||||||
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
||||||
@ -11,7 +11,7 @@ import { QuantityDTOInputField } from './quantity-dto-input-field';
|
|||||||
|
|
||||||
export type ItemRowProps = {
|
export type ItemRowProps = {
|
||||||
control: Control,
|
control: Control,
|
||||||
item: CustomerInvoiceItemFormData;
|
item: InvoiceItemFormData;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isFirst: boolean;
|
isFirst: boolean;
|
||||||
@ -47,7 +47,7 @@ export const ItemRow = ({
|
|||||||
<div className='h-5'>
|
<div className='h-5'>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
aria-label={`Seleccionar fila ${rowIndex + 1}`}
|
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}
|
checked={isSelected}
|
||||||
onCheckedChange={onToggleSelect}
|
onCheckedChange={onToggleSelect}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
|
|||||||
@ -4,14 +4,14 @@ import * as React from "react";
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import { useItemsTableNavigation } from '../../../hooks';
|
import { useItemsTableNavigation } from '../../../hooks';
|
||||||
import { useTranslation } from '../../../i18n';
|
import { useTranslation } from '../../../i18n';
|
||||||
import { CustomerInvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
import { InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||||
import { ItemRow } from './item-row';
|
import { ItemRow } from './item-row';
|
||||||
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
||||||
import { LastCellTabHook } from './last-cell-tab-hook';
|
import { LastCellTabHook } from './last-cell-tab-hook';
|
||||||
|
|
||||||
interface ItemsEditorProps {
|
interface ItemsEditorProps {
|
||||||
value?: CustomerInvoiceItemFormData[];
|
value?: InvoiceItemFormData[];
|
||||||
onChange?: (items: CustomerInvoiceItemFormData[]) => void;
|
onChange?: (items: InvoiceItemFormData[]) => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { useMoney } from '@erp/core/hooks';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from '../../../i18n';
|
import { useTranslation } from '../../../i18n';
|
||||||
import { CustomerInvoiceItemFormData } from '../../../schemas';
|
import { InvoiceItemFormData } from '../../../schemas';
|
||||||
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
||||||
import { CustomItemViewProps } from "./types";
|
import { CustomItemViewProps } from "./types";
|
||||||
|
|
||||||
@ -28,25 +28,25 @@ export interface TableViewProps extends CustomItemViewProps { }
|
|||||||
|
|
||||||
export const TableView = ({ items, actions }: TableViewProps) => {
|
export const TableView = ({ items, actions }: TableViewProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerInvoiceItemFormData>();
|
const { control } = useFormContext<InvoiceItemFormData>();
|
||||||
const { format } = useMoney();
|
const { format } = useMoney();
|
||||||
const [lines, setLines] = useState<CustomerInvoiceItemFormData[]>(items);
|
const [lines, setLines] = useState<InvoiceItemFormData[]>(items);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLines(items)
|
setLines(items)
|
||||||
}, [items])
|
}, [items])
|
||||||
|
|
||||||
// Mantiene sincronía con el formulario padre
|
// Mantiene sincronía con el formulario padre
|
||||||
const updateItems = (updated: CustomerInvoiceItemFormData[]) => {
|
const updateItems = (updated: InvoiceItemFormData[]) => {
|
||||||
setLines(updated);
|
setLines(updated);
|
||||||
onItemsChange(updated);
|
onItemsChange(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 🔹 Actualiza una fila con recalculo */
|
/** 🔹 Actualiza una fila con recalculo */
|
||||||
const updateItem = (index: number, patch: Partial<CustomerInvoiceItemFormData>) => {
|
const updateItem = (index: number, patch: Partial<InvoiceItemFormData>) => {
|
||||||
const newItems = [...lines];
|
const newItems = [...lines];
|
||||||
const merged = { ...newItems[index], ...patch };
|
const merged = { ...newItems[index], ...patch };
|
||||||
newItems[index] = calculateItemAmounts(merged as CustomerInvoiceItemFormData);
|
newItems[index] = calculateItemAmounts(merged as InvoiceItemFormData);
|
||||||
updateItems(newItems);
|
updateItems(newItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -79,8 +79,8 @@ export const TableView = ({ items, actions }: TableViewProps) => {
|
|||||||
|
|
||||||
/** 🔹 Añade una nueva línea vacía */
|
/** 🔹 Añade una nueva línea vacía */
|
||||||
const addNewItem = () => {
|
const addNewItem = () => {
|
||||||
const newItem: CustomerInvoiceItemFormData = {
|
const newItem: InvoiceItemFormData = {
|
||||||
isNonValued: false,
|
is_non_valued: false,
|
||||||
description: "",
|
description: "",
|
||||||
quantity: { value: "0", scale: "2" },
|
quantity: { value: "0", scale: "2" },
|
||||||
unit_amount: { value: "0", scale: "2", currency_code: "EUR" },
|
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 { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { UserIcon } from "lucide-react";
|
import { UserIcon } from "lucide-react";
|
||||||
|
import { ComponentProps } from 'react';
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { RecipientModalSelectorField } from "./recipient-modal-selector-field";
|
import { RecipientModalSelectorField } from "./recipient-modal-selector-field";
|
||||||
|
|
||||||
export const InvoiceRecipient = () => {
|
export const InvoiceRecipient = (props: ComponentProps<"fieldset">) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control, getValues } = useFormContext();
|
const { control, getValues } = useFormContext();
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ export const InvoiceRecipient = () => {
|
|||||||
const recipient = getValues('recipient');
|
const recipient = getValues('recipient');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset>
|
<Fieldset {...props}>
|
||||||
<Legend className='flex items-center gap-2 text-foreground'>
|
<Legend className='flex items-center gap-2 text-foreground'>
|
||||||
<UserIcon className='size-5' /> {t("form_groups.customer.title")}
|
<UserIcon className='size-5' /> {t("form_groups.customer.title")}
|
||||||
</Legend>
|
</Legend>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { PropsWithChildren, createContext, useCallback, useContext, useMemo, use
|
|||||||
|
|
||||||
export type InvoiceContextValue = {
|
export type InvoiceContextValue = {
|
||||||
company_id: string;
|
company_id: string;
|
||||||
|
status: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
language_code: string;
|
language_code: string;
|
||||||
is_proforma: boolean;
|
is_proforma: boolean;
|
||||||
@ -15,13 +16,14 @@ const InvoiceContext = createContext<InvoiceContextValue | null>(null);
|
|||||||
|
|
||||||
export interface InvoiceProviderParams {
|
export interface InvoiceProviderParams {
|
||||||
company_id: string;
|
company_id: string;
|
||||||
|
status: string; // default "draft"
|
||||||
language_code?: string; // default "es"
|
language_code?: string; // default "es"
|
||||||
currency_code?: string; // default "EUR"
|
currency_code?: string; // default "EUR"
|
||||||
is_proforma?: boolean; // default 'true'
|
is_proforma?: boolean; // default 'true'
|
||||||
children: React.ReactNode;
|
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",
|
currency_code: initialCurrency = "EUR",
|
||||||
is_proforma: initialProforma = true, children }: PropsWithChildren<InvoiceProviderParams>) => {
|
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 [language_code, setLanguage] = useState(initialLang);
|
||||||
const [currency_code, setCurrency] = useState(initialCurrency);
|
const [currency_code, setCurrency] = useState(initialCurrency);
|
||||||
const [is_proforma, setIsProforma] = useState(initialProforma);
|
const [is_proforma, setIsProforma] = useState(initialProforma);
|
||||||
|
const [status] = useState(initialStatus);
|
||||||
|
|
||||||
// Callbacks memoizados
|
// Callbacks memoizados
|
||||||
const setLanguageMemo = useCallback((language_code: string) => setLanguage(language_code), []);
|
const setLanguageMemo = useCallback((language_code: string) => setLanguage(language_code), []);
|
||||||
@ -39,6 +42,7 @@ export const InvoiceProvider = ({ company_id, language_code: initialLang = "es",
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
company_id,
|
company_id,
|
||||||
|
status,
|
||||||
language_code,
|
language_code,
|
||||||
currency_code,
|
currency_code,
|
||||||
is_proforma,
|
is_proforma,
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const CustomerInvoiceAdd = lazy(() =>
|
|||||||
import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate }))
|
import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate }))
|
||||||
);
|
);
|
||||||
const CustomerInvoiceUpdate = lazy(() =>
|
const CustomerInvoiceUpdate = lazy(() =>
|
||||||
import("./pages").then((m) => ({ default: m.CustomerInvoiceUpdatePage }))
|
import("./pages").then((m) => ({ default: m.InvoiceUpdatePage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => {
|
export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { MoneyDTO } from "@erp/core";
|
import { MoneyDTO } from "@erp/core";
|
||||||
import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
|
import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { CustomerInvoiceItemFormData } from "../../schemas";
|
import { InvoiceItemFormData } from "../../schemas";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcula totales derivados de un ítem de factura
|
* 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.
|
* 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 moneyHelper = useMoney();
|
||||||
const qtyHelper = useQuantity();
|
const qtyHelper = useQuantity();
|
||||||
const pctHelper = usePercentage();
|
const pctHelper = usePercentage();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { MoneyDTO } from "@erp/core";
|
import { MoneyDTO } from "@erp/core";
|
||||||
import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
|
import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { CustomerInvoiceItemFormData } from "../../schemas";
|
import { InvoiceItemFormData } from "../../schemas";
|
||||||
|
|
||||||
export type InvoiceTotals = Readonly<{
|
export type InvoiceTotals = Readonly<{
|
||||||
subtotal: number;
|
subtotal: number;
|
||||||
@ -23,9 +23,7 @@ export type InvoiceTotals = Readonly<{
|
|||||||
/**
|
/**
|
||||||
* Calcula los totales generales de la factura a partir de sus líneas.
|
* Calcula los totales generales de la factura a partir de sus líneas.
|
||||||
*/
|
*/
|
||||||
export function useCalcInvoiceTotals(
|
export function useCalcInvoiceTotals(items: InvoiceItemFormData[] | undefined): InvoiceTotals {
|
||||||
items: CustomerInvoiceItemFormData[] | undefined
|
|
||||||
): InvoiceTotals {
|
|
||||||
const money = useMoney();
|
const money = useMoney();
|
||||||
const qty = useQuantity();
|
const qty = useQuantity();
|
||||||
const pct = usePercentage();
|
const pct = usePercentage();
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import { areMoneyDTOEqual } from "@erp/core";
|
|||||||
import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
|
import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
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
|
* Hook que recalcula automáticamente los totales de cada línea
|
||||||
* y los totales generales de la factura cuando cambian los valores relevantes.
|
* 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 {
|
const {
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
@ -22,7 +22,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
|||||||
|
|
||||||
// Cálculo de una línea
|
// Cálculo de una línea
|
||||||
const calculateItemTotals = React.useCallback(
|
const calculateItemTotals = React.useCallback(
|
||||||
(item: CustomerInvoiceItemFormData) => {
|
(item: InvoiceItemFormData) => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
const zero = moneyHelper.fromNumber(0);
|
const zero = moneyHelper.fromNumber(0);
|
||||||
return {
|
return {
|
||||||
@ -65,7 +65,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
|||||||
|
|
||||||
// Cálculo de los totales de la factura a partir de los conceptos
|
// Cálculo de los totales de la factura a partir de los conceptos
|
||||||
const calculateInvoiceTotals = React.useCallback(
|
const calculateInvoiceTotals = React.useCallback(
|
||||||
(items: CustomerInvoiceItemFormData[]) => {
|
(items: InvoiceItemFormData[]) => {
|
||||||
let subtotalDTO = moneyHelper.fromNumber(0);
|
let subtotalDTO = moneyHelper.fromNumber(0);
|
||||||
let discountTotalDTO = moneyHelper.fromNumber(0);
|
let discountTotalDTO = moneyHelper.fromNumber(0);
|
||||||
let taxableBaseDTO = moneyHelper.fromNumber(0);
|
let taxableBaseDTO = moneyHelper.fromNumber(0);
|
||||||
@ -106,7 +106,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
|||||||
formData.items.forEach((item, i) => {
|
formData.items.forEach((item, i) => {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
const typedItem = item as CustomerInvoiceItemFormData;
|
const typedItem = item as InvoiceItemFormData;
|
||||||
const totals = calculateItemTotals(typedItem);
|
const totals = calculateItemTotals(typedItem);
|
||||||
const current = getValues(`items.${i}.total_amount`);
|
const current = getValues(`items.${i}.total_amount`);
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
|||||||
|
|
||||||
// Recalcular importes totales de la factura y
|
// Recalcular importes totales de la factura y
|
||||||
// actualizar valores calculados.
|
// actualizar valores calculados.
|
||||||
const typedItems = formData.items as CustomerInvoiceItemFormData[];
|
const typedItems = formData.items as InvoiceItemFormData[];
|
||||||
const totalsGlobal = calculateInvoiceTotals(typedItems);
|
const totalsGlobal = calculateInvoiceTotals(typedItems);
|
||||||
|
|
||||||
setValue("subtotal_amount", totalsGlobal.subtotalDTO);
|
setValue("subtotal_amount", totalsGlobal.subtotalDTO);
|
||||||
@ -136,7 +136,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
|||||||
const fieldName = name.split(".")[2];
|
const fieldName = name.split(".")[2];
|
||||||
|
|
||||||
if (["quantity", "unit_amount", "discount_percentage"].includes(fieldName)) {
|
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;
|
if (!typedItem) return;
|
||||||
|
|
||||||
// Recalcular línea
|
// Recalcular línea
|
||||||
@ -152,7 +152,7 @@ export function useInvoiceAutoRecalc(form: UseFormReturn<CustomerInvoiceFormData
|
|||||||
|
|
||||||
// Recalcular importes totales de la factura y
|
// Recalcular importes totales de la factura y
|
||||||
// actualizar valores calculados.
|
// actualizar valores calculados.
|
||||||
const typedItems = formData.items as CustomerInvoiceItemFormData[];
|
const typedItems = formData.items as InvoiceItemFormData[];
|
||||||
const totalsGlobal = calculateInvoiceTotals(typedItems);
|
const totalsGlobal = calculateInvoiceTotals(typedItems);
|
||||||
|
|
||||||
setValue("subtotal_amount", totalsGlobal.subtotalDTO);
|
setValue("subtotal_amount", totalsGlobal.subtotalDTO);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export * from "./calcs";
|
export * from "./calcs";
|
||||||
export * from "./use-create-customer-invoice-mutation";
|
export * from "./use-create-customer-invoice-mutation";
|
||||||
export * from "./use-customer-invoice-query";
|
|
||||||
export * from "./use-customer-invoices-query";
|
export * from "./use-customer-invoices-query";
|
||||||
export * from "./use-detail-columns";
|
export * from "./use-detail-columns";
|
||||||
|
export * from "./use-invoice-query";
|
||||||
export * from "./use-items-table-navigation";
|
export * from "./use-items-table-navigation";
|
||||||
export * from "./use-update-customer-invoice-mutation";
|
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 { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { CreateCustomerInvoiceRequestSchema } from "../../common";
|
import { CreateCustomerInvoiceRequestSchema } from "../../common";
|
||||||
import { CustomerInvoice, CustomerInvoiceFormData } from "../schemas";
|
import { CustomerInvoice, InvoiceFormData } from "../schemas";
|
||||||
|
|
||||||
type CreateCustomerInvoicePayload = {
|
type CreateCustomerInvoicePayload = {
|
||||||
data: CustomerInvoiceFormData;
|
data: InvoiceFormData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateCustomerInvoiceMutation = () => {
|
export const useCreateCustomerInvoiceMutation = () => {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ type CustomerInvoiceQueryOptions = {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useCustomerInvoiceQuery(invoiceId?: string, options?: CustomerInvoiceQueryOptions) {
|
export function useInvoiceQuery(invoiceId?: string, options?: CustomerInvoiceQueryOptions) {
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const enabled = (options?.enabled ?? true) && !!invoiceId;
|
const enabled = (options?.enabled ?? true) && !!invoiceId;
|
||||||
|
|
||||||
@ -5,8 +5,8 @@ import {
|
|||||||
UpdateCustomerInvoiceByIdRequestDTO,
|
UpdateCustomerInvoiceByIdRequestDTO,
|
||||||
UpdateCustomerInvoiceByIdRequestSchema,
|
UpdateCustomerInvoiceByIdRequestSchema,
|
||||||
} from "../../common";
|
} from "../../common";
|
||||||
import { CustomerInvoiceFormData } from "../schemas";
|
import { InvoiceFormData } from "../schemas";
|
||||||
import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-customer-invoice-query";
|
import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-invoice-query";
|
||||||
|
|
||||||
export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const;
|
export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const;
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ type UpdateCustomerInvoiceContext = {};
|
|||||||
|
|
||||||
type UpdateCustomerInvoicePayload = {
|
type UpdateCustomerInvoicePayload = {
|
||||||
id: string;
|
id: string;
|
||||||
data: Partial<CustomerInvoiceFormData>;
|
data: Partial<InvoiceFormData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useUpdateCustomerInvoice() {
|
export function useUpdateCustomerInvoice() {
|
||||||
@ -23,7 +23,7 @@ export function useUpdateCustomerInvoice() {
|
|||||||
const schema = UpdateCustomerInvoiceByIdRequestSchema;
|
const schema = UpdateCustomerInvoiceByIdRequestSchema;
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<
|
||||||
CustomerInvoiceFormData,
|
InvoiceFormData,
|
||||||
Error,
|
Error,
|
||||||
UpdateCustomerInvoicePayload,
|
UpdateCustomerInvoicePayload,
|
||||||
UpdateCustomerInvoiceContext
|
UpdateCustomerInvoiceContext
|
||||||
@ -53,9 +53,9 @@ export function useUpdateCustomerInvoice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updated = await dataSource.updateOne("customer-invoices", invoiceId, data);
|
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;
|
const { id: invoiceId } = variables;
|
||||||
|
|
||||||
// Refresca inmediatamente el detalle
|
// Refresca inmediatamente el detalle
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export * from "./customer-invoices-update-page";
|
export * from "./invoice-update-page";
|
||||||
|
|||||||
@ -17,15 +17,16 @@ import {
|
|||||||
PageHeader,
|
PageHeader,
|
||||||
} from "../../components";
|
} from "../../components";
|
||||||
import { InvoiceProvider } from '../../context';
|
import { InvoiceProvider } from '../../context';
|
||||||
import { useCustomerInvoiceQuery, useInvoiceAutoRecalc, useUpdateCustomerInvoice } from "../../hooks";
|
import { useInvoiceQuery, useUpdateCustomerInvoice } from "../../hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
CustomerInvoiceFormData,
|
InvoiceFormData,
|
||||||
CustomerInvoiceFormSchema,
|
InvoiceFormSchema,
|
||||||
defaultCustomerInvoiceFormData
|
defaultCustomerInvoiceFormData,
|
||||||
|
invoiceDtoToFormAdapter
|
||||||
} from "../../schemas";
|
} from "../../schemas";
|
||||||
|
|
||||||
export const CustomerInvoiceUpdatePage = () => {
|
export const InvoiceUpdatePage = () => {
|
||||||
const invoiceId = useUrlParamId();
|
const invoiceId = useUrlParamId();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -36,7 +37,7 @@ export const CustomerInvoiceUpdatePage = () => {
|
|||||||
isLoading: isLoadingInvoice,
|
isLoading: isLoadingInvoice,
|
||||||
isError: isLoadError,
|
isError: isLoadError,
|
||||||
error: loadError,
|
error: loadError,
|
||||||
} = useCustomerInvoiceQuery(invoiceId, { enabled: !!invoiceId });
|
} = useInvoiceQuery(invoiceId, { enabled: !!invoiceId });
|
||||||
|
|
||||||
// 2) Estado de actualización (mutación)
|
// 2) Estado de actualización (mutación)
|
||||||
const {
|
const {
|
||||||
@ -47,16 +48,17 @@ export const CustomerInvoiceUpdatePage = () => {
|
|||||||
} = useUpdateCustomerInvoice();
|
} = useUpdateCustomerInvoice();
|
||||||
|
|
||||||
// 3) Form hook
|
// 3) Form hook
|
||||||
const form = useHookForm<CustomerInvoiceFormData>({
|
const form = useHookForm<InvoiceFormData>({
|
||||||
resolverSchema: CustomerInvoiceFormSchema,
|
resolverSchema: InvoiceFormSchema,
|
||||||
initialValues: (invoiceData as unknown as CustomerInvoiceFormData) ?? defaultCustomerInvoiceFormData,
|
defaultValues: defaultCustomerInvoiceFormData,
|
||||||
|
values: invoiceData ? invoiceDtoToFormAdapter.fromDto(invoiceData) : undefined,
|
||||||
disabled: isUpdating,
|
disabled: isUpdating,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4) Activa recálculo automático de los totales de la factura cuando hay algún cambio en importes
|
// 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;
|
const { dirtyFields } = form.formState;
|
||||||
|
|
||||||
if (!formHasAnyDirty(dirtyFields)) {
|
if (!formHasAnyDirty(dirtyFields)) {
|
||||||
@ -74,7 +76,7 @@ export const CustomerInvoiceUpdatePage = () => {
|
|||||||
showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg"));
|
showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg"));
|
||||||
|
|
||||||
// 🔹 limpiar el form e isDirty pasa a false
|
// 🔹 limpiar el form e isDirty pasa a false
|
||||||
form.reset(data as unknown as CustomerInvoiceFormData);
|
form.reset(data as unknown as InvoiceFormData);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
showErrorToast(t("pages.update.errorTitle"), error.message);
|
showErrorToast(t("pages.update.errorTitle"), error.message);
|
||||||
@ -84,13 +86,13 @@ export const CustomerInvoiceUpdatePage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () =>
|
const handleReset = () =>
|
||||||
form.reset((invoiceData as unknown as CustomerInvoiceFormData) ?? defaultCustomerInvoiceFormData);
|
form.reset((invoiceData as unknown as InvoiceFormData) ?? defaultCustomerInvoiceFormData);
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (errors: FieldErrors<CustomerInvoiceFormData>) => {
|
const handleError = (errors: FieldErrors<InvoiceFormData>) => {
|
||||||
console.error("Errores en el formulario:", errors);
|
console.error("Errores en el formulario:", errors);
|
||||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||||
};
|
};
|
||||||
@ -136,6 +138,7 @@ export const CustomerInvoiceUpdatePage = () => {
|
|||||||
return (
|
return (
|
||||||
<InvoiceProvider
|
<InvoiceProvider
|
||||||
company_id={invoiceData.company_id}
|
company_id={invoiceData.company_id}
|
||||||
|
status={invoiceData.status}
|
||||||
language_code={invoiceData.language_code}
|
language_code={invoiceData.language_code}
|
||||||
currency_code={invoiceData.currency_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.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'>
|
<DialogContent className='max-w-md'>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
<DialogTitle className='flex items-center gap-2'>
|
||||||
<Plus className='h-5 w-5' />
|
<Plus className='size-5' />
|
||||||
Nuevo Cliente
|
Nuevo Cliente
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -248,7 +248,7 @@ const CustomerCard = ({ customer }: { customer: Customer }) => (
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className='p-4 space-y-2'>
|
<CardContent className='p-4 space-y-2'>
|
||||||
<div className='flex items-center gap-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>
|
<span className='font-semibold'>{customer.name}</span>
|
||||||
<Badge variant={customer.status === "Activo" ? "default" : "secondary"}>
|
<Badge variant={customer.status === "Activo" ? "default" : "secondary"}>
|
||||||
{customer.status}
|
{customer.status}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const CreateCustomerFormDialog = ({
|
|||||||
<DialogContent className='sm:max-w-[500px] bg-card border-border'>
|
<DialogContent className='sm:max-w-[500px] bg-card border-border'>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
<DialogTitle className='flex items-center gap-2'>
|
||||||
<Plus className='h-5 w-5' /> Agregar Nuevo Cliente
|
<Plus className='size-5' /> Agregar Nuevo Cliente
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Complete la información del cliente. Los campos marcados con * son obligatorios.
|
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'>
|
<DialogHeader className='px-6 pt-6 pb-4'>
|
||||||
<DialogTitle className='flex items-center justify-between'>
|
<DialogTitle className='flex items-center justify-between'>
|
||||||
<span className='flex items-center gap-2'>
|
<span className='flex items-center gap-2'>
|
||||||
<User className='h-5 w-5' />
|
<User className='size-5' />
|
||||||
Seleccionar Cliente
|
Seleccionar Cliente
|
||||||
</span>
|
</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|||||||
@ -102,7 +102,7 @@ export const CustomerViewPage = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
<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
|
Información Básica
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -136,7 +136,7 @@ export const CustomerViewPage = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
<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
|
Dirección
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -180,7 +180,7 @@ export const CustomerViewPage = () => {
|
|||||||
<Card className='md:col-span-2'>
|
<Card className='md:col-span-2'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
<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
|
Información de Contacto
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -306,7 +306,7 @@ export const CustomerViewPage = () => {
|
|||||||
<Card className='md:col-span-2'>
|
<Card className='md:col-span-2'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||||
<Languages className='h-5 w-5 text-primary' />
|
<Languages className='size-5 text-primary' />
|
||||||
Preferencias
|
Preferencias
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export const GetVerifactuRecordByIdResponseSchema = z.object({
|
|||||||
items: z.array(
|
items: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
isNonValued: z.string(),
|
is_non_valued: z.string(),
|
||||||
position: z.string(),
|
position: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
quantity: QuantitySchema,
|
quantity: QuantitySchema,
|
||||||
|
|||||||
@ -28,6 +28,8 @@ type TextAreaFieldProps<TFormValues extends FieldValues> = CommonInputProps & {
|
|||||||
|
|
||||||
/** Contador de caracteres (si usas maxLength) */
|
/** Contador de caracteres (si usas maxLength) */
|
||||||
showCounter?: boolean;
|
showCounter?: boolean;
|
||||||
|
maxLength?: number;
|
||||||
|
rows?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TextAreaField<TFormValues extends FieldValues>({
|
export function TextAreaField<TFormValues extends FieldValues>({
|
||||||
@ -42,6 +44,7 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
|||||||
className,
|
className,
|
||||||
showCounter = false,
|
showCounter = false,
|
||||||
maxLength,
|
maxLength,
|
||||||
|
rows = 3
|
||||||
}: TextAreaFieldProps<TFormValues>) {
|
}: TextAreaFieldProps<TFormValues>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isDisabled = disabled || readOnly;
|
const isDisabled = disabled || readOnly;
|
||||||
@ -57,7 +60,7 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
|||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className={cn("space-y-0", className)}>
|
<FormItem className={cn("space-y-0 flex flex-col ", className)}>
|
||||||
{label && (
|
{label && (
|
||||||
<div className='mb-1 flex justify-between gap-2'>
|
<div className='mb-1 flex justify-between gap-2'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
@ -81,8 +84,11 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
|||||||
<Textarea
|
<Textarea
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
placeholder={placeholder}
|
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}
|
maxLength={maxLength}
|
||||||
|
spellCheck={true}
|
||||||
|
rows={rows}
|
||||||
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@ -407,7 +407,7 @@ export function DataTable({
|
|||||||
Past Performance{" "}
|
Past Performance{" "}
|
||||||
<Badge
|
<Badge
|
||||||
variant='secondary'
|
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
|
3
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -416,7 +416,7 @@ export function DataTable({
|
|||||||
Key Personnel{" "}
|
Key Personnel{" "}
|
||||||
<Badge
|
<Badge
|
||||||
variant='secondary'
|
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
|
2
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@ -24,6 +24,18 @@ export class Collection<T> {
|
|||||||
this.totalItems = 0;
|
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.
|
* Agrega un nuevo elemento a la colección.
|
||||||
* @param item - Elemento a agregar.
|
* @param item - Elemento a agregar.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user