Edición de proformas y más
This commit is contained in:
parent
c9ba2d0370
commit
cfbc4e6657
@ -59,7 +59,7 @@ export const App = () => {
|
|||||||
<RouterProvider router={appRouter} />
|
<RouterProvider router={appRouter} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<Toaster expand={true} position="bottom-center" />
|
<Toaster expand={true} position="top-center" richColors />
|
||||||
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</DataSourceProvider>
|
</DataSourceProvider>
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import { z } from "zod/v4";
|
|||||||
export const TinSchema = z
|
export const TinSchema = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1, "TIN cannot be empty.")
|
.min(2, "TIN cannot be empty.")
|
||||||
.max(32, "TIN is too long.")
|
.max(10, "TIN is too long.")
|
||||||
.regex(/^(?=.*[A-Za-z0-9])[A-Za-z0-9.\-/ ]+$/, "TIN has an invalid format.");
|
.regex(/^(?=.*[A-Za-z0-9])[A-Za-z0-9.\-/ ]+$/, "TIN has an invalid format.");
|
||||||
|
|
||||||
export type TinDTO = z.infer<typeof TinSchema>;
|
export type TinDTO = z.infer<typeof TinSchema>;
|
||||||
|
|||||||
@ -70,8 +70,15 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
|||||||
data: TData,
|
data: TData,
|
||||||
params?: Record<string, unknown>
|
params?: Record<string, unknown>
|
||||||
): Promise<R> => {
|
): Promise<R> => {
|
||||||
|
const url = `${resource}/${id}`;
|
||||||
|
|
||||||
|
console.log("Axios updateOne => ", {
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
const res = await client.put<R, AxiosResponse<R>, TData>(
|
const res = await client.put<R, AxiosResponse<R>, TData>(
|
||||||
`${resource}/${id}`,
|
url,
|
||||||
data,
|
data,
|
||||||
params as AxiosRequestConfig<TData>
|
params as AxiosRequestConfig<TData>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,212 @@
|
|||||||
|
import type { FieldNamesMarkedBoolean } from "react-hook-form";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve el valor sin aplicar ninguna normalización.
|
||||||
|
*
|
||||||
|
* Propósito:
|
||||||
|
* - Permitir pasar valores ya alineados con el contrato de salida.
|
||||||
|
* - Evitar transformaciones innecesarias en casos triviales.
|
||||||
|
*
|
||||||
|
* Uso típico:
|
||||||
|
* - Campos que ya tienen exactamente el shape del patch.
|
||||||
|
* - Reemplazos completos de colecciones (`items`).
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - Solo usar cuando `Input === Output` (mismo tipo semántico).
|
||||||
|
* - No usar si hay diferencias de contrato (ej: `""` vs `null`).
|
||||||
|
* - No usar para ocultar transformaciones necesarias.
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* asIs(10) // 10
|
||||||
|
* asIs(true) // true
|
||||||
|
*
|
||||||
|
* // Caso válido: array ya en formato patch
|
||||||
|
* asIs([{ id: "1", description: null }])
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Ejemplo incorrecto:
|
||||||
|
* ```ts
|
||||||
|
* // ❌ description debería ser nullableString
|
||||||
|
* asIs({ description: "" })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @typeParam T - Tipo de entrada y salida.
|
||||||
|
* @param value - Valor sin normalizar.
|
||||||
|
* @returns Mismo valor sin modificaciones.
|
||||||
|
*/
|
||||||
|
export const asIs = <T>(value: T): T => value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte un string en un valor requerido normalizado.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - Elimina espacios laterales (`trim`)
|
||||||
|
* - Nunca devuelve `null` ni `undefined`
|
||||||
|
* - No valida contenido (eso pertenece a capa superior)
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* toRequiredString(" ABC ") // "ABC"
|
||||||
|
* toRequiredString("") // ""
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param value - Valor de entrada desde UI/form.
|
||||||
|
* @returns String normalizado.
|
||||||
|
*/
|
||||||
|
export const toRequiredString = (value: string): string => {
|
||||||
|
return value.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte un string en un valor nullable.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - Elimina espacios laterales (`trim`)
|
||||||
|
* - `""` o whitespace → `null`
|
||||||
|
* - valor con contenido → string normalizado
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* toNullableString(" ABC ") // "ABC"
|
||||||
|
* toNullableString("") // null
|
||||||
|
* toNullableString(" ") // null
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param value - Valor de entrada desde UI/form.
|
||||||
|
* @returns String normalizado o `null`.
|
||||||
|
*/
|
||||||
|
export const toNullableString = (value: string): string | null => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
|
||||||
|
return trimmed === "" ? null : trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaliza un número requerido.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - Devuelve el mismo valor
|
||||||
|
* - No valida rango ni precisión
|
||||||
|
* - No convierte `null` ni `undefined`
|
||||||
|
*
|
||||||
|
* Uso típico:
|
||||||
|
* - Campos numéricos obligatorios ya validados por el formulario
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* toNumber(10) // 10
|
||||||
|
* toNumber(0) // 0
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param value - Número de entrada.
|
||||||
|
* @returns Número sin transformación.
|
||||||
|
*/
|
||||||
|
export const toNumber = (value: number): number => {
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaliza un número nullable.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - `null` → `null`
|
||||||
|
* - número válido → mismo valor
|
||||||
|
* - No convierte `0` a null
|
||||||
|
*
|
||||||
|
* Uso típico:
|
||||||
|
* - Campos opcionales numéricos
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* toNullableNumber(10) // 10
|
||||||
|
* toNullableNumber(null) // null
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param value - Número o null.
|
||||||
|
* @returns Número o `null`.
|
||||||
|
*/
|
||||||
|
export const toNullableNumber = (value: number | null): number | null => {
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaliza un booleano.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - Devuelve el mismo valor
|
||||||
|
* - No realiza coerción (`"true"` → ❌)
|
||||||
|
*
|
||||||
|
* Uso típico:
|
||||||
|
* - Flags de UI ya tipados correctamente
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* toBoolean(true) // true
|
||||||
|
* toBoolean(false) // false
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param value - Booleano de entrada.
|
||||||
|
* @returns Booleano sin transformación.
|
||||||
|
*/
|
||||||
|
export const toBoolean = (value: boolean): boolean => {
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----
|
||||||
|
|
||||||
|
type FieldNormalizer<TInput, TOutput> = (value: TInput) => TOutput;
|
||||||
|
|
||||||
|
export type DirtyPatchSpec<TForm extends object, TPatch extends object> = {
|
||||||
|
[K in keyof TForm & keyof TPatch]?: FieldNormalizer<TForm[K], TPatch[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprueba si un campo top-level fue modificado.
|
||||||
|
*
|
||||||
|
* Se encapsula el cast porque `FieldNamesMarkedBoolean<T>` puede representar
|
||||||
|
* estructuras anidadas, pero este builder solo soporta claves top-level.
|
||||||
|
*/
|
||||||
|
const isTopLevelDirty = <TForm extends object>(
|
||||||
|
dirtyFields: FieldNamesMarkedBoolean<TForm>,
|
||||||
|
key: keyof TForm
|
||||||
|
): boolean => {
|
||||||
|
return Boolean((dirtyFields as Partial<Record<keyof TForm, unknown>>)[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construye un patch top-level a partir de `formData`, `dirtyFields` y una spec de normalizadores.
|
||||||
|
*
|
||||||
|
* Solo procesa claves declaradas en `spec`.
|
||||||
|
* Solo incluye campos marcados como dirty en primer nivel.
|
||||||
|
* No soporta paths anidados ni merge granular de arrays.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const patchSpec = {
|
||||||
|
* name: requiredString,
|
||||||
|
* notes: nullableString,
|
||||||
|
* items: raw,
|
||||||
|
* } satisfies DirtyPatchSpec<Form, Patch>;
|
||||||
|
*
|
||||||
|
* const patch = buildTopLevelDirtyPatch(formData, dirtyFields, patchSpec);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const buildTopLevelFormDirtyPatch = <TForm extends object, TPatch extends object>(
|
||||||
|
formData: TForm,
|
||||||
|
dirtyFields: FieldNamesMarkedBoolean<TForm>,
|
||||||
|
spec: DirtyPatchSpec<TForm, TPatch>
|
||||||
|
): TPatch => {
|
||||||
|
const patch: Partial<TPatch> = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(spec) as Array<keyof TForm & keyof TPatch>) {
|
||||||
|
if (!isTopLevelDirty(dirtyFields, key)) continue;
|
||||||
|
|
||||||
|
const normalize = spec[key];
|
||||||
|
if (!normalize) continue;
|
||||||
|
|
||||||
|
patch[key] = normalize(formData[key]) as TPatch[typeof key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return patch as TPatch;
|
||||||
|
};
|
||||||
@ -1,2 +1,3 @@
|
|||||||
|
export * from "./build-top-level-form-dirty-patch";
|
||||||
export * from "./form-utils";
|
export * from "./form-utils";
|
||||||
export * from "./http-url-utils";
|
export * from "./http-url-utils";
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { IModuleClient, ModuleClientParams } from "./lib";
|
import type { IModuleClient, ModuleClientParams } from "./lib";
|
||||||
|
|
||||||
export const MODULE_NAME = "Core";
|
export const MODULE_NAME = "Core";
|
||||||
const MODULE_VERSION = "1.0.0";
|
const MODULE_VERSION = "1.0.0";
|
||||||
|
|||||||
@ -50,6 +50,8 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
return Result.fail(updateResult.error);
|
return Result.fail(updateResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(proforma);
|
||||||
|
|
||||||
// Persistir cambios
|
// Persistir cambios
|
||||||
const saveResult = await this.repository.update(proforma, transaction);
|
const saveResult = await this.repository.update(proforma, transaction);
|
||||||
|
|
||||||
|
|||||||
@ -16,23 +16,19 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
|||||||
private readonly itemsBuilder: IProformaItemsFullSnapshotBuilder,
|
private readonly itemsBuilder: IProformaItemsFullSnapshotBuilder,
|
||||||
private readonly recipientBuilder: IProformaRecipientFullSnapshotBuilder,
|
private readonly recipientBuilder: IProformaRecipientFullSnapshotBuilder,
|
||||||
private readonly taxesBuilder: IProformaTaxesFullSnapshotBuilder
|
private readonly taxesBuilder: IProformaTaxesFullSnapshotBuilder
|
||||||
|
//private readonly paymentMethodBuilder: IProformaPaymentMethodFullSnapshotBuilder
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
toOutput(proforma: Proforma): GetProformaByIdResponseDTO {
|
toOutput(proforma: Proforma): GetProformaByIdResponseDTO {
|
||||||
const items = this.itemsBuilder.toOutput(proforma.items);
|
const items = this.itemsBuilder.toOutput(proforma.items);
|
||||||
const recipient = this.recipientBuilder.toOutput(proforma);
|
const recipient = this.recipientBuilder.toOutput(proforma);
|
||||||
const taxes = this.taxesBuilder.toOutput(proforma.taxes());
|
const taxes = this.taxesBuilder.toOutput(proforma.taxes());
|
||||||
|
//const paymentMethod = this.paymentMethodBuilder.toOutput(proforma.paymentMethod);
|
||||||
|
|
||||||
const payment = proforma.paymentMethod.match(
|
const paymentMethod = maybeToNullable(proforma.paymentMethodId, (value) => ({
|
||||||
(payment) => {
|
id: value.toString(),
|
||||||
const { id, payment_description } = payment.toObjectString();
|
description: "",
|
||||||
return {
|
}));
|
||||||
id: id,
|
|
||||||
description: payment_description,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
() => null
|
|
||||||
);
|
|
||||||
|
|
||||||
const allTotals = proforma.totals();
|
const allTotals = proforma.totals();
|
||||||
|
|
||||||
@ -59,7 +55,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
|||||||
|
|
||||||
linked_invoice_id: maybeToNullable(proforma.linkedInvoiceId, (value) => value.toString()),
|
linked_invoice_id: maybeToNullable(proforma.linkedInvoiceId, (value) => value.toString()),
|
||||||
|
|
||||||
payment_method: payment,
|
payment_method: paymentMethod,
|
||||||
|
|
||||||
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
|
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
|
||||||
items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(),
|
items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(),
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import type { ProformaItemDetailDTO } from "@erp/customer-invoices/common";
|
import type { ProformaItemDetailDTO } from "@erp/customer-invoices/common";
|
||||||
import {
|
import {
|
||||||
maybeToEmptyMoneyObjectString,
|
|
||||||
maybeToEmptyPercentageObjectString,
|
maybeToEmptyPercentageObjectString,
|
||||||
maybeToEmptyQuantityObjectString,
|
|
||||||
maybeToEmptyString,
|
maybeToEmptyString,
|
||||||
maybeToNullable,
|
maybeToNullable,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
@ -17,6 +15,7 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
|
|||||||
private mapItem(proformaItem: ProformaItem, index: number): ProformaItemDetailDTO {
|
private mapItem(proformaItem: ProformaItem, index: number): ProformaItemDetailDTO {
|
||||||
const allAmounts = proformaItem.totals();
|
const allAmounts = proformaItem.totals();
|
||||||
const isValued = proformaItem.isValued();
|
const isValued = proformaItem.isValued();
|
||||||
|
const currencyCode = proformaItem.currencyCode.code;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: proformaItem.id.toPrimitive(),
|
id: proformaItem.id.toPrimitive(),
|
||||||
@ -25,54 +24,58 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
|
|||||||
|
|
||||||
description: maybeToNullable(proformaItem.description, (value) => value.toString()),
|
description: maybeToNullable(proformaItem.description, (value) => value.toString()),
|
||||||
|
|
||||||
quantity: maybeToEmptyQuantityObjectString(proformaItem.quantity),
|
quantity: maybeToNullable(proformaItem.quantity, (value) => value.toObjectString()),
|
||||||
unit_amount: maybeToEmptyMoneyObjectString(proformaItem.unitAmount),
|
unit_amount: maybeToNullable(proformaItem.unitAmount, (value) => value.toObjectString()),
|
||||||
|
|
||||||
subtotal_amount: isValued
|
subtotal_amount: isValued
|
||||||
? allAmounts.subtotalAmount.toObjectString()
|
? allAmounts.subtotalAmount.toObjectString()
|
||||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
|
|
||||||
item_discount_percentage: maybeToEmptyPercentageObjectString(
|
item_discount_percentage: maybeToNullable(proformaItem.itemDiscountPercentage, (value) =>
|
||||||
proformaItem.itemDiscountPercentage
|
value.toObjectString()
|
||||||
),
|
),
|
||||||
item_discount_amount: isValued
|
item_discount_amount: isValued
|
||||||
? allAmounts.itemDiscountAmount.toObjectString()
|
? allAmounts.itemDiscountAmount.toObjectString()
|
||||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
|
|
||||||
global_discount_percentage: proformaItem.globalDiscountPercentage.toObjectString(),
|
global_discount_percentage: proformaItem.globalDiscountPercentage.toObjectString(),
|
||||||
|
|
||||||
global_discount_amount: isValued
|
global_discount_amount: isValued
|
||||||
? allAmounts.globalDiscountAmount.toObjectString()
|
? allAmounts.globalDiscountAmount.toObjectString()
|
||||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
|
|
||||||
total_discount_amount: isValued
|
total_discount_amount: isValued
|
||||||
? allAmounts.totalDiscountAmount.toObjectString()
|
? allAmounts.totalDiscountAmount.toObjectString()
|
||||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
|
|
||||||
taxable_amount: isValued
|
taxable_amount: isValued
|
||||||
? allAmounts.taxableAmount.toObjectString()
|
? allAmounts.taxableAmount.toObjectString()
|
||||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
|
|
||||||
iva_code: maybeToEmptyString(proformaItem.ivaCode()),
|
iva_code: maybeToEmptyString(proformaItem.ivaCode()),
|
||||||
iva_percentage: maybeToEmptyPercentageObjectString(proformaItem.ivaPercentage()),
|
iva_percentage: maybeToEmptyPercentageObjectString(proformaItem.ivaPercentage()),
|
||||||
iva_amount: isValued ? allAmounts.ivaAmount.toObjectString() : ItemAmount.EMPTY_MONEY_OBJECT,
|
iva_amount: isValued
|
||||||
|
? allAmounts.ivaAmount.toObjectString()
|
||||||
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
|
|
||||||
rec_code: maybeToEmptyString(proformaItem.recCode()),
|
rec_code: maybeToEmptyString(proformaItem.recCode()),
|
||||||
rec_percentage: maybeToEmptyPercentageObjectString(proformaItem.recPercentage()),
|
rec_percentage: maybeToEmptyPercentageObjectString(proformaItem.recPercentage()),
|
||||||
rec_amount: isValued ? allAmounts.recAmount.toObjectString() : ItemAmount.EMPTY_MONEY_OBJECT,
|
rec_amount: isValued
|
||||||
|
? allAmounts.recAmount.toObjectString()
|
||||||
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
|
|
||||||
retention_code: maybeToEmptyString(proformaItem.retentionCode()),
|
retention_code: maybeToEmptyString(proformaItem.retentionCode()),
|
||||||
retention_percentage: maybeToEmptyPercentageObjectString(proformaItem.retentionPercentage()),
|
retention_percentage: maybeToEmptyPercentageObjectString(proformaItem.retentionPercentage()),
|
||||||
retention_amount: isValued
|
retention_amount: isValued
|
||||||
? allAmounts.retentionAmount.toObjectString()
|
? allAmounts.retentionAmount.toObjectString()
|
||||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
|
|
||||||
taxes_amount: isValued
|
taxes_amount: isValued
|
||||||
? allAmounts.taxesAmount.toObjectString()
|
? allAmounts.taxesAmount.toObjectString()
|
||||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
total_amount: isValued
|
total_amount: isValued
|
||||||
? allAmounts.totalAmount.toObjectString()
|
? allAmounts.totalAmount.toObjectString()
|
||||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -151,7 +151,9 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
|||||||
return Result.ok(proforma);
|
return Result.ok(proforma);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static validateCreateProps(props: IProformaCreateProps): Result<void, Error> {
|
private static validateCreateProps(
|
||||||
|
props: IProformaCreateProps | ProformaInternalProps
|
||||||
|
): Result<void, Error> {
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +171,14 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
|||||||
...otherProps,
|
...otherProps,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(candidateProps);
|
||||||
|
|
||||||
// Validacciones
|
// Validacciones
|
||||||
|
const validationResult = Proforma.validateCreateProps(candidateProps);
|
||||||
|
|
||||||
|
if (validationResult.isFailure) {
|
||||||
|
return Result.fail(validationResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
// Aplicar cambios
|
// Aplicar cambios
|
||||||
Object.assign(this.props, candidateProps);
|
Object.assign(this.props, candidateProps);
|
||||||
|
|||||||
@ -10,12 +10,12 @@ export const ProformaItemDetailSchema = z.object({
|
|||||||
position: ItemPositionSchema,
|
position: ItemPositionSchema,
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
|
|
||||||
quantity: QuantitySchema,
|
quantity: QuantitySchema.nullable(),
|
||||||
unit_amount: MoneySchema,
|
unit_amount: MoneySchema.nullable(),
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
subtotal_amount: MoneySchema,
|
||||||
|
|
||||||
item_discount_percentage: PercentageSchema,
|
item_discount_percentage: PercentageSchema.nullable(),
|
||||||
item_discount_amount: MoneySchema,
|
item_discount_amount: MoneySchema,
|
||||||
|
|
||||||
global_discount_percentage: PercentageSchema,
|
global_discount_percentage: PercentageSchema,
|
||||||
|
|||||||
@ -24,7 +24,6 @@ export const GetProformaByIdAdapter = {
|
|||||||
return {
|
return {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
companyId: dto.company_id,
|
companyId: dto.company_id,
|
||||||
isProforma: dto.is_proforma === "1",
|
|
||||||
|
|
||||||
invoiceNumber: dto.invoice_number,
|
invoiceNumber: dto.invoice_number,
|
||||||
status: dto.status as ProformaStatus,
|
status: dto.status as ProformaStatus,
|
||||||
@ -69,6 +68,8 @@ const mapItem = (dto: GetProformaByIdResponseDTO["items"][number]): ProformaItem
|
|||||||
return {
|
return {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
position: Number(dto.position),
|
position: Number(dto.position),
|
||||||
|
isValued: dto.is_valued,
|
||||||
|
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
|
|
||||||
quantity: QuantityDTOHelper.toNumber(dto.quantity),
|
quantity: QuantityDTOHelper.toNumber(dto.quantity),
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
export interface ProformaItem {
|
export interface ProformaItem {
|
||||||
id: string;
|
id: string;
|
||||||
position: number;
|
position: number;
|
||||||
|
isValued: boolean;
|
||||||
|
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|||||||
@ -33,7 +33,12 @@ export const useProformaUpdateMutation = () => {
|
|||||||
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Hola!!!");
|
||||||
|
|
||||||
const dto: UpdateProformaByIdResult = await updateProformaById(dataSource, params);
|
const dto: UpdateProformaByIdResult = await updateProformaById(dataSource, params);
|
||||||
|
|
||||||
|
console.log(dto);
|
||||||
|
|
||||||
return GetProformaByIdAdapter.fromDto(dto);
|
return GetProformaByIdAdapter.fromDto(dto);
|
||||||
},
|
},
|
||||||
onSuccess: async (proforma) => {
|
onSuccess: async (proforma) => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { ProformaItem } from "../../shared";
|
|||||||
import type { ProformaItemUpdateForm } from "../entities";
|
import type { ProformaItemUpdateForm } from "../entities";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapea un cliente a un formulario de actualización de cliente.
|
* Mapea una linea de proforma a un formulario de actualización de cliente.
|
||||||
*
|
*
|
||||||
* @param proforma
|
* @param proforma
|
||||||
* @returns
|
* @returns
|
||||||
@ -14,6 +14,7 @@ export const mapProformaItemsToProformaItemsUpdateForm = (
|
|||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
position: item.position,
|
position: item.position,
|
||||||
|
isValued: item.isValued,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
unitAmount: item.unitAmount,
|
unitAmount: item.unitAmount,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { ProformaUpdateForm } from "../entities";
|
|||||||
import { mapProformaItemsToProformaItemsUpdateForm } from "./proforma-items-to-proforma-items-update-form.adapter";
|
import { mapProformaItemsToProformaItemsUpdateForm } from "./proforma-items-to-proforma-items-update-form.adapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapea un cliente a un formulario de actualización de cliente.
|
* Mapea una proforma a un formulario de actualización de proforma.
|
||||||
*
|
*
|
||||||
* @param proforma
|
* @param proforma
|
||||||
* @returns
|
* @returns
|
||||||
|
|||||||
@ -2,6 +2,13 @@ import type { CustomerSelectionOption } from "@erp/customers/common";
|
|||||||
|
|
||||||
import type { Proforma } from "../../shared/entities";
|
import type { Proforma } from "../../shared/entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrae de la proforma el cliente y lo trata como cliente seleccionado para esa proforma.
|
||||||
|
*
|
||||||
|
* @param proforma
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
export const mapProformaToSelectedCustomer = (
|
export const mapProformaToSelectedCustomer = (
|
||||||
proforma?: Proforma | null
|
proforma?: Proforma | null
|
||||||
): CustomerSelectionOption | null => {
|
): CustomerSelectionOption | null => {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
import { applyValidationErrorCollection } from "@erp/core";
|
||||||
import { formHasAnyDirty } from "@erp/core/client";
|
import { formHasAnyDirty } from "@erp/core/client";
|
||||||
import { useHookForm } from "@erp/core/hooks";
|
import { useHookForm } from "@erp/core/hooks";
|
||||||
import type { CustomerSelectionOption } from "@erp/customers";
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
|
import { type ValidationErrorCollection, isValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
||||||
import { useEffect, useId, useMemo, useState } from "react";
|
import { useEffect, useId, useMemo, useState } from "react";
|
||||||
import type { FieldErrors } from "react-hook-form";
|
import type { FieldErrors } from "react-hook-form";
|
||||||
@ -15,7 +17,7 @@ import {
|
|||||||
buildProformaUpdateDefault,
|
buildProformaUpdateDefault,
|
||||||
buildProformaUpdatePatch,
|
buildProformaUpdatePatch,
|
||||||
buildUpdateProformaByIdParams,
|
buildUpdateProformaByIdParams,
|
||||||
focusFirstProformaUpdateError,
|
focusFirstProformaUpdateFormError,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
import { useUpdateProformaItemsController } from "./use-update-proforma-items-controller";
|
import { useUpdateProformaItemsController } from "./use-update-proforma-items-controller";
|
||||||
@ -28,6 +30,18 @@ export interface UseUpdateProformaControllerOptions {
|
|||||||
errorToasts?: boolean;
|
errorToasts?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
|
||||||
|
if (isValidationErrorCollection(error)) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Error("Unknown error");
|
||||||
|
};
|
||||||
|
|
||||||
export const useUpdateProformaController = (
|
export const useUpdateProformaController = (
|
||||||
proformaId?: string,
|
proformaId?: string,
|
||||||
options?: UseUpdateProformaControllerOptions
|
options?: UseUpdateProformaControllerOptions
|
||||||
@ -66,6 +80,7 @@ export const useUpdateProformaController = (
|
|||||||
|
|
||||||
const [selectedCustomer, setSelectedCustomer] = useState<CustomerSelectionOption | null>(null);
|
const [selectedCustomer, setSelectedCustomer] = useState<CustomerSelectionOption | null>(null);
|
||||||
|
|
||||||
|
/** Reiniciar el form al recibir datos */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!proformaData) return;
|
if (!proformaData) return;
|
||||||
|
|
||||||
@ -115,8 +130,6 @@ export const useUpdateProformaController = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(form.formState.dirtyFields);
|
|
||||||
|
|
||||||
if (!formHasAnyDirty(form.formState.dirtyFields)) {
|
if (!formHasAnyDirty(form.formState.dirtyFields)) {
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
t("proformas.update.no_changes.title"),
|
t("proformas.update.no_changes.title"),
|
||||||
@ -125,11 +138,24 @@ export const useUpdateProformaController = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousData = proformaData;
|
//const previousData = proformaData;
|
||||||
|
|
||||||
const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields);
|
const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields);
|
||||||
|
|
||||||
|
if (Object.keys(patchData).length === 0) {
|
||||||
|
showWarningToast(
|
||||||
|
t("pages.update.no_changes.title", "Sin cambios"),
|
||||||
|
t("pages.update.no_changes.message", "No has realizado ningún cambio.")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Parche de actualización construido:", patchData);
|
||||||
|
|
||||||
const params = buildUpdateProformaByIdParams(proformaId, patchData);
|
const params = buildUpdateProformaByIdParams(proformaId, patchData);
|
||||||
|
|
||||||
|
console.log("Enviando actualización con params:", params);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enviamos cambios al servidor
|
// Enviamos cambios al servidor
|
||||||
const updated = await mutateAsync(params);
|
const updated = await mutateAsync(params);
|
||||||
@ -151,38 +177,59 @@ export const useUpdateProformaController = (
|
|||||||
|
|
||||||
options?.onUpdated?.(updated);
|
options?.onUpdated?.(updated);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const normalizedError =
|
const normalizedError = normalizeSubmitError(error);
|
||||||
error instanceof Error ? error : new Error(t("pages.update.error.unknown"));
|
|
||||||
|
|
||||||
form.reset(
|
// No revierto el form para que no se pierdan los cambios que
|
||||||
|
// ha hecho el usuario y no han sido guardados.
|
||||||
|
/*form.reset(
|
||||||
previousData
|
previousData
|
||||||
? mapProformaToProformaUpdateForm(previousData)
|
? mapProformaToProformaUpdateForm(previousData)
|
||||||
: buildProformaUpdateDefault(),
|
: buildProformaUpdateDefault(),
|
||||||
{ keepDirty: false }
|
{ keepDirty: false }
|
||||||
);
|
|
||||||
|
|
||||||
setSelectedCustomer(previousData ? mapProformaToSelectedCustomer(previousData) : null);
|
setSelectedCustomer(previousData ? mapProformaToSelectedCustomer(previousData) : null);
|
||||||
|
|
||||||
|
);*/
|
||||||
|
|
||||||
|
if (isValidationErrorCollection(normalizedError)) {
|
||||||
|
applyValidationErrorCollection(form, normalizedError);
|
||||||
|
|
||||||
|
console.log("Errores de validación aplicados al form:", form.formState.errors);
|
||||||
|
|
||||||
|
focusFirstProformaUpdateFormError(form);
|
||||||
|
|
||||||
|
if (options?.errorToasts !== false) {
|
||||||
|
showWarningToast(
|
||||||
|
t("pages.update.validation_error.title", "Revisa los campos"),
|
||||||
|
t(
|
||||||
|
"pages.update.validation_error.message",
|
||||||
|
"Hay errores de validación. Corrige los campos indicados."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
options?.onError?.(normalizedError, params);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.errorToasts !== false) {
|
if (options?.errorToasts !== false) {
|
||||||
showErrorToast(t("proformas.update.error.title"), normalizedError.message);
|
showErrorToast(t("proformas.update.error.title"), normalizedError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
options?.onError?.(normalizedError, params);
|
options?.onError?.(normalizedError, params);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<ProformaUpdateForm>) => {
|
(errors: FieldErrors<ProformaUpdateForm>) => {
|
||||||
console.log(errors);
|
console.log(errors);
|
||||||
focusFirstProformaUpdateError(errors);
|
focusFirstProformaUpdateFormError(form);
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
t("proformas.update.validation.title"),
|
t("forms.validation.title", "Revisa los campos"),
|
||||||
t("proformas.update.validation.message")
|
t("forms.validation.message", "Hay errores de validación en el formulario.")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Evento onSubmit ya preparado para el <form>
|
// Evento onSubmit ya preparado para el <form>
|
||||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = (event: React.SubmitEvent<HTMLFormElement>) => {
|
||||||
event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM
|
event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM
|
||||||
submitHandler(event);
|
submitHandler(event);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
export interface ProformaItemUpdateForm {
|
export interface ProformaItemUpdateForm {
|
||||||
id: string;
|
id: string;
|
||||||
position: number;
|
position: number;
|
||||||
|
isValued: boolean;
|
||||||
|
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
quantity: number | null;
|
quantity: number | null;
|
||||||
unitAmount: number | null;
|
unitAmount: number | null;
|
||||||
|
|
||||||
itemDiscountPercentage: number | null;
|
itemDiscountPercentage: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,10 +17,13 @@ import { z } from "zod/v4";
|
|||||||
export const ProformaItemUpdateFormSchema = z.object({
|
export const ProformaItemUpdateFormSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
position: z.number().int().nonnegative(),
|
position: z.number().int().nonnegative(),
|
||||||
|
isValued: z.boolean(),
|
||||||
|
|
||||||
description: z.string().trim(),
|
description: z.string(),
|
||||||
quantity: z.number().positive().nullable(),
|
|
||||||
|
quantity: z.number().nullable(),
|
||||||
unitAmount: z.number().nonnegative().nullable(),
|
unitAmount: z.number().nonnegative().nullable(),
|
||||||
|
|
||||||
itemDiscountPercentage: z.number().min(0).max(100).nullable(),
|
itemDiscountPercentage: z.number().min(0).max(100).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* ProformaItemUpdatePatch representa los cambios que se han hecho
|
||||||
|
* en el formulario de edición y que se aplicarán al item de una proforma.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - `undefined` => no enviar
|
||||||
|
* - `null` => borrar explícitamente
|
||||||
|
* - `""` no se usa aquí para campos anulables; el builder lo convierte a null
|
||||||
|
* - mantiene naming camelCase porque sigue siendo contrato frontend
|
||||||
|
*
|
||||||
|
* OJO => no hay campos opcionales por que la actualización en los
|
||||||
|
* items de una proforma es COMPLETA.
|
||||||
|
*/
|
||||||
|
|
||||||
export interface ProformaItemUpdatePatch {
|
export interface ProformaItemUpdatePatch {
|
||||||
id: string;
|
id: string;
|
||||||
position: number;
|
position: number;
|
||||||
description: string;
|
isValued: boolean;
|
||||||
|
|
||||||
|
description: string | null;
|
||||||
|
|
||||||
quantity: number | null;
|
quantity: number | null;
|
||||||
unitAmount: number | null;
|
unitAmount: number | null;
|
||||||
|
|
||||||
itemDiscountPercentage: number | null;
|
itemDiscountPercentage: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* ProformaUpdateForm representa el shape de datos del formulario de actualización de proformas.
|
* ProformaUpdateForm representa los datos del formulario de actualización de proformas.
|
||||||
* Es decir, los campos que se muestran en el formulario y que el usuario puede editar.
|
* Es decir, los campos que se muestran en el formulario y que el usuario puede editar.
|
||||||
*
|
*
|
||||||
* Es específico de la UI y no tiene por qué coincidir
|
* Es específico de la UI y no tiene por qué coincidir
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { CurrencyCodeSchema, LanguageCodeSchema } from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema";
|
import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema";
|
||||||
@ -17,23 +18,23 @@ import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const ProformaUpdateFormSchema = z.object({
|
export const ProformaUpdateFormSchema = z.object({
|
||||||
series: z.string().default(""),
|
series: z.string().or(z.literal("")),
|
||||||
|
|
||||||
invoiceDate: z.string().default(""),
|
invoiceDate: z.string(),
|
||||||
operationDate: z.string().default(""),
|
operationDate: z.string().or(z.literal("")),
|
||||||
|
|
||||||
customerId: z.string().default(""),
|
customerId: z.string(),
|
||||||
|
|
||||||
description: z.string().default(""),
|
description: z.string(),
|
||||||
reference: z.string().default(""),
|
reference: z.string().or(z.literal("")),
|
||||||
notes: z.string().default(""),
|
notes: z.string().or(z.literal("")),
|
||||||
|
|
||||||
languageCode: z.string().min(1, "Debe indicar un idioma").default("es"),
|
languageCode: LanguageCodeSchema,
|
||||||
currencyCode: z.string().min(1, "Debe indicar una moneda").default("EUR"),
|
currencyCode: CurrencyCodeSchema,
|
||||||
|
|
||||||
globalDiscountPercentage: z.number().default(0),
|
globalDiscountPercentage: z.number(),
|
||||||
|
|
||||||
paymentMethod: z.string().default(""),
|
paymentMethod: z.string().or(z.literal("")),
|
||||||
|
|
||||||
items: z.array(ProformaItemUpdateFormSchema),
|
items: z.array(ProformaItemUpdateFormSchema),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,17 +1,34 @@
|
|||||||
import type { ProformaUpdateForm } from "./proforma-update-form.entity";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProformaUpdatePatch representa los cambios que se van a aplicar a una proforma.
|
* ProformaUpdatePatch representa los cambios que se han hecho
|
||||||
* Se representa con las mismas propiedades que ProformaUpdateForm,
|
* en el formulario de edición y que se aplicarán a la proforma.
|
||||||
* pero todas ellas son opcionales.
|
|
||||||
*
|
|
||||||
* A la API solo hay que enviar los campos que han cambiado.
|
|
||||||
*
|
*
|
||||||
* Reglas:
|
* Reglas:
|
||||||
* - debe ser un Partial de ProformaUpdateForm
|
* - `undefined` => no enviar
|
||||||
* - no debe tener campos adicionales ni transformaciones
|
* - `null` => borrar explícitamente
|
||||||
* - debe ser un shape orientado a la API, no a la UI ni al dominio
|
* - `""` no se usa aquí para campos anulables; el builder lo convierte a null
|
||||||
* - sin shape DTO, solo tipos simples y directos
|
* - mantiene naming camelCase porque sigue siendo contrato frontend
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ProformaUpdatePatch = Partial<ProformaUpdateForm>;
|
import type { ProformaItemUpdatePatch } from "./proforma-item-update-patch.entity";
|
||||||
|
|
||||||
|
export type ProformaUpdatePatch = {
|
||||||
|
series?: string | null;
|
||||||
|
|
||||||
|
invoiceDate?: string;
|
||||||
|
operationDate?: string | null;
|
||||||
|
|
||||||
|
customerId?: string;
|
||||||
|
|
||||||
|
reference?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
|
||||||
|
globalDiscountPercentage?: number;
|
||||||
|
|
||||||
|
paymentMehodId?: string;
|
||||||
|
|
||||||
|
languageCode?: string;
|
||||||
|
currencyCode?: string;
|
||||||
|
|
||||||
|
items?: ProformaItemUpdatePatch[];
|
||||||
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
FormSectionCard,
|
FormSectionCard,
|
||||||
FormSectionGrid,
|
FormSectionGrid,
|
||||||
SelectField,
|
SelectField,
|
||||||
|
TextAreaField,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
|
|
||||||
@ -72,6 +73,17 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
placeholder={t("form_fields.proformas.description.placeholder")}
|
placeholder={t("form_fields.proformas.description.placeholder")}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextAreaField
|
||||||
|
className="md:col-span-12"
|
||||||
|
disabled={disabled}
|
||||||
|
label={t("form_fields.proformas.notes.label")}
|
||||||
|
maxLength={256}
|
||||||
|
name="notes"
|
||||||
|
placeholder={t("form_fields.proformas.notes.placeholder")}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
</FormSectionGrid>
|
</FormSectionGrid>
|
||||||
</FormSectionCard>
|
</FormSectionCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { AmountField, PercentageField, QuantityField, TextField } from "@repo/rdx-ui/components";
|
import {
|
||||||
|
AmountField,
|
||||||
|
CheckboxField,
|
||||||
|
PercentageField,
|
||||||
|
QuantityField,
|
||||||
|
TextField,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
import { Button, Card, CardContent } from "@repo/shadcn-ui/components";
|
import { Button, Card, CardContent } from "@repo/shadcn-ui/components";
|
||||||
import { CopyIcon, MoveDownIcon, MoveUpIcon, Trash2Icon } from "lucide-react";
|
import { CopyIcon, MoveDownIcon, MoveUpIcon, Trash2Icon } from "lucide-react";
|
||||||
|
|
||||||
@ -34,6 +40,12 @@ export const ProformaUpdateItemRowEditor = ({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="space-y-4 pt-6">
|
<CardContent className="space-y-4 pt-6">
|
||||||
<div className="grid grid-cols-12 gap-4">
|
<div className="grid grid-cols-12 gap-4">
|
||||||
|
<CheckboxField
|
||||||
|
description={t("customers.fields.is_valued.description")}
|
||||||
|
label={t("customers.fields.is_valued.label")}
|
||||||
|
name={`items.${index}.isValued`}
|
||||||
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="col-span-12"
|
className="col-span-12"
|
||||||
label={t("form_fields.items.description.label", "Descripción")}
|
label={t("form_fields.items.description.label", "Descripción")}
|
||||||
|
|||||||
@ -6,7 +6,10 @@ export const buildProformaItemUpdateDefault = (position: number): ProformaItemUp
|
|||||||
return {
|
return {
|
||||||
id: UniqueID.generateNewID().toString(),
|
id: UniqueID.generateNewID().toString(),
|
||||||
position,
|
position,
|
||||||
|
isValued: false,
|
||||||
|
|
||||||
description: "",
|
description: "",
|
||||||
|
|
||||||
quantity: null,
|
quantity: null,
|
||||||
unitAmount: null,
|
unitAmount: null,
|
||||||
itemDiscountPercentage: null,
|
itemDiscountPercentage: null,
|
||||||
|
|||||||
@ -3,6 +3,19 @@ import type { FieldNamesMarkedBoolean } from "react-hook-form";
|
|||||||
|
|
||||||
import type { ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
|
import type { ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construye el patch de actualización de proforma a partir
|
||||||
|
* de los campos dirty del formulario.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - Solo incluye campos modificados.
|
||||||
|
* - Los objetos anidados se procesan recursivamente.
|
||||||
|
* - `items` se envía completo si cualquier campo interno ha cambiado.
|
||||||
|
*
|
||||||
|
* Importante:
|
||||||
|
* - No normaliza valores.
|
||||||
|
*/
|
||||||
|
|
||||||
export const buildProformaUpdatePatch = (
|
export const buildProformaUpdatePatch = (
|
||||||
formData: ProformaUpdateForm,
|
formData: ProformaUpdateForm,
|
||||||
dirtyFields: FieldNamesMarkedBoolean<ProformaUpdateForm>
|
dirtyFields: FieldNamesMarkedBoolean<ProformaUpdateForm>
|
||||||
|
|||||||
@ -1,5 +1,23 @@
|
|||||||
|
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
|
||||||
|
import { ObjectHelper } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { UpdateProformaByIdParams } from "../../shared/api";
|
import type { UpdateProformaByIdParams } from "../../shared/api";
|
||||||
import type { ProformaUpdatePatch } from "../entities";
|
import type { ProformaItemUpdatePatch, ProformaUpdatePatch } from "../entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte el patch del formulario de actualización de proforma
|
||||||
|
* en los parámetros esperados por la API.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - Respeta campos enviados explícitamente como `null`.
|
||||||
|
* - Convierte camelCase frontend a snake_case API.
|
||||||
|
* - Convierte porcentajes, cantidades e importes a objetos `{ value, scale }`.
|
||||||
|
* - `items` se envía como reemplazo completo de la colección.
|
||||||
|
*
|
||||||
|
* @param id - Identificador de la proforma.
|
||||||
|
* @param patch - Patch construido desde los campos dirty del formulario.
|
||||||
|
* @returns Parámetros para la llamada de actualización.
|
||||||
|
*/
|
||||||
|
|
||||||
export const buildUpdateProformaByIdParams = (
|
export const buildUpdateProformaByIdParams = (
|
||||||
id: string,
|
id: string,
|
||||||
@ -9,38 +27,86 @@ export const buildUpdateProformaByIdParams = (
|
|||||||
throw new Error("proformaId is required");
|
throw new Error("proformaId is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: UpdateProformaByIdParams["data"] = {
|
const data: UpdateProformaByIdParams["data"] = {};
|
||||||
series: patch.series,
|
|
||||||
|
|
||||||
invoice_date: patch.invoiceDate,
|
if (ObjectHelper.hasOwn(patch, "series")) {
|
||||||
operation_date: patch.operationDate,
|
data.series = patch.series;
|
||||||
|
}
|
||||||
|
|
||||||
customer_id: patch.customerId,
|
if (ObjectHelper.hasOwn(patch, "invoiceDate")) {
|
||||||
|
data.invoice_date = patch.invoiceDate;
|
||||||
|
}
|
||||||
|
|
||||||
reference: patch.reference,
|
if (ObjectHelper.hasOwn(patch, "operationDate")) {
|
||||||
description: patch.description,
|
data.operation_date = patch.operationDate;
|
||||||
notes: patch.notes,
|
}
|
||||||
|
|
||||||
language_code: patch.languageCode,
|
if (ObjectHelper.hasOwn(patch, "customerId")) {
|
||||||
currency_code: patch.currencyCode,
|
data.customer_id = patch.customerId;
|
||||||
|
}
|
||||||
|
|
||||||
items: patch.items?.map((item) => ({
|
if (ObjectHelper.hasOwn(patch, "reference")) {
|
||||||
id: item.id,
|
data.reference = patch.reference;
|
||||||
position: String(item.position),
|
}
|
||||||
description: item.description,
|
|
||||||
quantity: item.quantity === null ? undefined : String(item.quantity),
|
if (ObjectHelper.hasOwn(patch, "description")) {
|
||||||
unit_amount: item.unitAmount === null ? undefined : String(item.unitAmount),
|
data.description = patch.description;
|
||||||
item_discount_percentage:
|
}
|
||||||
item.itemDiscountPercentage === null
|
|
||||||
? undefined
|
if (ObjectHelper.hasOwn(patch, "notes")) {
|
||||||
: { value: String(item.itemDiscountPercentage), scale: "2" },
|
data.notes = patch.notes;
|
||||||
})),
|
}
|
||||||
};
|
|
||||||
|
if (ObjectHelper.hasOwn(patch, "globalDiscountPercentage")) {
|
||||||
|
data.global_discount_percentage = PercentageDTOHelper.fromNumber(
|
||||||
|
patch.globalDiscountPercentage!,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ObjectHelper.hasOwn(patch, "languageCode")) {
|
||||||
|
data.language_code = patch.languageCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ObjectHelper.hasOwn(patch, "currencyCode")) {
|
||||||
|
data.currency_code = patch.currencyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ObjectHelper.hasOwn(patch, "items")) {
|
||||||
|
data.items = patch.items?.map(toProformaItemUpdateDTO);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
data: Object.fromEntries(
|
data,
|
||||||
Object.entries(data).filter(([, value]) => value !== undefined)
|
} satisfies UpdateProformaByIdParams;
|
||||||
) satisfies UpdateProformaByIdParams["data"],
|
};
|
||||||
|
|
||||||
|
const toProformaItemUpdateDTO = (
|
||||||
|
item: ProformaItemUpdatePatch
|
||||||
|
): NonNullable<UpdateProformaByIdParams["data"]["items"]>[number] => {
|
||||||
|
const quantity =
|
||||||
|
item.quantity === null ? null : QuantityDTOHelper.fromNumber(item.quantity, 4).value;
|
||||||
|
|
||||||
|
const unit_amount =
|
||||||
|
item.unitAmount === null ? null : MoneyDTOHelper.fromNumber(item.unitAmount, "EUR", 2).value;
|
||||||
|
|
||||||
|
const is_valued = item.isValued;
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: item.position,
|
||||||
|
is_valued,
|
||||||
|
|
||||||
|
description: item.description,
|
||||||
|
|
||||||
|
quantity,
|
||||||
|
unit_amount,
|
||||||
|
|
||||||
|
item_discount_percentage:
|
||||||
|
item.itemDiscountPercentage === null
|
||||||
|
? null
|
||||||
|
: PercentageDTOHelper.fromNumber(item.itemDiscountPercentage, 2),
|
||||||
|
|
||||||
|
taxes: "#;#;#",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
import type { FieldErrors } from "react-hook-form";
|
|
||||||
|
|
||||||
import type { ProformaUpdateForm } from "../entities";
|
|
||||||
|
|
||||||
export const focusFirstProformaUpdateError = (errors: FieldErrors<ProformaUpdateForm>) => {
|
|
||||||
const firstKey = Object.keys(errors)[0] as keyof ProformaUpdateForm | undefined;
|
|
||||||
|
|
||||||
if (!firstKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`)?.focus();
|
|
||||||
};
|
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
|
||||||
|
import type { ProformaUpdateForm } from "../entities";
|
||||||
|
|
||||||
|
export const focusFirstProformaUpdateFormError = (form: UseFormReturn<ProformaUpdateForm>) => {
|
||||||
|
const errors = form.formState.errors;
|
||||||
|
const firstKey = Object.keys(errors)[0] as keyof ProformaUpdateForm | undefined;
|
||||||
|
|
||||||
|
if (firstKey) {
|
||||||
|
form.setFocus(firstKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
export * from "./build-proforma-item-update-default";
|
export * from "./build-proforma-item-update-default";
|
||||||
export * from "./build-proforma-update-default";
|
export * from "./build-proforma-update-default";
|
||||||
|
export * from "./build-proforma-update-default";
|
||||||
export * from "./build-proforma-update-patch";
|
export * from "./build-proforma-update-patch";
|
||||||
export * from "./build-update-proforma-by-id-params";
|
export * from "./build-update-proforma-by-id-params";
|
||||||
export * from "./focus-first-proforma-update-error";
|
export * from "./focus-first-proforma-update-form-error";
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export function updateCustomerById(
|
|||||||
const { id, data } = params;
|
const { id, data } = params;
|
||||||
|
|
||||||
if (!id) throw new Error("customerId is required");
|
if (!id) throw new Error("customerId is required");
|
||||||
|
|
||||||
return dataSource.updateOne<UpdateCustomerByIdRequestDTO, UpdateCustomerByIdResponseDTO>(
|
return dataSource.updateOne<UpdateCustomerByIdRequestDTO, UpdateCustomerByIdResponseDTO>(
|
||||||
"customers",
|
"customers",
|
||||||
id,
|
id,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { applyValidationErrorCollection } from "@erp/core";
|
import { applyValidationErrorCollection } from "@erp/core";
|
||||||
|
import { formHasAnyDirty } from "@erp/core/client";
|
||||||
import { useHookForm } from "@erp/core/hooks";
|
import { useHookForm } from "@erp/core/hooks";
|
||||||
import { type ValidationErrorCollection, isValidationErrorCollection } from "@repo/rdx-ddd";
|
import { type ValidationErrorCollection, isValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
||||||
@ -13,12 +14,9 @@ import {
|
|||||||
useCustomerUpdateMutation,
|
useCustomerUpdateMutation,
|
||||||
} from "../../shared";
|
} from "../../shared";
|
||||||
import { mapCustomerToCustomerUpdateForm } from "../adapters";
|
import { mapCustomerToCustomerUpdateForm } from "../adapters";
|
||||||
|
import { type CustomerUpdateForm, CustomerUpdateFormSchema } from "../entities";
|
||||||
import {
|
import {
|
||||||
type CustomerUpdateForm,
|
buildCustomerUpdateDefault,
|
||||||
CustomerUpdateFormSchema,
|
|
||||||
defaultCustomerUpdateForm,
|
|
||||||
} from "../entities";
|
|
||||||
import {
|
|
||||||
buildCustomerUpdatePatch,
|
buildCustomerUpdatePatch,
|
||||||
buildUpdateCustomerByIdParams,
|
buildUpdateCustomerByIdParams,
|
||||||
focusFirstCustomerUpdateFormError,
|
focusFirstCustomerUpdateFormError,
|
||||||
@ -68,7 +66,7 @@ export const useCustomerUpdateController = (
|
|||||||
} = useCustomerUpdateMutation();
|
} = useCustomerUpdateMutation();
|
||||||
|
|
||||||
const initialValues = useMemo<CustomerUpdateForm>(() => {
|
const initialValues = useMemo<CustomerUpdateForm>(() => {
|
||||||
if (!customerData) return defaultCustomerUpdateForm;
|
if (!customerData) return buildCustomerUpdateDefault();
|
||||||
|
|
||||||
return mapCustomerToCustomerUpdateForm(customerData);
|
return mapCustomerToCustomerUpdateForm(customerData);
|
||||||
}, [customerData]);
|
}, [customerData]);
|
||||||
@ -95,7 +93,7 @@ export const useCustomerUpdateController = (
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
const initialData = customerData
|
const initialData = customerData
|
||||||
? mapCustomerToCustomerUpdateForm(customerData)
|
? mapCustomerToCustomerUpdateForm(customerData)
|
||||||
: defaultCustomerUpdateForm;
|
: buildCustomerUpdateDefault();
|
||||||
|
|
||||||
form.reset(initialData, { keepDirty: false });
|
form.reset(initialData, { keepDirty: false });
|
||||||
};
|
};
|
||||||
@ -107,6 +105,14 @@ export const useCustomerUpdateController = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!formHasAnyDirty(form.formState.dirtyFields)) {
|
||||||
|
showWarningToast(
|
||||||
|
t("customers.update.no_changes.title"),
|
||||||
|
t("customers.update.no_changes.message")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//const previousData = customerData;
|
//const previousData = customerData;
|
||||||
|
|
||||||
const patchData = buildCustomerUpdatePatch(formData, form.formState.dirtyFields);
|
const patchData = buildCustomerUpdatePatch(formData, form.formState.dirtyFields);
|
||||||
@ -149,7 +155,7 @@ export const useCustomerUpdateController = (
|
|||||||
// No revierto el form para que no se pierdan los cambios que
|
// No revierto el form para que no se pierdan los cambios que
|
||||||
// ha hecho el usuario y no han sido guardados.
|
// ha hecho el usuario y no han sido guardados.
|
||||||
/*form.reset(
|
/*form.reset(
|
||||||
previousData ? mapCustomerToCustomerUpdateForm(previousData) : defaultCustomerUpdateForm,
|
previousData ? mapCustomerToCustomerUpdateForm(previousData) : buildCustomerUpdateDefault(),
|
||||||
{ keepDirty: false }
|
{ keepDirty: false }
|
||||||
);*/
|
);*/
|
||||||
|
|
||||||
@ -186,6 +192,7 @@ export const useCustomerUpdateController = (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<CustomerUpdateForm>) => {
|
(errors: FieldErrors<CustomerUpdateForm>) => {
|
||||||
|
console.log(errors);
|
||||||
focusFirstCustomerUpdateFormError(form);
|
focusFirstCustomerUpdateFormError(form);
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
@ -196,7 +203,7 @@ export const useCustomerUpdateController = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Evento onSubmit ya preparado para el <form>
|
// Evento onSubmit ya preparado para el <form>
|
||||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = (event: React.SubmitEvent<HTMLFormElement>) => {
|
||||||
event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM
|
event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM
|
||||||
submitHandler(event);
|
submitHandler(event);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
import type { CustomerUpdateForm } from "./customer-update-form.entity";
|
|
||||||
|
|
||||||
export const defaultCustomerUpdateForm: CustomerUpdateForm = {
|
|
||||||
reference: "",
|
|
||||||
isCompany: true,
|
|
||||||
name: "",
|
|
||||||
tradeName: "",
|
|
||||||
tin: "",
|
|
||||||
|
|
||||||
defaultTaxes: [],
|
|
||||||
|
|
||||||
street: "",
|
|
||||||
street2: "",
|
|
||||||
city: "",
|
|
||||||
province: "",
|
|
||||||
postalCode: "",
|
|
||||||
country: "es",
|
|
||||||
|
|
||||||
primaryEmail: "",
|
|
||||||
secondaryEmail: "",
|
|
||||||
primaryPhone: "",
|
|
||||||
secondaryPhone: "",
|
|
||||||
primaryMobile: "",
|
|
||||||
secondaryMobile: "",
|
|
||||||
|
|
||||||
fax: "",
|
|
||||||
website: "",
|
|
||||||
|
|
||||||
legalRecord: "",
|
|
||||||
|
|
||||||
languageCode: "es",
|
|
||||||
currencyCode: "EUR",
|
|
||||||
};
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* CustomerUpdateForm representa el shape de datos del formulario de actualización de cliente.
|
* CustomerUpdateForm representa los datos del formulario de actualización de cliente.
|
||||||
* Es decir, los campos que se muestran en el formulario y que el usuario puede editar.
|
* Es decir, los campos que se muestran en el formulario y que el usuario puede editar.
|
||||||
*
|
*
|
||||||
* Es específico de la UI y no tiene por qué coincidir
|
* Es específico de la UI y no tiene por qué coincidir
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* CustomerUpdatePatch representa los cambios efectivos que se enviarán en update.
|
* CustomerUpdatePatch representa los cambios que se han hecho
|
||||||
|
* en el formulario de edición.
|
||||||
*
|
*
|
||||||
* Reglas:
|
* Reglas:
|
||||||
* - `undefined` => no enviar
|
* - `undefined` => no enviar
|
||||||
@ -8,7 +9,7 @@
|
|||||||
* - mantiene naming camelCase porque sigue siendo contrato frontend
|
* - mantiene naming camelCase porque sigue siendo contrato frontend
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface CustomerUpdatePatch {
|
export type CustomerUpdatePatch = {
|
||||||
reference?: string | null;
|
reference?: string | null;
|
||||||
isCompany?: boolean;
|
isCompany?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -38,4 +39,4 @@ export interface CustomerUpdatePatch {
|
|||||||
|
|
||||||
languageCode?: string;
|
languageCode?: string;
|
||||||
currencyCode?: string;
|
currencyCode?: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export * from "./customer-update-form.entity";
|
export * from "./customer-update-form.entity";
|
||||||
export * from "./customer-update-form.schema";
|
export * from "./customer-update-form.schema";
|
||||||
export * from "./customer-update-form-default.entity";
|
|
||||||
export * from "./customer-update-patch.entity";
|
export * from "./customer-update-patch.entity";
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
import type { CustomerUpdateForm } from "../entities";
|
||||||
|
|
||||||
|
export const buildCustomerUpdateDefault = (): CustomerUpdateForm => {
|
||||||
|
return {
|
||||||
|
reference: "",
|
||||||
|
isCompany: true,
|
||||||
|
name: "",
|
||||||
|
tradeName: "",
|
||||||
|
tin: "",
|
||||||
|
|
||||||
|
defaultTaxes: [],
|
||||||
|
|
||||||
|
street: "",
|
||||||
|
street2: "",
|
||||||
|
city: "",
|
||||||
|
province: "",
|
||||||
|
postalCode: "",
|
||||||
|
country: "es",
|
||||||
|
|
||||||
|
primaryEmail: "",
|
||||||
|
secondaryEmail: "",
|
||||||
|
primaryPhone: "",
|
||||||
|
secondaryPhone: "",
|
||||||
|
primaryMobile: "",
|
||||||
|
secondaryMobile: "",
|
||||||
|
|
||||||
|
fax: "",
|
||||||
|
website: "",
|
||||||
|
|
||||||
|
legalRecord: "",
|
||||||
|
|
||||||
|
languageCode: "es",
|
||||||
|
currencyCode: "EUR",
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./build-customer.update-patch";
|
export * from "./build-customer.update-patch";
|
||||||
|
export * from "./build-customer-update-default";
|
||||||
export * from "./build-update-customer-by-id-params";
|
export * from "./build-update-customer-by-id-params";
|
||||||
export * from "./focus-first-customer-update-form-error";
|
export * from "./focus-first-customer-update-form-error";
|
||||||
|
|||||||
@ -7,13 +7,23 @@ import { MoneyValue, Percentage, Quantity } from "../value-objects";
|
|||||||
/**
|
/**
|
||||||
* Aplica una transformación a un valor potencialmente nulo/indefinido.
|
* Aplica una transformación a un valor potencialmente nulo/indefinido.
|
||||||
*
|
*
|
||||||
* Si `input` es `null` o `undefined`, devuelve `null`.
|
* Casos:
|
||||||
* En caso contrario, aplica `map`.
|
* - `null` → `null`
|
||||||
|
* - `undefined` → `null`
|
||||||
|
* - valor no nulo → `map(value)`
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* mapNullable(null, String); // null
|
||||||
|
* mapNullable(undefined, String); // null
|
||||||
|
* mapNullable(12, String); // "12"
|
||||||
|
* mapNullable("abc", (value) => value.toUpperCase()); // "ABC"
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @typeParam T - Tipo de entrada no nulo.
|
* @typeParam T - Tipo de entrada no nulo.
|
||||||
* @typeParam R - Tipo de salida.
|
* @typeParam R - Tipo de salida.
|
||||||
* @param input - Valor potencialmente nulo o indefinido.
|
* @param input - Valor potencialmente nulo o indefinido.
|
||||||
* @param map - Función de transformación.
|
* @param map - Función de transformación aplicada solo si `input` tiene valor.
|
||||||
* @returns Resultado de `map` o `null` si el input es nulo.
|
* @returns Resultado de `map` o `null` si el input es nulo.
|
||||||
*/
|
*/
|
||||||
function mapNullable<T, R>(input: T | null | undefined, map: (value: T) => R): R | null {
|
function mapNullable<T, R>(input: T | null | undefined, map: (value: T) => R): R | null {
|
||||||
@ -23,10 +33,17 @@ function mapNullable<T, R>(input: T | null | undefined, map: (value: T) => R): R
|
|||||||
/**
|
/**
|
||||||
* Serializa un `Maybe<T>` a un valor de salida, aplicando una política explícita para `None`.
|
* Serializa un `Maybe<T>` a un valor de salida, aplicando una política explícita para `None`.
|
||||||
*
|
*
|
||||||
* - Si `Maybe` es `None`, devuelve `onNone`.
|
* Casos:
|
||||||
* - Si es `Some`, aplica `map`.
|
* - `Maybe.none()` → `onNone`
|
||||||
|
* - `Maybe.some(value)` → `map(value)`
|
||||||
|
* - valor falsy inesperado → `onNone`
|
||||||
*
|
*
|
||||||
* Esta función centraliza la lógica común de serialización de `Maybe`.
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* serializeMaybe(Maybe.none<string>(), null, (value) => value); // null
|
||||||
|
* serializeMaybe(Maybe.none<string>(), "", (value) => value); // ""
|
||||||
|
* serializeMaybe(Maybe.some("abc"), null, (value) => value.toUpperCase()); // "ABC"
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @typeParam T - Tipo interno del Maybe.
|
* @typeParam T - Tipo interno del Maybe.
|
||||||
* @typeParam R - Tipo de salida.
|
* @typeParam R - Tipo de salida.
|
||||||
@ -40,18 +57,28 @@ function serializeMaybe<T, R>(maybe: Maybe<T>, onNone: R, map: (value: T) => R):
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convierte un valor (normal o `Maybe`) en un valor nullable (`R | null`).
|
* Convierte un valor normal, nullable o `Maybe` en un valor nullable (`R | null`).
|
||||||
*
|
*
|
||||||
* Soporta:
|
* Casos:
|
||||||
* - `null | undefined` → `null`
|
* - `null` → `null`
|
||||||
* - `T` → `map(T)`
|
* - `undefined` → `null`
|
||||||
|
* - valor directo `T` → `map(value)`
|
||||||
* - `Maybe.none()` → `null`
|
* - `Maybe.none()` → `null`
|
||||||
* - `Maybe.some(T)` → `map(T)`
|
* - `Maybe.some(value)` → `map(value)`
|
||||||
*
|
*
|
||||||
* @typeParam T - Tipo de entrada.
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* toNullable(null, String); // null
|
||||||
|
* toNullable(undefined, String); // null
|
||||||
|
* toNullable(12, String); // "12"
|
||||||
|
* toNullable(Maybe.none<number>(), String); // null
|
||||||
|
* toNullable(Maybe.some(12), String); // "12"
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @typeParam V - Tipo de entrada.
|
||||||
* @typeParam R - Tipo de salida.
|
* @typeParam R - Tipo de salida.
|
||||||
* @param input - Valor o `Maybe` a transformar.
|
* @param input - Valor directo, nullable o `Maybe` a transformar.
|
||||||
* @param map - Función de transformación.
|
* @param map - Función de transformación aplicada solo cuando hay valor.
|
||||||
* @returns Valor transformado o `null`.
|
* @returns Valor transformado o `null`.
|
||||||
*/
|
*/
|
||||||
export function toNullable<V, R>(input: null | undefined, map: (t: V) => R): null;
|
export function toNullable<V, R>(input: null | undefined, map: (t: V) => R): null;
|
||||||
@ -69,18 +96,29 @@ export function toNullable<V, R>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convierte un valor nullable en un `Maybe<T>` aplicando validación.
|
* Convierte un valor nullable o vacío en un `Maybe<T>` aplicando validación.
|
||||||
*
|
*
|
||||||
* - Si el valor es `null`, `undefined` o vacío → `Maybe.none()`
|
* Casos:
|
||||||
* - Si tiene valor → aplica `validate`
|
* - `null` → `Result.ok(Maybe.none())`
|
||||||
* - Success → `Maybe.some`
|
* - `undefined` → `Result.ok(Maybe.none())`
|
||||||
* - Failure → propaga error
|
* - `""` → `Result.ok(Maybe.none())`
|
||||||
|
* - whitespace, si `isNullishOrEmpty` lo considera vacío → `Result.ok(Maybe.none())`
|
||||||
|
* - valor con contenido + validación correcta → `Result.ok(Maybe.some(value))`
|
||||||
|
* - valor con contenido + validación fallida → `Result.fail(error)`
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* maybeFromNullableResult(null, TextValue.create); // Ok(None)
|
||||||
|
* maybeFromNullableResult("", TextValue.create); // Ok(None)
|
||||||
|
* maybeFromNullableResult("ACME", TextValue.create); // Ok(Some(TextValue))
|
||||||
|
* maybeFromNullableResult("x", TextValue.create); // Fail(error), si el VO lo rechaza
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @typeParam T - Tipo validado.
|
* @typeParam T - Tipo validado.
|
||||||
* @typeParam S - Tipo de entrada.
|
* @typeParam S - Tipo de entrada.
|
||||||
* @param input - Valor a validar.
|
* @param input - Valor a validar.
|
||||||
* @param validate - Función de validación.
|
* @param validate - Función de validación/construcción del valor cuando `input` tiene contenido.
|
||||||
* @returns Resultado con `Maybe<T>`.
|
* @returns Resultado con `Maybe<T>` o error de validación.
|
||||||
*/
|
*/
|
||||||
export function maybeFromNullableResult<T, S>(
|
export function maybeFromNullableResult<T, S>(
|
||||||
input: S,
|
input: S,
|
||||||
@ -97,17 +135,26 @@ export function maybeFromNullableResult<T, S>(
|
|||||||
/**
|
/**
|
||||||
* Serializa un `Maybe<T>` a un objeto aplicando una política de objeto vacío.
|
* Serializa un `Maybe<T>` a un objeto aplicando una política de objeto vacío.
|
||||||
*
|
*
|
||||||
* - `None` → `emptyObject`
|
* Casos:
|
||||||
* - `Some` → `map(value)` o `defaultSerializer(value)`
|
* - `Maybe.none()` → `emptyObject`
|
||||||
|
* - `Maybe.some(value)` sin `map` → `defaultSerializer(value)`
|
||||||
|
* - `Maybe.some(value)` con `map` → `map(value)`
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* maybeToEmptyObjectString(Maybe.none(), emptyMoney, serializeMoney); // emptyMoney
|
||||||
|
* maybeToEmptyObjectString(Maybe.some(money), emptyMoney, serializeMoney); // serializeMoney(money)
|
||||||
|
* maybeToEmptyObjectString(Maybe.some(money), emptyMoney, serializeMoney, customMap); // customMap(money)
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*
|
*
|
||||||
* @typeParam T - Tipo interno del Maybe.
|
* @typeParam T - Tipo interno del Maybe.
|
||||||
* @typeParam R - Tipo de salida (objeto de transporte).
|
* @typeParam R - Tipo de salida, normalmente objeto de transporte.
|
||||||
* @param maybe - Instancia `Maybe`.
|
* @param maybe - Instancia `Maybe`.
|
||||||
* @param emptyObject - Objeto vacío a devolver si es `None`.
|
* @param emptyObject - Objeto a devolver si es `None`.
|
||||||
* @param defaultSerializer - Serializador por defecto.
|
* @param defaultSerializer - Serializador por defecto para `Some`.
|
||||||
* @param map - Serializador opcional personalizado.
|
* @param map - Serializador opcional personalizado; prevalece sobre `defaultSerializer`.
|
||||||
* @returns Objeto serializado.
|
* @returns Objeto serializado.
|
||||||
*/
|
*/
|
||||||
function maybeToEmptyObjectString<T, R>(
|
function maybeToEmptyObjectString<T, R>(
|
||||||
@ -124,11 +171,28 @@ function maybeToEmptyObjectString<T, R>(
|
|||||||
/**
|
/**
|
||||||
* Serializa un `Maybe<MoneyValue>` a un objeto de transporte.
|
* Serializa un `Maybe<MoneyValue>` a un objeto de transporte.
|
||||||
*
|
*
|
||||||
* - `None` → objeto vacío de dinero
|
* Casos:
|
||||||
* - `Some` → `map` o `toObjectString()`
|
* - `Maybe.none()` → `MoneyValue.EMPTY_MONEY_OBJECT`
|
||||||
|
* - `Maybe.some(value)` sin `map` → `value.toObjectString()`
|
||||||
|
* - `Maybe.some(value)` con `map` → `map(value)`
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* maybeToEmptyMoneyObjectString(Maybe.none());
|
||||||
|
* // { value: "", scale: "", currency_code: "" }
|
||||||
|
*
|
||||||
|
* maybeToEmptyMoneyObjectString(Maybe.some(money));
|
||||||
|
* // { value: "1000", scale: "2", currency_code: "EUR" }
|
||||||
|
*
|
||||||
|
* maybeToEmptyMoneyObjectString(Maybe.some(money), (value) => ({
|
||||||
|
* value: value.toDecimalString(),
|
||||||
|
* scale: "2",
|
||||||
|
* currency_code: "EUR",
|
||||||
|
* }));
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @param maybe - Maybe de `MoneyValue`.
|
* @param maybe - Maybe de `MoneyValue`.
|
||||||
* @param map - Transformación opcional.
|
* @param map - Transformación opcional; si existe, sustituye a `toObjectString()`.
|
||||||
* @returns Objeto de dinero serializado.
|
* @returns Objeto de dinero serializado.
|
||||||
*/
|
*/
|
||||||
export function maybeToEmptyMoneyObjectString(
|
export function maybeToEmptyMoneyObjectString(
|
||||||
@ -146,11 +210,27 @@ export function maybeToEmptyMoneyObjectString(
|
|||||||
/**
|
/**
|
||||||
* Serializa un `Maybe<Percentage>` a un objeto de transporte.
|
* Serializa un `Maybe<Percentage>` a un objeto de transporte.
|
||||||
*
|
*
|
||||||
* - `None` → objeto vacío
|
* Casos:
|
||||||
* - `Some` → `map` o `toObjectString()`
|
* - `Maybe.none()` → `Percentage.EMPTY_PERCENTAGE_OBJECT`
|
||||||
|
* - `Maybe.some(value)` sin `map` → `value.toObjectString()`
|
||||||
|
* - `Maybe.some(value)` con `map` → `map(value)`
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* maybeToEmptyPercentageObjectString(Maybe.none());
|
||||||
|
* // { value: "", scale: "" }
|
||||||
|
*
|
||||||
|
* maybeToEmptyPercentageObjectString(Maybe.some(percentage));
|
||||||
|
* // { value: "2100", scale: "2" }
|
||||||
|
*
|
||||||
|
* maybeToEmptyPercentageObjectString(Maybe.some(percentage), (value) => ({
|
||||||
|
* value: value.toDecimalString(),
|
||||||
|
* scale: "2",
|
||||||
|
* }));
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @param maybe - Maybe de `Percentage`.
|
* @param maybe - Maybe de `Percentage`.
|
||||||
* @param map - Transformación opcional.
|
* @param map - Transformación opcional; si existe, sustituye a `toObjectString()`.
|
||||||
* @returns Objeto de porcentaje serializado.
|
* @returns Objeto de porcentaje serializado.
|
||||||
*/
|
*/
|
||||||
export function maybeToEmptyPercentageObjectString(
|
export function maybeToEmptyPercentageObjectString(
|
||||||
@ -168,11 +248,27 @@ export function maybeToEmptyPercentageObjectString(
|
|||||||
/**
|
/**
|
||||||
* Serializa un `Maybe<Quantity>` a un objeto de transporte.
|
* Serializa un `Maybe<Quantity>` a un objeto de transporte.
|
||||||
*
|
*
|
||||||
* - `None` → objeto vacío
|
* Casos:
|
||||||
* - `Some` → `map` o `toObjectString()`
|
* - `Maybe.none()` → `Quantity.EMPTY_QUANTITY_OBJECT`
|
||||||
|
* - `Maybe.some(value)` sin `map` → `value.toObjectString()`
|
||||||
|
* - `Maybe.some(value)` con `map` → `map(value)`
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* maybeToEmptyQuantityObjectString(Maybe.none());
|
||||||
|
* // { value: "", scale: "" }
|
||||||
|
*
|
||||||
|
* maybeToEmptyQuantityObjectString(Maybe.some(quantity));
|
||||||
|
* // { value: "1500", scale: "3" }
|
||||||
|
*
|
||||||
|
* maybeToEmptyQuantityObjectString(Maybe.some(quantity), (value) => ({
|
||||||
|
* value: value.toDecimalString(),
|
||||||
|
* scale: "3",
|
||||||
|
* }));
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @param maybe - Maybe de `Quantity`.
|
* @param maybe - Maybe de `Quantity`.
|
||||||
* @param map - Transformación opcional.
|
* @param map - Transformación opcional; si existe, sustituye a `toObjectString()`.
|
||||||
* @returns Objeto de cantidad serializado.
|
* @returns Objeto de cantidad serializado.
|
||||||
*/
|
*/
|
||||||
export function maybeToEmptyQuantityObjectString(
|
export function maybeToEmptyQuantityObjectString(
|
||||||
@ -188,14 +284,26 @@ export function maybeToEmptyQuantityObjectString(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convierte un string nullable en `Maybe<string>`.
|
* Convierte un string nullable o vacío en `Maybe<string>`.
|
||||||
*
|
*
|
||||||
* - `null | undefined` → `None`
|
* Casos:
|
||||||
* - `""` o whitespace → `None`
|
* - `null` → `Maybe.none()`
|
||||||
* - valor válido → `Some(trimmed)`
|
* - `undefined` → `Maybe.none()`
|
||||||
|
* - `""` → `Maybe.none()`
|
||||||
|
* - `" "` → `Maybe.none()`
|
||||||
|
* - `" abc "` → `Maybe.some("abc")`
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* maybeFromNullableOrEmptyString(null); // None
|
||||||
|
* maybeFromNullableOrEmptyString(undefined); // None
|
||||||
|
* maybeFromNullableOrEmptyString(""); // None
|
||||||
|
* maybeFromNullableOrEmptyString(" "); // None
|
||||||
|
* maybeFromNullableOrEmptyString(" ACME "); // Some("ACME")
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @param input - String de entrada.
|
* @param input - String de entrada.
|
||||||
* @returns Maybe del string.
|
* @returns Maybe del string normalizado con `trim()`.
|
||||||
*/
|
*/
|
||||||
export function maybeFromNullableOrEmptyString(input?: string | null): Maybe<string> {
|
export function maybeFromNullableOrEmptyString(input?: string | null): Maybe<string> {
|
||||||
if (input == null) return Maybe.none<string>();
|
if (input == null) return Maybe.none<string>();
|
||||||
@ -207,14 +315,22 @@ export function maybeFromNullableOrEmptyString(input?: string | null): Maybe<str
|
|||||||
/**
|
/**
|
||||||
* Convierte un `Maybe<T>` en `R | null`.
|
* Convierte un `Maybe<T>` en `R | null`.
|
||||||
*
|
*
|
||||||
* - `None` → `null`
|
* Casos:
|
||||||
* - `Some` → `map(value)`
|
* - `Maybe.none()` → `null`
|
||||||
|
* - `Maybe.some(value)` → `map(value)`
|
||||||
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* maybeToNullable(Maybe.none<number>(), String); // null
|
||||||
|
* maybeToNullable(Maybe.some(12), String); // "12"
|
||||||
|
* maybeToNullable(Maybe.some("abc"), (value) => value.toUpperCase()); // "ABC"
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @typeParam T - Tipo interno.
|
* @typeParam T - Tipo interno.
|
||||||
* @typeParam R - Tipo de salida.
|
* @typeParam R - Tipo de salida.
|
||||||
* @param maybe - Instancia Maybe.
|
* @param maybe - Instancia Maybe.
|
||||||
* @param map - Transformación.
|
* @param map - Transformación aplicada solo en caso `Some`.
|
||||||
* @returns Valor serializado o null.
|
* @returns Valor serializado o `null`.
|
||||||
*/
|
*/
|
||||||
export function maybeToNullable<T, R>(maybe: Maybe<T>, map: (t: T) => R): R | null {
|
export function maybeToNullable<T, R>(maybe: Maybe<T>, map: (t: T) => R): R | null {
|
||||||
return serializeMaybe(maybe, null, map);
|
return serializeMaybe(maybe, null, map);
|
||||||
@ -223,12 +339,22 @@ export function maybeToNullable<T, R>(maybe: Maybe<T>, map: (t: T) => R): R | nu
|
|||||||
/**
|
/**
|
||||||
* Convierte un `Maybe<T>` en `string | null`.
|
* Convierte un `Maybe<T>` en `string | null`.
|
||||||
*
|
*
|
||||||
* - `None` → `null`
|
* Casos:
|
||||||
* - `Some` → `map(value)` o `String(value)`
|
* - `Maybe.none()` → `null`
|
||||||
|
* - `Maybe.some(value)` sin `map` → `String(value)`
|
||||||
|
* - `Maybe.some(value)` con `map` → `String(map(value))`
|
||||||
*
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* maybeToNullableString(Maybe.none<number>()); // null
|
||||||
|
* maybeToNullableString(Maybe.some(12)); // "12"
|
||||||
|
* maybeToNullableString(Maybe.some(user), (value) => value.name); // "John"
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @typeParam T - Tipo interno.
|
||||||
* @param maybe - Instancia Maybe.
|
* @param maybe - Instancia Maybe.
|
||||||
* @param map - Transformación opcional.
|
* @param map - Transformación opcional.
|
||||||
* @returns String o null.
|
* @returns String serializado o `null`.
|
||||||
*/
|
*/
|
||||||
export function maybeToNullableString<T>(maybe: Maybe<T>, map?: (t: T) => string): string | null {
|
export function maybeToNullableString<T>(maybe: Maybe<T>, map?: (t: T) => string): string | null {
|
||||||
return serializeMaybe(maybe, null, (value) => (map ? String(map(value)) : String(value)));
|
return serializeMaybe(maybe, null, (value) => (map ? String(map(value)) : String(value)));
|
||||||
@ -237,12 +363,22 @@ export function maybeToNullableString<T>(maybe: Maybe<T>, map?: (t: T) => string
|
|||||||
/**
|
/**
|
||||||
* Convierte un `Maybe<T>` en `string`.
|
* Convierte un `Maybe<T>` en `string`.
|
||||||
*
|
*
|
||||||
* - `None` → `""`
|
* Casos:
|
||||||
* - `Some` → `map(value)` o `String(value)`
|
* - `Maybe.none()` → `""`
|
||||||
|
* - `Maybe.some(value)` sin `map` → `String(value)`
|
||||||
|
* - `Maybe.some(value)` con `map` → `map(value)`
|
||||||
*
|
*
|
||||||
|
* Ejemplos:
|
||||||
|
* ```ts
|
||||||
|
* maybeToEmptyString(Maybe.none<number>()); // ""
|
||||||
|
* maybeToEmptyString(Maybe.some(12)); // "12"
|
||||||
|
* maybeToEmptyString(Maybe.some(user), (value) => value.name); // "John"
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @typeParam T - Tipo interno.
|
||||||
* @param maybe - Instancia Maybe.
|
* @param maybe - Instancia Maybe.
|
||||||
* @param map - Transformación opcional.
|
* @param map - Transformación opcional.
|
||||||
* @returns String (nunca null).
|
* @returns String serializado; nunca `null`.
|
||||||
*/
|
*/
|
||||||
export function maybeToEmptyString<T>(maybe: Maybe<T>, map?: (t: T) => string): string {
|
export function maybeToEmptyString<T>(maybe: Maybe<T>, map?: (t: T) => string): string {
|
||||||
return serializeMaybe(maybe, "", (value) => (map ? map(value) : String(value)));
|
return serializeMaybe(maybe, "", (value) => (map ? map(value) : String(value)));
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { translateZodValidationError } from "../helpers";
|
import { translateZodValidationError } from "../helpers";
|
||||||
|
|
||||||
import { ValueObject } from "./value-object";
|
import { ValueObject } from "./value-object";
|
||||||
|
|
||||||
interface TINNumberProps {
|
interface TINNumberProps {
|
||||||
@ -20,7 +22,8 @@ export class TINNumber extends ValueObject<TINNumberProps> {
|
|||||||
})
|
})
|
||||||
.max(TINNumber.MAX_LENGTH, {
|
.max(TINNumber.MAX_LENGTH, {
|
||||||
message: `TIN must be at most ${TINNumber.MAX_LENGTH} characters long`,
|
message: `TIN must be at most ${TINNumber.MAX_LENGTH} characters long`,
|
||||||
});
|
})
|
||||||
|
.regex(/^(?=.*[A-Za-z0-9])[A-Za-z0-9.\-/ ]+$/, "TIN has an invalid format.");
|
||||||
|
|
||||||
return schema.safeParse(value);
|
return schema.safeParse(value);
|
||||||
}
|
}
|
||||||
|
|||||||
98
packages/rdx-ui/src/components/form/checkbox-field.tsx
Normal file
98
packages/rdx-ui/src/components/form/checkbox-field.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Field,
|
||||||
|
FieldContent,
|
||||||
|
FieldDescription,
|
||||||
|
FieldError,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Controller, type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { FormFieldLabel } from "./form-field-label.tsx";
|
||||||
|
|
||||||
|
type CheckboxFieldProps<TFormValues extends FieldValues> = {
|
||||||
|
name: FieldPath<TFormValues>;
|
||||||
|
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
|
||||||
|
orientation?: "vertical" | "horizontal" | "responsive";
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
inputClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CheckboxField = <TFormValues extends FieldValues>({
|
||||||
|
name,
|
||||||
|
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
|
||||||
|
disabled = false,
|
||||||
|
required = false,
|
||||||
|
readOnly = false,
|
||||||
|
|
||||||
|
orientation = "horizontal",
|
||||||
|
|
||||||
|
className,
|
||||||
|
inputClassName,
|
||||||
|
}: CheckboxFieldProps<TFormValues>) => {
|
||||||
|
const { control, formState } = useFormContext<TFormValues>();
|
||||||
|
|
||||||
|
const inputId = React.useId();
|
||||||
|
const descriptionId = description ? `${inputId}-description` : undefined;
|
||||||
|
|
||||||
|
const isDisabled = disabled || readOnly || formState.isSubmitting;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field, fieldState }) => {
|
||||||
|
const hasError = Boolean(fieldState.error);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
className={cn("gap-2", className)}
|
||||||
|
data-invalid={hasError}
|
||||||
|
orientation={orientation}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
aria-describedby={descriptionId}
|
||||||
|
aria-invalid={hasError || undefined}
|
||||||
|
aria-required={required || undefined}
|
||||||
|
checked={field.value === true}
|
||||||
|
className={cn(inputClassName)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
id={inputId}
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
field.onChange(checked === true);
|
||||||
|
}}
|
||||||
|
ref={field.ref}
|
||||||
|
required={required}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FieldContent className="gap-1">
|
||||||
|
<FormFieldLabel className="font-normal" htmlFor={inputId} required={required}>
|
||||||
|
{label}
|
||||||
|
</FormFieldLabel>
|
||||||
|
|
||||||
|
{description ? (
|
||||||
|
<FieldDescription id={descriptionId}>{description}</FieldDescription>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./checkbox-field.tsx";
|
||||||
export * from "./date-picker-field.tsx";
|
export * from "./date-picker-field.tsx";
|
||||||
export * from "./date-picker-input-field/index.ts";
|
export * from "./date-picker-input-field/index.ts";
|
||||||
export * from "./decimal-field/index.ts";
|
export * from "./decimal-field/index.ts";
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export * from "./collection";
|
export * from "./collection";
|
||||||
export * from "./id-utils";
|
export * from "./id-utils";
|
||||||
export * from "./maybe";
|
export * from "./maybe";
|
||||||
|
export * from "./object-helper";
|
||||||
export * from "./patch-field";
|
export * from "./patch-field";
|
||||||
export * from "./result";
|
export * from "./result";
|
||||||
export * from "./result-collection";
|
export * from "./result-collection";
|
||||||
|
|||||||
30
packages/rdx-utils/src/helpers/object-helper.ts
Normal file
30
packages/rdx-utils/src/helpers/object-helper.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Comprueba si un objeto posee una propiedad propia (no heredada) con la clave especificada.
|
||||||
|
*
|
||||||
|
* Esta función actúa como un wrapper tipado sobre `Object.hasOwn`, garantizando
|
||||||
|
* seguridad de tipos al restringir la clave (`key`) a las propiedades definidas
|
||||||
|
* en el tipo del objeto (`T`).
|
||||||
|
*
|
||||||
|
* @typeParam T - Tipo del objeto sobre el cual se realizará la comprobación.
|
||||||
|
*
|
||||||
|
* @param obj - Objeto en el que se desea verificar la existencia de la propiedad.
|
||||||
|
* @param key - Clave de la propiedad a comprobar; debe ser una clave válida de `T`.
|
||||||
|
*
|
||||||
|
* @returns `true` si el objeto tiene la propiedad como propia (no en su prototipo),
|
||||||
|
* `false` en caso contrario.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const user = { name: "Alice", age: 30 };
|
||||||
|
*
|
||||||
|
* hasOwn(user, "name"); // true
|
||||||
|
* hasOwn(user, "toString"); // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const hasOwn = <T extends object>(obj: T, key: keyof T): boolean => {
|
||||||
|
return Object.hasOwn(obj, key);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ObjectHelper = {
|
||||||
|
hasOwn,
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import Joi, { ValidationError } from "joi";
|
import Joi, { type ValidationError } from "joi";
|
||||||
|
|
||||||
import { Result } from "./result";
|
import { Result } from "./result";
|
||||||
|
|
||||||
export type TRuleValidatorResult<T> = Result<T, ValidationError>;
|
export type TRuleValidatorResult<T> = Result<T, ValidationError>;
|
||||||
|
|||||||
@ -6,11 +6,12 @@ const config = {
|
|||||||
"apps/**/*.{ts,tsx}",
|
"apps/**/*.{ts,tsx}",
|
||||||
"../../packages/shadcn-ui/src/**/*.{ts,tsx}",
|
"../../packages/shadcn-ui/src/**/*.{ts,tsx}",
|
||||||
"../../modules/**/*.{ts,tsx}",
|
"../../modules/**/*.{ts,tsx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user