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} />
|
||||
</Suspense>
|
||||
</TooltipProvider>
|
||||
<Toaster expand={true} position="bottom-center" />
|
||||
<Toaster expand={true} position="top-center" richColors />
|
||||
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
||||
</AuthProvider>
|
||||
</DataSourceProvider>
|
||||
|
||||
@ -16,8 +16,8 @@ import { z } from "zod/v4";
|
||||
export const TinSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TIN cannot be empty.")
|
||||
.max(32, "TIN is too long.")
|
||||
.min(2, "TIN cannot be empty.")
|
||||
.max(10, "TIN is too long.")
|
||||
.regex(/^(?=.*[A-Za-z0-9])[A-Za-z0-9.\-/ ]+$/, "TIN has an invalid format.");
|
||||
|
||||
export type TinDTO = z.infer<typeof TinSchema>;
|
||||
|
||||
@ -70,8 +70,15 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
||||
data: TData,
|
||||
params?: Record<string, unknown>
|
||||
): Promise<R> => {
|
||||
const url = `${resource}/${id}`;
|
||||
|
||||
console.log("Axios updateOne => ", {
|
||||
url,
|
||||
data,
|
||||
});
|
||||
|
||||
const res = await client.put<R, AxiosResponse<R>, TData>(
|
||||
`${resource}/${id}`,
|
||||
url,
|
||||
data,
|
||||
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 "./http-url-utils";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IModuleClient, ModuleClientParams } from "./lib";
|
||||
import type { IModuleClient, ModuleClientParams } from "./lib";
|
||||
|
||||
export const MODULE_NAME = "Core";
|
||||
const MODULE_VERSION = "1.0.0";
|
||||
|
||||
@ -50,6 +50,8 @@ export class ProformaUpdater implements IProformaUpdater {
|
||||
return Result.fail(updateResult.error);
|
||||
}
|
||||
|
||||
console.log(proforma);
|
||||
|
||||
// Persistir cambios
|
||||
const saveResult = await this.repository.update(proforma, transaction);
|
||||
|
||||
|
||||
@ -16,23 +16,19 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
||||
private readonly itemsBuilder: IProformaItemsFullSnapshotBuilder,
|
||||
private readonly recipientBuilder: IProformaRecipientFullSnapshotBuilder,
|
||||
private readonly taxesBuilder: IProformaTaxesFullSnapshotBuilder
|
||||
//private readonly paymentMethodBuilder: IProformaPaymentMethodFullSnapshotBuilder
|
||||
) {}
|
||||
|
||||
toOutput(proforma: Proforma): GetProformaByIdResponseDTO {
|
||||
const items = this.itemsBuilder.toOutput(proforma.items);
|
||||
const recipient = this.recipientBuilder.toOutput(proforma);
|
||||
const taxes = this.taxesBuilder.toOutput(proforma.taxes());
|
||||
//const paymentMethod = this.paymentMethodBuilder.toOutput(proforma.paymentMethod);
|
||||
|
||||
const payment = proforma.paymentMethod.match(
|
||||
(payment) => {
|
||||
const { id, payment_description } = payment.toObjectString();
|
||||
return {
|
||||
id: id,
|
||||
description: payment_description,
|
||||
};
|
||||
},
|
||||
() => null
|
||||
);
|
||||
const paymentMethod = maybeToNullable(proforma.paymentMethodId, (value) => ({
|
||||
id: value.toString(),
|
||||
description: "",
|
||||
}));
|
||||
|
||||
const allTotals = proforma.totals();
|
||||
|
||||
@ -59,7 +55,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
||||
|
||||
linked_invoice_id: maybeToNullable(proforma.linkedInvoiceId, (value) => value.toString()),
|
||||
|
||||
payment_method: payment,
|
||||
payment_method: paymentMethod,
|
||||
|
||||
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
|
||||
items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(),
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||
import type { ProformaItemDetailDTO } from "@erp/customer-invoices/common";
|
||||
import {
|
||||
maybeToEmptyMoneyObjectString,
|
||||
maybeToEmptyPercentageObjectString,
|
||||
maybeToEmptyQuantityObjectString,
|
||||
maybeToEmptyString,
|
||||
maybeToNullable,
|
||||
} from "@repo/rdx-ddd";
|
||||
@ -17,6 +15,7 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
|
||||
private mapItem(proformaItem: ProformaItem, index: number): ProformaItemDetailDTO {
|
||||
const allAmounts = proformaItem.totals();
|
||||
const isValued = proformaItem.isValued();
|
||||
const currencyCode = proformaItem.currencyCode.code;
|
||||
|
||||
return {
|
||||
id: proformaItem.id.toPrimitive(),
|
||||
@ -25,54 +24,58 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
|
||||
|
||||
description: maybeToNullable(proformaItem.description, (value) => value.toString()),
|
||||
|
||||
quantity: maybeToEmptyQuantityObjectString(proformaItem.quantity),
|
||||
unit_amount: maybeToEmptyMoneyObjectString(proformaItem.unitAmount),
|
||||
quantity: maybeToNullable(proformaItem.quantity, (value) => value.toObjectString()),
|
||||
unit_amount: maybeToNullable(proformaItem.unitAmount, (value) => value.toObjectString()),
|
||||
|
||||
subtotal_amount: isValued
|
||||
? allAmounts.subtotalAmount.toObjectString()
|
||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
||||
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||
|
||||
item_discount_percentage: maybeToEmptyPercentageObjectString(
|
||||
proformaItem.itemDiscountPercentage
|
||||
item_discount_percentage: maybeToNullable(proformaItem.itemDiscountPercentage, (value) =>
|
||||
value.toObjectString()
|
||||
),
|
||||
item_discount_amount: isValued
|
||||
? allAmounts.itemDiscountAmount.toObjectString()
|
||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
||||
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||
|
||||
global_discount_percentage: proformaItem.globalDiscountPercentage.toObjectString(),
|
||||
|
||||
global_discount_amount: isValued
|
||||
? allAmounts.globalDiscountAmount.toObjectString()
|
||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
||||
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||
|
||||
total_discount_amount: isValued
|
||||
? allAmounts.totalDiscountAmount.toObjectString()
|
||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
||||
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||
|
||||
taxable_amount: isValued
|
||||
? allAmounts.taxableAmount.toObjectString()
|
||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
||||
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||
|
||||
iva_code: maybeToEmptyString(proformaItem.ivaCode()),
|
||||
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_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_percentage: maybeToEmptyPercentageObjectString(proformaItem.retentionPercentage()),
|
||||
retention_amount: isValued
|
||||
? allAmounts.retentionAmount.toObjectString()
|
||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
||||
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||
|
||||
taxes_amount: isValued
|
||||
? allAmounts.taxesAmount.toObjectString()
|
||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
||||
: ItemAmount.zero(currencyCode).toObjectString(),
|
||||
total_amount: isValued
|
||||
? 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);
|
||||
}
|
||||
|
||||
private static validateCreateProps(props: IProformaCreateProps): Result<void, Error> {
|
||||
private static validateCreateProps(
|
||||
props: IProformaCreateProps | ProformaInternalProps
|
||||
): Result<void, Error> {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@ -169,7 +171,14 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
||||
...otherProps,
|
||||
};
|
||||
|
||||
console.log(candidateProps);
|
||||
|
||||
// Validacciones
|
||||
const validationResult = Proforma.validateCreateProps(candidateProps);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(validationResult.error);
|
||||
}
|
||||
|
||||
// Aplicar cambios
|
||||
Object.assign(this.props, candidateProps);
|
||||
|
||||
@ -10,12 +10,12 @@ export const ProformaItemDetailSchema = z.object({
|
||||
position: ItemPositionSchema,
|
||||
description: z.string().nullable(),
|
||||
|
||||
quantity: QuantitySchema,
|
||||
unit_amount: MoneySchema,
|
||||
quantity: QuantitySchema.nullable(),
|
||||
unit_amount: MoneySchema.nullable(),
|
||||
|
||||
subtotal_amount: MoneySchema,
|
||||
|
||||
item_discount_percentage: PercentageSchema,
|
||||
item_discount_percentage: PercentageSchema.nullable(),
|
||||
item_discount_amount: MoneySchema,
|
||||
|
||||
global_discount_percentage: PercentageSchema,
|
||||
|
||||
@ -24,7 +24,6 @@ export const GetProformaByIdAdapter = {
|
||||
return {
|
||||
id: dto.id,
|
||||
companyId: dto.company_id,
|
||||
isProforma: dto.is_proforma === "1",
|
||||
|
||||
invoiceNumber: dto.invoice_number,
|
||||
status: dto.status as ProformaStatus,
|
||||
@ -69,6 +68,8 @@ const mapItem = (dto: GetProformaByIdResponseDTO["items"][number]): ProformaItem
|
||||
return {
|
||||
id: dto.id,
|
||||
position: Number(dto.position),
|
||||
isValued: dto.is_valued,
|
||||
|
||||
description: dto.description,
|
||||
|
||||
quantity: QuantityDTOHelper.toNumber(dto.quantity),
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
export interface ProformaItem {
|
||||
id: string;
|
||||
position: number;
|
||||
isValued: boolean;
|
||||
|
||||
description: string;
|
||||
|
||||
quantity: number;
|
||||
|
||||
@ -33,7 +33,12 @@ export const useProformaUpdateMutation = () => {
|
||||
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
||||
}
|
||||
|
||||
console.log("Hola!!!");
|
||||
|
||||
const dto: UpdateProformaByIdResult = await updateProformaById(dataSource, params);
|
||||
|
||||
console.log(dto);
|
||||
|
||||
return GetProformaByIdAdapter.fromDto(dto);
|
||||
},
|
||||
onSuccess: async (proforma) => {
|
||||
|
||||
@ -2,7 +2,7 @@ import type { ProformaItem } from "../../shared";
|
||||
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
|
||||
* @returns
|
||||
@ -14,6 +14,7 @@ export const mapProformaItemsToProformaItemsUpdateForm = (
|
||||
return {
|
||||
id: item.id,
|
||||
position: item.position,
|
||||
isValued: item.isValued,
|
||||
description: item.description,
|
||||
quantity: item.quantity,
|
||||
unitAmount: item.unitAmount,
|
||||
|
||||
@ -4,7 +4,7 @@ import type { ProformaUpdateForm } from "../entities";
|
||||
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
|
||||
* @returns
|
||||
|
||||
@ -2,6 +2,13 @@ import type { CustomerSelectionOption } from "@erp/customers/common";
|
||||
|
||||
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 = (
|
||||
proforma?: Proforma | null
|
||||
): CustomerSelectionOption | null => {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { applyValidationErrorCollection } from "@erp/core";
|
||||
import { formHasAnyDirty } from "@erp/core/client";
|
||||
import { useHookForm } from "@erp/core/hooks";
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { type ValidationErrorCollection, isValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
||||
import { useEffect, useId, useMemo, useState } from "react";
|
||||
import type { FieldErrors } from "react-hook-form";
|
||||
@ -15,7 +17,7 @@ import {
|
||||
buildProformaUpdateDefault,
|
||||
buildProformaUpdatePatch,
|
||||
buildUpdateProformaByIdParams,
|
||||
focusFirstProformaUpdateError,
|
||||
focusFirstProformaUpdateFormError,
|
||||
} from "../utils";
|
||||
|
||||
import { useUpdateProformaItemsController } from "./use-update-proforma-items-controller";
|
||||
@ -28,6 +30,18 @@ export interface UseUpdateProformaControllerOptions {
|
||||
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 = (
|
||||
proformaId?: string,
|
||||
options?: UseUpdateProformaControllerOptions
|
||||
@ -66,6 +80,7 @@ export const useUpdateProformaController = (
|
||||
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<CustomerSelectionOption | null>(null);
|
||||
|
||||
/** Reiniciar el form al recibir datos */
|
||||
useEffect(() => {
|
||||
if (!proformaData) return;
|
||||
|
||||
@ -115,8 +130,6 @@ export const useUpdateProformaController = (
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(form.formState.dirtyFields);
|
||||
|
||||
if (!formHasAnyDirty(form.formState.dirtyFields)) {
|
||||
showWarningToast(
|
||||
t("proformas.update.no_changes.title"),
|
||||
@ -125,11 +138,24 @@ export const useUpdateProformaController = (
|
||||
return;
|
||||
}
|
||||
|
||||
const previousData = proformaData;
|
||||
//const previousData = proformaData;
|
||||
|
||||
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);
|
||||
|
||||
console.log("Enviando actualización con params:", params);
|
||||
|
||||
try {
|
||||
// Enviamos cambios al servidor
|
||||
const updated = await mutateAsync(params);
|
||||
@ -151,38 +177,59 @@ export const useUpdateProformaController = (
|
||||
|
||||
options?.onUpdated?.(updated);
|
||||
} catch (error: unknown) {
|
||||
const normalizedError =
|
||||
error instanceof Error ? error : new Error(t("pages.update.error.unknown"));
|
||||
const normalizedError = normalizeSubmitError(error);
|
||||
|
||||
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
|
||||
? mapProformaToProformaUpdateForm(previousData)
|
||||
: buildProformaUpdateDefault(),
|
||||
{ keepDirty: false }
|
||||
);
|
||||
|
||||
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) {
|
||||
showErrorToast(t("proformas.update.error.title"), normalizedError.message);
|
||||
}
|
||||
|
||||
options?.onError?.(normalizedError, params);
|
||||
}
|
||||
},
|
||||
(errors: FieldErrors<ProformaUpdateForm>) => {
|
||||
console.log(errors);
|
||||
focusFirstProformaUpdateError(errors);
|
||||
focusFirstProformaUpdateFormError(form);
|
||||
|
||||
showWarningToast(
|
||||
t("proformas.update.validation.title"),
|
||||
t("proformas.update.validation.message")
|
||||
t("forms.validation.title", "Revisa los campos"),
|
||||
t("forms.validation.message", "Hay errores de validación en el formulario.")
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// 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
|
||||
submitHandler(event);
|
||||
};
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
export interface ProformaItemUpdateForm {
|
||||
id: string;
|
||||
position: number;
|
||||
isValued: boolean;
|
||||
|
||||
description: string;
|
||||
|
||||
quantity: number | null;
|
||||
unitAmount: number | null;
|
||||
|
||||
itemDiscountPercentage: number | null;
|
||||
}
|
||||
|
||||
@ -17,10 +17,13 @@ import { z } from "zod/v4";
|
||||
export const ProformaItemUpdateFormSchema = z.object({
|
||||
id: z.uuid(),
|
||||
position: z.number().int().nonnegative(),
|
||||
isValued: z.boolean(),
|
||||
|
||||
description: z.string().trim(),
|
||||
quantity: z.number().positive().nullable(),
|
||||
description: z.string(),
|
||||
|
||||
quantity: z.number().nullable(),
|
||||
unitAmount: z.number().nonnegative().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 {
|
||||
id: string;
|
||||
position: number;
|
||||
description: string;
|
||||
isValued: boolean;
|
||||
|
||||
description: string | null;
|
||||
|
||||
quantity: number | null;
|
||||
unitAmount: 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 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 { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema";
|
||||
@ -17,23 +18,23 @@ import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema
|
||||
*/
|
||||
|
||||
export const ProformaUpdateFormSchema = z.object({
|
||||
series: z.string().default(""),
|
||||
series: z.string().or(z.literal("")),
|
||||
|
||||
invoiceDate: z.string().default(""),
|
||||
operationDate: z.string().default(""),
|
||||
invoiceDate: z.string(),
|
||||
operationDate: z.string().or(z.literal("")),
|
||||
|
||||
customerId: z.string().default(""),
|
||||
customerId: z.string(),
|
||||
|
||||
description: z.string().default(""),
|
||||
reference: z.string().default(""),
|
||||
notes: z.string().default(""),
|
||||
description: z.string(),
|
||||
reference: z.string().or(z.literal("")),
|
||||
notes: z.string().or(z.literal("")),
|
||||
|
||||
languageCode: z.string().min(1, "Debe indicar un idioma").default("es"),
|
||||
currencyCode: z.string().min(1, "Debe indicar una moneda").default("EUR"),
|
||||
languageCode: LanguageCodeSchema,
|
||||
currencyCode: CurrencyCodeSchema,
|
||||
|
||||
globalDiscountPercentage: z.number().default(0),
|
||||
globalDiscountPercentage: z.number(),
|
||||
|
||||
paymentMethod: z.string().default(""),
|
||||
paymentMethod: z.string().or(z.literal("")),
|
||||
|
||||
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.
|
||||
* Se representa con las mismas propiedades que ProformaUpdateForm,
|
||||
* pero todas ellas son opcionales.
|
||||
*
|
||||
* A la API solo hay que enviar los campos que han cambiado.
|
||||
* ProformaUpdatePatch representa los cambios que se han hecho
|
||||
* en el formulario de edición y que se aplicarán a la proforma.
|
||||
*
|
||||
* Reglas:
|
||||
* - debe ser un Partial de ProformaUpdateForm
|
||||
* - no debe tener campos adicionales ni transformaciones
|
||||
* - debe ser un shape orientado a la API, no a la UI ni al dominio
|
||||
* - sin shape DTO, solo tipos simples y directos
|
||||
* - `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
|
||||
*/
|
||||
|
||||
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,
|
||||
FormSectionGrid,
|
||||
SelectField,
|
||||
TextAreaField,
|
||||
TextField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
|
||||
@ -72,6 +73,17 @@ export const ProformaUpdateHeaderEditor = ({
|
||||
placeholder={t("form_fields.proformas.description.placeholder")}
|
||||
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>
|
||||
</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 { CopyIcon, MoveDownIcon, MoveUpIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
@ -34,6 +40,12 @@ export const ProformaUpdateItemRowEditor = ({
|
||||
<Card>
|
||||
<CardContent className="space-y-4 pt-6">
|
||||
<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
|
||||
className="col-span-12"
|
||||
label={t("form_fields.items.description.label", "Descripción")}
|
||||
|
||||
@ -6,7 +6,10 @@ export const buildProformaItemUpdateDefault = (position: number): ProformaItemUp
|
||||
return {
|
||||
id: UniqueID.generateNewID().toString(),
|
||||
position,
|
||||
isValued: false,
|
||||
|
||||
description: "",
|
||||
|
||||
quantity: null,
|
||||
unitAmount: null,
|
||||
itemDiscountPercentage: null,
|
||||
|
||||
@ -3,6 +3,19 @@ import type { FieldNamesMarkedBoolean } from "react-hook-form";
|
||||
|
||||
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 = (
|
||||
formData: 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 { 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 = (
|
||||
id: string,
|
||||
@ -9,38 +27,86 @@ export const buildUpdateProformaByIdParams = (
|
||||
throw new Error("proformaId is required");
|
||||
}
|
||||
|
||||
const data: UpdateProformaByIdParams["data"] = {
|
||||
series: patch.series,
|
||||
const data: UpdateProformaByIdParams["data"] = {};
|
||||
|
||||
invoice_date: patch.invoiceDate,
|
||||
operation_date: patch.operationDate,
|
||||
if (ObjectHelper.hasOwn(patch, "series")) {
|
||||
data.series = patch.series;
|
||||
}
|
||||
|
||||
customer_id: patch.customerId,
|
||||
if (ObjectHelper.hasOwn(patch, "invoiceDate")) {
|
||||
data.invoice_date = patch.invoiceDate;
|
||||
}
|
||||
|
||||
reference: patch.reference,
|
||||
description: patch.description,
|
||||
notes: patch.notes,
|
||||
if (ObjectHelper.hasOwn(patch, "operationDate")) {
|
||||
data.operation_date = patch.operationDate;
|
||||
}
|
||||
|
||||
language_code: patch.languageCode,
|
||||
currency_code: patch.currencyCode,
|
||||
if (ObjectHelper.hasOwn(patch, "customerId")) {
|
||||
data.customer_id = patch.customerId;
|
||||
}
|
||||
|
||||
items: patch.items?.map((item) => ({
|
||||
id: item.id,
|
||||
position: String(item.position),
|
||||
description: item.description,
|
||||
quantity: item.quantity === null ? undefined : String(item.quantity),
|
||||
unit_amount: item.unitAmount === null ? undefined : String(item.unitAmount),
|
||||
item_discount_percentage:
|
||||
item.itemDiscountPercentage === null
|
||||
? undefined
|
||||
: { value: String(item.itemDiscountPercentage), scale: "2" },
|
||||
})),
|
||||
};
|
||||
if (ObjectHelper.hasOwn(patch, "reference")) {
|
||||
data.reference = patch.reference;
|
||||
}
|
||||
|
||||
if (ObjectHelper.hasOwn(patch, "description")) {
|
||||
data.description = patch.description;
|
||||
}
|
||||
|
||||
if (ObjectHelper.hasOwn(patch, "notes")) {
|
||||
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 {
|
||||
id,
|
||||
data: Object.fromEntries(
|
||||
Object.entries(data).filter(([, value]) => value !== undefined)
|
||||
) satisfies UpdateProformaByIdParams["data"],
|
||||
data,
|
||||
} satisfies UpdateProformaByIdParams;
|
||||
};
|
||||
|
||||
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-update-default";
|
||||
export * from "./build-proforma-update-default";
|
||||
export * from "./build-proforma-update-patch";
|
||||
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;
|
||||
|
||||
if (!id) throw new Error("customerId is required");
|
||||
|
||||
return dataSource.updateOne<UpdateCustomerByIdRequestDTO, UpdateCustomerByIdResponseDTO>(
|
||||
"customers",
|
||||
id,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { applyValidationErrorCollection } from "@erp/core";
|
||||
import { formHasAnyDirty } from "@erp/core/client";
|
||||
import { useHookForm } from "@erp/core/hooks";
|
||||
import { type ValidationErrorCollection, isValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
||||
@ -13,12 +14,9 @@ import {
|
||||
useCustomerUpdateMutation,
|
||||
} from "../../shared";
|
||||
import { mapCustomerToCustomerUpdateForm } from "../adapters";
|
||||
import { type CustomerUpdateForm, CustomerUpdateFormSchema } from "../entities";
|
||||
import {
|
||||
type CustomerUpdateForm,
|
||||
CustomerUpdateFormSchema,
|
||||
defaultCustomerUpdateForm,
|
||||
} from "../entities";
|
||||
import {
|
||||
buildCustomerUpdateDefault,
|
||||
buildCustomerUpdatePatch,
|
||||
buildUpdateCustomerByIdParams,
|
||||
focusFirstCustomerUpdateFormError,
|
||||
@ -68,7 +66,7 @@ export const useCustomerUpdateController = (
|
||||
} = useCustomerUpdateMutation();
|
||||
|
||||
const initialValues = useMemo<CustomerUpdateForm>(() => {
|
||||
if (!customerData) return defaultCustomerUpdateForm;
|
||||
if (!customerData) return buildCustomerUpdateDefault();
|
||||
|
||||
return mapCustomerToCustomerUpdateForm(customerData);
|
||||
}, [customerData]);
|
||||
@ -95,7 +93,7 @@ export const useCustomerUpdateController = (
|
||||
const resetForm = () => {
|
||||
const initialData = customerData
|
||||
? mapCustomerToCustomerUpdateForm(customerData)
|
||||
: defaultCustomerUpdateForm;
|
||||
: buildCustomerUpdateDefault();
|
||||
|
||||
form.reset(initialData, { keepDirty: false });
|
||||
};
|
||||
@ -107,6 +105,14 @@ export const useCustomerUpdateController = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formHasAnyDirty(form.formState.dirtyFields)) {
|
||||
showWarningToast(
|
||||
t("customers.update.no_changes.title"),
|
||||
t("customers.update.no_changes.message")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
//const previousData = customerData;
|
||||
|
||||
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
|
||||
// ha hecho el usuario y no han sido guardados.
|
||||
/*form.reset(
|
||||
previousData ? mapCustomerToCustomerUpdateForm(previousData) : defaultCustomerUpdateForm,
|
||||
previousData ? mapCustomerToCustomerUpdateForm(previousData) : buildCustomerUpdateDefault(),
|
||||
{ keepDirty: false }
|
||||
);*/
|
||||
|
||||
@ -186,6 +192,7 @@ export const useCustomerUpdateController = (
|
||||
}
|
||||
},
|
||||
(errors: FieldErrors<CustomerUpdateForm>) => {
|
||||
console.log(errors);
|
||||
focusFirstCustomerUpdateFormError(form);
|
||||
|
||||
showWarningToast(
|
||||
@ -196,7 +203,7 @@ export const useCustomerUpdateController = (
|
||||
);
|
||||
|
||||
// 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
|
||||
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 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:
|
||||
* - `undefined` => no enviar
|
||||
@ -8,7 +9,7 @@
|
||||
* - mantiene naming camelCase porque sigue siendo contrato frontend
|
||||
*/
|
||||
|
||||
export interface CustomerUpdatePatch {
|
||||
export type CustomerUpdatePatch = {
|
||||
reference?: string | null;
|
||||
isCompany?: boolean;
|
||||
name?: string;
|
||||
@ -38,4 +39,4 @@ export interface CustomerUpdatePatch {
|
||||
|
||||
languageCode?: string;
|
||||
currencyCode?: string;
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export * from "./customer-update-form.entity";
|
||||
export * from "./customer-update-form.schema";
|
||||
export * from "./customer-update-form-default.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-default";
|
||||
export * from "./build-update-customer-by-id-params";
|
||||
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.
|
||||
*
|
||||
* Si `input` es `null` o `undefined`, devuelve `null`.
|
||||
* En caso contrario, aplica `map`.
|
||||
* Casos:
|
||||
* - `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 R - Tipo de salida.
|
||||
* @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.
|
||||
*/
|
||||
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`.
|
||||
*
|
||||
* - Si `Maybe` es `None`, devuelve `onNone`.
|
||||
* - Si es `Some`, aplica `map`.
|
||||
* Casos:
|
||||
* - `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 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:
|
||||
* - `null | undefined` → `null`
|
||||
* - `T` → `map(T)`
|
||||
* Casos:
|
||||
* - `null` → `null`
|
||||
* - `undefined` → `null`
|
||||
* - valor directo `T` → `map(value)`
|
||||
* - `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.
|
||||
* @param input - Valor o `Maybe` a transformar.
|
||||
* @param map - Función de transformación.
|
||||
* @param input - Valor directo, nullable o `Maybe` a transformar.
|
||||
* @param map - Función de transformación aplicada solo cuando hay valor.
|
||||
* @returns Valor transformado o `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()`
|
||||
* - Si tiene valor → aplica `validate`
|
||||
* - Success → `Maybe.some`
|
||||
* - Failure → propaga error
|
||||
* Casos:
|
||||
* - `null` → `Result.ok(Maybe.none())`
|
||||
* - `undefined` → `Result.ok(Maybe.none())`
|
||||
* - `""` → `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 S - Tipo de entrada.
|
||||
* @param input - Valor a validar.
|
||||
* @param validate - Función de validación.
|
||||
* @returns Resultado con `Maybe<T>`.
|
||||
* @param validate - Función de validación/construcción del valor cuando `input` tiene contenido.
|
||||
* @returns Resultado con `Maybe<T>` o error de validación.
|
||||
*/
|
||||
export function maybeFromNullableResult<T, 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.
|
||||
*
|
||||
* - `None` → `emptyObject`
|
||||
* - `Some` → `map(value)` o `defaultSerializer(value)`
|
||||
* Casos:
|
||||
* - `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
|
||||
*
|
||||
* @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 emptyObject - Objeto vacío a devolver si es `None`.
|
||||
* @param defaultSerializer - Serializador por defecto.
|
||||
* @param map - Serializador opcional personalizado.
|
||||
* @param emptyObject - Objeto a devolver si es `None`.
|
||||
* @param defaultSerializer - Serializador por defecto para `Some`.
|
||||
* @param map - Serializador opcional personalizado; prevalece sobre `defaultSerializer`.
|
||||
* @returns Objeto serializado.
|
||||
*/
|
||||
function maybeToEmptyObjectString<T, R>(
|
||||
@ -124,11 +171,28 @@ function maybeToEmptyObjectString<T, R>(
|
||||
/**
|
||||
* Serializa un `Maybe<MoneyValue>` a un objeto de transporte.
|
||||
*
|
||||
* - `None` → objeto vacío de dinero
|
||||
* - `Some` → `map` o `toObjectString()`
|
||||
* Casos:
|
||||
* - `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 map - Transformación opcional.
|
||||
* @param map - Transformación opcional; si existe, sustituye a `toObjectString()`.
|
||||
* @returns Objeto de dinero serializado.
|
||||
*/
|
||||
export function maybeToEmptyMoneyObjectString(
|
||||
@ -146,11 +210,27 @@ export function maybeToEmptyMoneyObjectString(
|
||||
/**
|
||||
* Serializa un `Maybe<Percentage>` a un objeto de transporte.
|
||||
*
|
||||
* - `None` → objeto vacío
|
||||
* - `Some` → `map` o `toObjectString()`
|
||||
* Casos:
|
||||
* - `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 map - Transformación opcional.
|
||||
* @param map - Transformación opcional; si existe, sustituye a `toObjectString()`.
|
||||
* @returns Objeto de porcentaje serializado.
|
||||
*/
|
||||
export function maybeToEmptyPercentageObjectString(
|
||||
@ -168,11 +248,27 @@ export function maybeToEmptyPercentageObjectString(
|
||||
/**
|
||||
* Serializa un `Maybe<Quantity>` a un objeto de transporte.
|
||||
*
|
||||
* - `None` → objeto vacío
|
||||
* - `Some` → `map` o `toObjectString()`
|
||||
* Casos:
|
||||
* - `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 map - Transformación opcional.
|
||||
* @param map - Transformación opcional; si existe, sustituye a `toObjectString()`.
|
||||
* @returns Objeto de cantidad serializado.
|
||||
*/
|
||||
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`
|
||||
* - `""` o whitespace → `None`
|
||||
* - valor válido → `Some(trimmed)`
|
||||
* Casos:
|
||||
* - `null` → `Maybe.none()`
|
||||
* - `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.
|
||||
* @returns Maybe del string.
|
||||
* @returns Maybe del string normalizado con `trim()`.
|
||||
*/
|
||||
export function maybeFromNullableOrEmptyString(input?: string | null): Maybe<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`.
|
||||
*
|
||||
* - `None` → `null`
|
||||
* - `Some` → `map(value)`
|
||||
* Casos:
|
||||
* - `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 R - Tipo de salida.
|
||||
* @param maybe - Instancia Maybe.
|
||||
* @param map - Transformación.
|
||||
* @returns Valor serializado o null.
|
||||
* @param map - Transformación aplicada solo en caso `Some`.
|
||||
* @returns Valor serializado o `null`.
|
||||
*/
|
||||
export function maybeToNullable<T, R>(maybe: Maybe<T>, map: (t: T) => R): R | null {
|
||||
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`.
|
||||
*
|
||||
* - `None` → `null`
|
||||
* - `Some` → `map(value)` o `String(value)`
|
||||
* Casos:
|
||||
* - `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 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 {
|
||||
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`.
|
||||
*
|
||||
* - `None` → `""`
|
||||
* - `Some` → `map(value)` o `String(value)`
|
||||
* Casos:
|
||||
* - `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 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 {
|
||||
return serializeMaybe(maybe, "", (value) => (map ? map(value) : String(value)));
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { translateZodValidationError } from "../helpers";
|
||||
|
||||
import { ValueObject } from "./value-object";
|
||||
|
||||
interface TINNumberProps {
|
||||
@ -20,7 +22,8 @@ export class TINNumber extends ValueObject<TINNumberProps> {
|
||||
})
|
||||
.max(TINNumber.MAX_LENGTH, {
|
||||
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);
|
||||
}
|
||||
|
||||
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-input-field/index.ts";
|
||||
export * from "./decimal-field/index.ts";
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export * from "./collection";
|
||||
export * from "./id-utils";
|
||||
export * from "./maybe";
|
||||
export * from "./object-helper";
|
||||
export * from "./patch-field";
|
||||
export * from "./result";
|
||||
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";
|
||||
|
||||
export type TRuleValidatorResult<T> = Result<T, ValidationError>;
|
||||
|
||||
@ -6,11 +6,12 @@ const config = {
|
||||
"apps/**/*.{ts,tsx}",
|
||||
"../../packages/shadcn-ui/src/**/*.{ts,tsx}",
|
||||
"../../modules/**/*.{ts,tsx}",
|
||||
],
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user