Crear un cliente
This commit is contained in:
parent
d5438e2f4e
commit
329bc6c98c
@ -8,7 +8,7 @@ export const CoreModuleManifest: IModuleClient = {
|
|||||||
version: MODULE_VERSION,
|
version: MODULE_VERSION,
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
protected: true,
|
protected: true,
|
||||||
layout: "app",
|
layout: "app-sidebar",
|
||||||
|
|
||||||
routes: (params: ModuleClientParams) => {
|
routes: (params: ModuleClientParams) => {
|
||||||
return [];
|
return [];
|
||||||
@ -18,3 +18,4 @@ export const CoreModuleManifest: IModuleClient = {
|
|||||||
export default CoreModuleManifest;
|
export default CoreModuleManifest;
|
||||||
|
|
||||||
export * from "./lib";
|
export * from "./lib";
|
||||||
|
export * from "./utils";
|
||||||
|
|||||||
1
modules/core/src/web/utils/index.ts
Normal file
1
modules/core/src/web/utils/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./text-normalization-utils";
|
||||||
21
modules/core/src/web/utils/text-normalization-utils.ts
Normal file
21
modules/core/src/web/utils/text-normalization-utils.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Normalizes a required text form value.
|
||||||
|
*
|
||||||
|
* It trims surrounding whitespace and always returns a string.
|
||||||
|
*/
|
||||||
|
export const toRequiredText = (value: string): string => {
|
||||||
|
return value.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a nullable text form value.
|
||||||
|
*
|
||||||
|
* Rules:
|
||||||
|
* - empty or whitespace-only string => null
|
||||||
|
* - otherwise => trimmed string
|
||||||
|
*/
|
||||||
|
export const toNullableText = (value: string): string | null => {
|
||||||
|
const trimmedValue = value.trim();
|
||||||
|
|
||||||
|
return trimmedValue === "" ? null : trimmedValue;
|
||||||
|
};
|
||||||
@ -1,32 +1,45 @@
|
|||||||
|
import {
|
||||||
|
CountryCodeSchema,
|
||||||
|
CurrencyCodeSchema,
|
||||||
|
EmailSchema,
|
||||||
|
LandPhoneSchema,
|
||||||
|
LanguageCodeSchema,
|
||||||
|
MobilePhoneSchema,
|
||||||
|
PostalCodeSchema,
|
||||||
|
TinSchema,
|
||||||
|
WebsiteSchema,
|
||||||
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export const CreateCustomerRequestSchema = z.object({
|
export const CreateCustomerAddressRequestSchema = z.object({
|
||||||
id: z.string().nonempty(),
|
|
||||||
|
|
||||||
reference: z.string().optional(),
|
|
||||||
is_company: z.boolean().default(true),
|
|
||||||
name: z.string(),
|
|
||||||
trade_name: z.string().optional(),
|
|
||||||
tin: z.string().optional(),
|
|
||||||
|
|
||||||
street: z.string().optional(),
|
street: z.string().optional(),
|
||||||
street2: z.string().optional(),
|
street2: z.string().optional(),
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
province: z.string().optional(),
|
province: z.string().optional(),
|
||||||
postal_code: z.string().optional(),
|
postal_code: PostalCodeSchema.optional(),
|
||||||
country: z.string().toLowerCase().default("es").optional(),
|
country: CountryCodeSchema,
|
||||||
|
});
|
||||||
|
|
||||||
email_primary: z.string().optional(),
|
export const CreateCustomerContactRequestSchema = z.object({
|
||||||
email_secondary: z.string().optional(),
|
email_primary: EmailSchema.optional(),
|
||||||
phone_primary: z.string().optional(),
|
email_secondary: EmailSchema.optional(),
|
||||||
phone_secondary: z.string().optional(),
|
phone_primary: LandPhoneSchema.optional(),
|
||||||
mobile_primary: z.string().optional(),
|
phone_secondary: LandPhoneSchema.optional(),
|
||||||
mobile_secondary: z.string().optional(),
|
mobile_primary: MobilePhoneSchema.optional(),
|
||||||
|
mobile_secondary: MobilePhoneSchema.optional(),
|
||||||
|
fax: LandPhoneSchema.optional(),
|
||||||
|
website: WebsiteSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
fax: z.string().optional(),
|
export const CreateCustomerRequestSchema = z.object({
|
||||||
website: z.string().optional(),
|
id: z.uuid(),
|
||||||
|
|
||||||
legal_record: z.string().optional(),
|
reference: z.string().optional(),
|
||||||
|
|
||||||
|
is_company: z.boolean(),
|
||||||
|
name: z.string(),
|
||||||
|
trade_name: z.string().optional(),
|
||||||
|
tin: TinSchema.optional(),
|
||||||
|
|
||||||
payment_method_id: z.uuid().nullable().optional(),
|
payment_method_id: z.uuid().nullable().optional(),
|
||||||
payment_term_id: z.uuid().nullable().optional(),
|
payment_term_id: z.uuid().nullable().optional(),
|
||||||
@ -34,8 +47,13 @@ export const CreateCustomerRequestSchema = z.object({
|
|||||||
uses_equivalence_surcharge: z.boolean().default(false),
|
uses_equivalence_surcharge: z.boolean().default(false),
|
||||||
uses_retention: z.boolean().default(false),
|
uses_retention: z.boolean().default(false),
|
||||||
|
|
||||||
language_code: z.string().toLowerCase().default("es"),
|
address: CreateCustomerAddressRequestSchema,
|
||||||
currency_code: z.string().toUpperCase().default("EUR"),
|
contact: CreateCustomerContactRequestSchema.optional(),
|
||||||
|
|
||||||
|
legal_record: z.string().optional(),
|
||||||
|
|
||||||
|
language_code: LanguageCodeSchema,
|
||||||
|
currency_code: CurrencyCodeSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateCustomerRequestDTO = z.infer<typeof CreateCustomerRequestSchema>;
|
export type CreateCustomerRequestDTO = z.infer<typeof CreateCustomerRequestSchema>;
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
MobilePhoneSchema,
|
MobilePhoneSchema,
|
||||||
PostalCodeSchema,
|
PostalCodeSchema,
|
||||||
TinSchema,
|
TinSchema,
|
||||||
URLSchema,
|
WebsiteSchema,
|
||||||
} from "@erp/core";
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ export const GetCustomerByIdResponseSchema = z.object({
|
|||||||
mobile_secondary: MobilePhoneSchema.nullable(),
|
mobile_secondary: MobilePhoneSchema.nullable(),
|
||||||
|
|
||||||
fax: LandPhoneSchema.nullable(),
|
fax: LandPhoneSchema.nullable(),
|
||||||
website: URLSchema.nullable(),
|
website: WebsiteSchema.nullable(),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
legal_record: z.string().nullable(),
|
legal_record: z.string().nullable(),
|
||||||
|
|||||||
@ -89,8 +89,18 @@
|
|||||||
"success": {
|
"success": {
|
||||||
"message": "The customer has been created successfully.",
|
"message": "The customer has been created successfully.",
|
||||||
"title": "Customer created"
|
"title": "Customer created"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"message": "There are validation errors. Fix the highlighted fields.",
|
||||||
|
"title": "Check the fields"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"header": {
|
||||||
|
"back": "Back",
|
||||||
|
"delete": "Delete",
|
||||||
|
"keyboard_shortcuts": "View keyboard shortcuts",
|
||||||
|
"modified": "Modified"
|
||||||
|
},
|
||||||
"title": "New customer"
|
"title": "New customer"
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
|
|||||||
@ -89,8 +89,18 @@
|
|||||||
"success": {
|
"success": {
|
||||||
"message": "Se ha creado el cliente correctamente.",
|
"message": "Se ha creado el cliente correctamente.",
|
||||||
"title": "Cliente creado"
|
"title": "Cliente creado"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"message": "Hay errores de validación. Corrige los campos indicados.",
|
||||||
|
"title": "Revisa los campos"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"header": {
|
||||||
|
"back": "Volver",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"keyboard_shortcuts": "Ver atajos de teclado",
|
||||||
|
"modified": "Modificada"
|
||||||
|
},
|
||||||
"title": "Nuevo cliente"
|
"title": "Nuevo cliente"
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
import type { Customer } from "../../shared";
|
|
||||||
import type { CustomerCreateForm } from "../entities";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapea un cliente a un formulario de creación de cliente.
|
|
||||||
* Es decir, adapta el shape de datos del dominio al shape de datos
|
|
||||||
* que necesita la UI para mostrar el formulario de creación.
|
|
||||||
*
|
|
||||||
* @param customer: El cliente que se va a adaptar al formulario de creación.
|
|
||||||
* @returns Un objeto con el shape de CustomerCreateForm,
|
|
||||||
* con los datos del cliente adaptados para mostrar
|
|
||||||
* en el formulario de creación.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const mapCustomerToCustomerCreateForm = (customer: Customer): CustomerCreateForm => {
|
|
||||||
return {
|
|
||||||
reference: customer.reference ?? "",
|
|
||||||
isCompany: customer.isCompany,
|
|
||||||
name: customer.name ?? "",
|
|
||||||
tradeName: customer.tradeName ?? "",
|
|
||||||
tin: customer.tin ?? "",
|
|
||||||
|
|
||||||
defaultTaxes: customer.defaultTaxes ?? [],
|
|
||||||
|
|
||||||
street: customer.street ?? "",
|
|
||||||
street2: customer.street2 ?? "",
|
|
||||||
city: customer.city ?? "",
|
|
||||||
province: customer.province ?? "",
|
|
||||||
postalCode: customer.postalCode ?? "",
|
|
||||||
country: customer.country ?? "es",
|
|
||||||
|
|
||||||
primaryEmail: customer.primaryEmail ?? "",
|
|
||||||
secondaryEmail: customer.secondaryEmail ?? "",
|
|
||||||
primaryPhone: customer.primaryPhone ?? "",
|
|
||||||
secondaryPhone: customer.secondaryPhone ?? "",
|
|
||||||
primaryMobile: customer.primaryMobile ?? "",
|
|
||||||
secondaryMobile: customer.secondaryMobile ?? "",
|
|
||||||
|
|
||||||
fax: customer.fax ?? "",
|
|
||||||
website: customer.website ?? "",
|
|
||||||
|
|
||||||
legalRecord: customer.legalRecord ?? "",
|
|
||||||
|
|
||||||
languageCode: customer.languageCode ?? "es",
|
|
||||||
currencyCode: customer.currencyCode ?? "EUR",
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./customer-to-customer-create-form.adapter";
|
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./use-customer-create.controller";
|
export * from "./use-customer-create.controller";
|
||||||
|
export * from "./use-customer-create-fiscal-controller";
|
||||||
export * from "./use-customer-create-page.controller";
|
export * from "./use-customer-create-page.controller";
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
getPaymentMethodOptions,
|
||||||
|
usePaymentMethodsListQuery,
|
||||||
|
} from "@erp/catalogs/client/payment-methods";
|
||||||
|
import {
|
||||||
|
getPaymentTermOptions,
|
||||||
|
usePaymentTermsListQuery,
|
||||||
|
} from "@erp/catalogs/client/payment-terms";
|
||||||
|
import { getTaxRegimeOptions, useTaxRegimesListQuery } from "@erp/catalogs/client/tax-regimes";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export const useCustomerCreateFiscalController = () => {
|
||||||
|
const paymentMethodsQuery = usePaymentMethodsListQuery({
|
||||||
|
criteria: {
|
||||||
|
pageSize: 999,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "isActive",
|
||||||
|
operator: "EQUALS",
|
||||||
|
value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const paymentTermsQuery = usePaymentTermsListQuery({
|
||||||
|
criteria: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "isActive",
|
||||||
|
operator: "EQUALS",
|
||||||
|
value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const taxRegimesQuery = useTaxRegimesListQuery({
|
||||||
|
criteria: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "isActive",
|
||||||
|
operator: "EQUALS",
|
||||||
|
value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const paymentMethodOptions = useMemo(() => {
|
||||||
|
return getPaymentMethodOptions(paymentMethodsQuery.data?.items ?? []);
|
||||||
|
}, [paymentMethodsQuery.data?.items]);
|
||||||
|
|
||||||
|
const paymentTermOptions = useMemo(() => {
|
||||||
|
return getPaymentTermOptions(paymentTermsQuery.data?.items ?? []);
|
||||||
|
}, [paymentTermsQuery.data?.items]);
|
||||||
|
|
||||||
|
const taxRegimeOptions = useMemo(() => {
|
||||||
|
return getTaxRegimeOptions(taxRegimesQuery.data?.items ?? []);
|
||||||
|
}, [taxRegimesQuery.data?.items]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
paymentMethodOptions,
|
||||||
|
paymentTermOptions,
|
||||||
|
taxRegimeOptions,
|
||||||
|
|
||||||
|
isLoading: paymentMethodsQuery.isLoading || paymentTermsQuery.isLoading,
|
||||||
|
|
||||||
|
isFetching: paymentMethodsQuery.isFetching || paymentTermsQuery.isFetching,
|
||||||
|
|
||||||
|
isError: paymentMethodsQuery.isError || paymentTermsQuery.isError,
|
||||||
|
|
||||||
|
error: paymentMethodsQuery.error ?? paymentTermsQuery.error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseCustomerCreateFiscalControllerResult = ReturnType<
|
||||||
|
typeof useCustomerCreateFiscalController
|
||||||
|
>;
|
||||||
@ -3,15 +3,17 @@ import { useNavigate, useSearchParams } from "react-router-dom";
|
|||||||
import { useCustomerCreateController } from "./use-customer-create.controller";
|
import { useCustomerCreateController } from "./use-customer-create.controller";
|
||||||
|
|
||||||
export const useCustomerCreatePageController = () => {
|
export const useCustomerCreatePageController = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const returnTo = searchParams.get("returnTo") ?? "/customers";
|
|
||||||
|
|
||||||
const createCtrl = useCustomerCreateController({
|
const createCtrl = useCustomerCreateController({
|
||||||
onCreated: (customer) => navigate(`/customers/${customer.id}`),
|
onCreated: (createdCustomer) => {
|
||||||
|
navigate(`/customers/${createdCustomer.id}/edit`);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const returnTo = searchParams.get("returnTo") ?? "/customers";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createCtrl,
|
createCtrl,
|
||||||
returnTo,
|
returnTo,
|
||||||
|
|||||||
@ -1,28 +1,49 @@
|
|||||||
|
import { applyValidationErrorCollection } from "@erp/core";
|
||||||
|
import { formHasAnyDirty } from "@erp/core/client";
|
||||||
import { useHookForm } from "@erp/core/hooks";
|
import { useHookForm } from "@erp/core/hooks";
|
||||||
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
import {
|
||||||
|
UniqueID,
|
||||||
|
type ValidationErrorCollection,
|
||||||
|
isValidationErrorCollection,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import {
|
||||||
|
focusFirstInputFormError,
|
||||||
|
showErrorToast,
|
||||||
|
showSuccessToast,
|
||||||
|
showWarningToast,
|
||||||
|
} from "@repo/rdx-ui/helpers";
|
||||||
import { useId } from "react";
|
import { useId } from "react";
|
||||||
import type { FieldErrors } from "react-hook-form";
|
import type { FieldErrors } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import type { CreateCustomerParams, Customer } from "../../shared";
|
import { type CreateCustomerParams, type Customer, useCustomerCreateMutation } from "../../shared";
|
||||||
import { useCustomerCreateMutation } from "../../shared/hooks/use-customer-create-mutation";
|
import { type CustomerCreateForm, CustomerCreateFormSchema } from "../entities";
|
||||||
import { mapCustomerToCustomerCreateForm } from "../adapters";
|
import { buildCreateCustomerParams, buildCustomerCreateDefault } from "../utils";
|
||||||
import {
|
import { buildCustomerCreateData } from "../utils/build-customer-create-data";
|
||||||
type CustomerCreateForm,
|
|
||||||
CustomerCreateFormSchema,
|
import { useCustomerCreateFiscalController } from "./use-customer-create-fiscal-controller";
|
||||||
defaultCustomerCreateForm,
|
|
||||||
} from "../entities";
|
|
||||||
import { buildCreateCustomerParams } from "../utils";
|
|
||||||
|
|
||||||
export interface UseCustomerCreateControllerOptions {
|
export interface UseCustomerCreateControllerOptions {
|
||||||
onCreated?(created: Customer): void;
|
onCreated?(created: Customer): void;
|
||||||
successToasts?: boolean; // mostrar o no toast automáticcamente
|
successToasts?: boolean; // mostrar o no toast automáticamente
|
||||||
|
|
||||||
onError?(error: Error, inputPayload: CreateCustomerParams): void;
|
onError?(error: Error, params: CreateCustomerParams): void;
|
||||||
errorToasts?: boolean; // mostrar o no toast automáticcamente
|
errorToasts?: boolean; // mostrar o no toast automáticamente
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCustomerCreateController = (options?: UseCustomerCreateControllerOptions) => {
|
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
|
||||||
|
if (isValidationErrorCollection(error)) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Error("Unknown error");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateCustomerController = (options?: UseCustomerCreateControllerOptions) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const formId = useId(); // id único por instancia
|
const formId = useId(); // id único por instancia
|
||||||
|
|
||||||
@ -37,29 +58,38 @@ export const useCustomerCreateController = (options?: UseCustomerCreateControlle
|
|||||||
// 2) Form hook
|
// 2) Form hook
|
||||||
const form = useHookForm<CustomerCreateForm>({
|
const form = useHookForm<CustomerCreateForm>({
|
||||||
resolverSchema: CustomerCreateFormSchema,
|
resolverSchema: CustomerCreateFormSchema,
|
||||||
initialValues: defaultCustomerCreateForm,
|
initialValues: buildCustomerCreateDefault(),
|
||||||
disabled: isCreating,
|
disabled: isCreating,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Handlers */
|
/** Handlers */
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
form.reset(defaultCustomerCreateForm, { keepDirty: false });
|
form.reset(buildCustomerCreateDefault(), { keepDirty: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitHandler = form.handleSubmit(
|
const submitHandler = form.handleSubmit(
|
||||||
async (formData: CustomerCreateForm) => {
|
async (formData: CustomerCreateForm) => {
|
||||||
const params: CreateCustomerParams = buildCreateCustomerParams(formData);
|
if (!formHasAnyDirty(form.formState.dirtyFields)) {
|
||||||
|
showWarningToast(
|
||||||
|
t("create.feedback.no_changes.title"),
|
||||||
|
t("create.feedback.no_changes.message")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = buildCustomerCreateData(formData);
|
||||||
|
const customerId = UniqueID.generateNewID().toString();
|
||||||
|
|
||||||
|
const params = buildCreateCustomerParams(customerId, data);
|
||||||
|
console.log("Enviando creación con params:", params);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enviamos cambios al servidor
|
const created = await mutateAsync(params);
|
||||||
const created = await mutateAsync(params); // payload es CustomerCreatePayload
|
|
||||||
|
|
||||||
// Ha ido bien -> actualizamos form con datos reales
|
// Ha ido bien -> actualizamos form con datos por defecto
|
||||||
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
||||||
form.reset(mapCustomerToCustomerCreateForm(created), {
|
resetForm();
|
||||||
keepDirty: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options?.successToasts !== false) {
|
if (options?.successToasts !== false) {
|
||||||
showSuccessToast(
|
showSuccessToast(
|
||||||
@ -69,22 +99,45 @@ export const useCustomerCreateController = (options?: UseCustomerCreateControlle
|
|||||||
}
|
}
|
||||||
options?.onCreated?.(created);
|
options?.onCreated?.(created);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const normalizedError =
|
console.log(error);
|
||||||
error instanceof Error ? error : new Error(t("create.errors.unknown"));
|
const normalizedError = normalizeSubmitError(error);
|
||||||
|
console.debug(normalizedError);
|
||||||
|
|
||||||
|
// No revierto el form para que no se pierdan los cambios que
|
||||||
|
// ha hecho el usuario y no han sido guardados.
|
||||||
|
/*resetForm()*/
|
||||||
|
|
||||||
|
if (isValidationErrorCollection(normalizedError)) {
|
||||||
|
applyValidationErrorCollection(form, normalizedError);
|
||||||
|
|
||||||
|
console.log("Errores de validación aplicados al form:", form.formState.errors);
|
||||||
|
|
||||||
|
focusFirstInputFormError(form);
|
||||||
|
|
||||||
if (options?.errorToasts !== false) {
|
if (options?.errorToasts !== false) {
|
||||||
showErrorToast(t("create.errors.title"), normalizedError.message);
|
showWarningToast(
|
||||||
|
t("create.feedback.validation.title"),
|
||||||
|
t("create.feedback.validation.message")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
options?.onError?.(normalizedError, params);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.errorToasts !== false) {
|
||||||
|
showErrorToast(
|
||||||
|
t("create.errors.title"),
|
||||||
|
normalizedError.message || t("create.errors.unknown")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
options?.onError?.(normalizedError, params);
|
options?.onError?.(normalizedError, params);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<CustomerCreateForm>) => {
|
(errors: FieldErrors<CustomerCreateForm>) => {
|
||||||
const firstKey = Object.keys(errors)[0] as keyof CustomerCreateForm | undefined;
|
console.error(errors);
|
||||||
|
focusFirstInputFormError(form);
|
||||||
if (firstKey) {
|
|
||||||
document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`)?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
t("common.feedback.validation.title"),
|
t("common.feedback.validation.title"),
|
||||||
@ -94,11 +147,13 @@ export const useCustomerCreateController = (options?: UseCustomerCreateControlle
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fiscalCtrl = useCustomerCreateFiscalController();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// form
|
// form
|
||||||
form,
|
form,
|
||||||
@ -113,8 +168,13 @@ export const useCustomerCreateController = (options?: UseCustomerCreateControlle
|
|||||||
isCreateError,
|
isCreateError,
|
||||||
createError,
|
createError,
|
||||||
|
|
||||||
|
// Otros controladores
|
||||||
|
fiscalCtrl,
|
||||||
|
|
||||||
// No devolver FormProvider, así el controller es más
|
// No devolver FormProvider, así el controller es más
|
||||||
// flexible y reusable (p.ej. para un modal)
|
// flexible y reusable (p.ej. para un modal)
|
||||||
// FormProvider,
|
// FormProvider,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCustomerCreateController = useCreateCustomerController;
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* CustomerCreateData representa los datos normalizados de creación
|
||||||
|
* antes de construir el contrato de API.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - mantiene naming camelCase porque sigue siendo contrato frontend.
|
||||||
|
* - los selects opcionales sin selección quedan como undefined.
|
||||||
|
* - no es DTO backend.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type CustomerCreateData = {
|
||||||
|
isCompany: boolean;
|
||||||
|
name: string;
|
||||||
|
tradeName?: string;
|
||||||
|
tin?: string;
|
||||||
|
reference?: string;
|
||||||
|
|
||||||
|
street?: string;
|
||||||
|
street2?: string;
|
||||||
|
city?: string;
|
||||||
|
province?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country: string;
|
||||||
|
|
||||||
|
primaryEmail?: string;
|
||||||
|
secondaryEmail?: string;
|
||||||
|
primaryPhone?: string;
|
||||||
|
secondaryPhone?: string;
|
||||||
|
primaryMobile?: string;
|
||||||
|
secondaryMobile?: string;
|
||||||
|
|
||||||
|
fax?: string;
|
||||||
|
website?: string;
|
||||||
|
|
||||||
|
legalRecord?: string;
|
||||||
|
|
||||||
|
paymentMethodId?: string;
|
||||||
|
paymentTermId?: string;
|
||||||
|
taxRegimeCode?: string;
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge: boolean;
|
||||||
|
usesRetention: boolean;
|
||||||
|
|
||||||
|
languageCode: string;
|
||||||
|
currencyCode: string;
|
||||||
|
};
|
||||||
@ -39,6 +39,13 @@ export interface CustomerCreateForm {
|
|||||||
|
|
||||||
legalRecord: string;
|
legalRecord: string;
|
||||||
|
|
||||||
|
paymentMethodId: string;
|
||||||
|
paymentTermId: string;
|
||||||
|
taxRegimeCode: string;
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge: boolean;
|
||||||
|
usesRetention: boolean;
|
||||||
|
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,12 +24,13 @@ import { z } from "zod/v4";
|
|||||||
* - sin detalles impuestos por el widget
|
* - sin detalles impuestos por el widget
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const CustomerCreateFormSchema = z.object({
|
export const CustomerCreateFormSchema = z.object(
|
||||||
|
{
|
||||||
reference: z.string(),
|
reference: z.string(),
|
||||||
isCompany: z.boolean(),
|
isCompany: z.boolean(),
|
||||||
|
|
||||||
name: z.string().min(1, "El nombre es obligatorio"),
|
name: z.string().min(1, "El nombre es obligatorio"),
|
||||||
tradeName: z.string(),
|
tradeName: z.string().or(z.literal("")),
|
||||||
tin: TinSchema.or(z.literal("")),
|
tin: TinSchema.or(z.literal("")),
|
||||||
|
|
||||||
street: z.string(),
|
street: z.string(),
|
||||||
@ -52,6 +53,17 @@ export const CustomerCreateFormSchema = z.object({
|
|||||||
|
|
||||||
legalRecord: z.string().or(z.literal("")),
|
legalRecord: z.string().or(z.literal("")),
|
||||||
|
|
||||||
|
paymentMethodId: z.string().or(z.literal("")),
|
||||||
|
paymentTermId: z.string().or(z.literal("")),
|
||||||
|
taxRegimeCode: z.string().or(z.literal("")),
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge: z.boolean(),
|
||||||
|
usesRetention: z.boolean(),
|
||||||
|
|
||||||
languageCode: LanguageCodeSchema,
|
languageCode: LanguageCodeSchema,
|
||||||
currencyCode: CurrencyCodeSchema,
|
currencyCode: CurrencyCodeSchema,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
error: "error de esquema",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
|
export * from "./customer-create-data.entity";
|
||||||
export * from "./customer-create-form.entity";
|
export * from "./customer-create-form.entity";
|
||||||
export * from "./customer-create-form.schema";
|
export * from "./customer-create-form.schema";
|
||||||
export * from "./customer-create-form-default.entity";
|
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
|
|
||||||
|
export const CustomerEditorSkeleton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppContent>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div aria-hidden="true" className="space-y-2">
|
||||||
|
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
|
||||||
|
<div className="h-5 w-96 rounded-md bg-muted animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BackHistoryButton />
|
||||||
|
<Button aria-busy disabled>
|
||||||
|
{t("update.actions.submit")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden="true" className="mt-6 grid gap-4">
|
||||||
|
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
|
||||||
|
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
|
||||||
|
<div className="h-28 w-full rounded-md bg-muted animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<span className="sr-only">{t("update.loading")}</span>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-editor-skeleton";
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { SelectField } from "@repo/rdx-ui/components";
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldDescription,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSet,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
|
||||||
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../../shared";
|
|
||||||
|
|
||||||
interface CustomerAdditionalConfigFieldsProps {
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CustomerAdditionalConfigFields = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: CustomerAdditionalConfigFieldsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet className={className} {...props}>
|
|
||||||
<FieldLegend>{t("form_groups.preferences.title")}</FieldLegend>
|
|
||||||
<FieldDescription>{t("form_groups.preferences.description")}</FieldDescription>
|
|
||||||
|
|
||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
|
||||||
<Field className="lg:col-span-2">
|
|
||||||
<SelectField
|
|
||||||
description={t("fields.language_code.description")}
|
|
||||||
items={[...LANGUAGE_OPTIONS]}
|
|
||||||
label={t("fields.language_code.label")}
|
|
||||||
name="languageCode"
|
|
||||||
placeholder={t("fields.language_code.placeholder")}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field className="lg:col-span-2">
|
|
||||||
<SelectField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.currency_code.description")}
|
|
||||||
items={[...CURRENCY_OPTIONS]}
|
|
||||||
label={t("fields.currency_code.label")}
|
|
||||||
name="currencyCode"
|
|
||||||
placeholder={t("fields.currency_code.placeholder")}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
import { SelectField, TextField } from "@repo/rdx-ui/components";
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldDescription,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSet,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
|
||||||
import { COUNTRY_OPTIONS } from "../../../shared";
|
|
||||||
|
|
||||||
interface CustomerAddressFieldsProps {
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CustomerAddressFields = ({ className, ...props }: CustomerAddressFieldsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet className={className} {...props}>
|
|
||||||
<FieldLegend>{t("form_groups.address.title")}</FieldLegend>
|
|
||||||
<FieldDescription>{t("form_groups.address.description")}</FieldDescription>
|
|
||||||
|
|
||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.street.description")}
|
|
||||||
label={t("fields.street.label")}
|
|
||||||
name="street"
|
|
||||||
placeholder={t("fields.street.placeholder")}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.street2.description")}
|
|
||||||
label={t("fields.street2.label")}
|
|
||||||
name="street2"
|
|
||||||
placeholder={t("fields.street2.placeholder")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.city.description")}
|
|
||||||
label={t("fields.city.label")}
|
|
||||||
name="city"
|
|
||||||
placeholder={t("fields.city.placeholder")}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
description={t("fields.postal_code.description")}
|
|
||||||
label={t("fields.postal_code.label")}
|
|
||||||
name="postalCode"
|
|
||||||
placeholder={t("fields.postal_code.placeholder")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field className="lg:col-span-2 lg:col-start-1">
|
|
||||||
<TextField
|
|
||||||
description={t("fields.province.description")}
|
|
||||||
label={t("fields.province.label")}
|
|
||||||
name="province"
|
|
||||||
placeholder={t("fields.province.placeholder")}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field className="lg:col-span-2">
|
|
||||||
<SelectField
|
|
||||||
description={t("fields.country.description")}
|
|
||||||
items={[...COUNTRY_OPTIONS]}
|
|
||||||
label={t("fields.country.label")}
|
|
||||||
name="country"
|
|
||||||
placeholder={t("fields.country.placeholder")}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
import { FormFieldLabel, TextAreaField, TextField } from "@repo/rdx-ui/components";
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldContent,
|
|
||||||
FieldDescription,
|
|
||||||
FieldError,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSet,
|
|
||||||
RadioGroup,
|
|
||||||
RadioGroupItem,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
|
||||||
import type { CustomerCreateForm } from "../../entities";
|
|
||||||
|
|
||||||
interface CustomerBasicInfoFieldsProps extends React.ComponentProps<typeof FieldSet> {}
|
|
||||||
|
|
||||||
export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicInfoFieldsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { control, setFocus } = useFormContext<CustomerCreateForm>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFocus("name");
|
|
||||||
}, [setFocus]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet className={className} {...props}>
|
|
||||||
<FieldLegend>{t("form_groups.basic_info.title")}</FieldLegend>
|
|
||||||
<FieldDescription>{t("form_groups.basic_info.description")}</FieldDescription>
|
|
||||||
|
|
||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.name.description")}
|
|
||||||
label={t("fields.name.label")}
|
|
||||||
name="name"
|
|
||||||
placeholder={t("fields.name.placeholder")}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="isCompany"
|
|
||||||
render={({ field, fieldState }) => {
|
|
||||||
return (
|
|
||||||
<Field
|
|
||||||
className="gap-1 lg:col-span-1 lg:col-start-1"
|
|
||||||
data-invalid={fieldState.invalid}
|
|
||||||
>
|
|
||||||
<FormFieldLabel required>{t("fields.customer_type.label")}</FormFieldLabel>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
className="gap-3"
|
|
||||||
disabled={field.disabled}
|
|
||||||
name={field.name}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
field.onChange(value === "company");
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
value={field.value ? "company" : "individual"}
|
|
||||||
>
|
|
||||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<RadioGroupItem
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
id="customer-type-company"
|
|
||||||
value="company"
|
|
||||||
/>
|
|
||||||
<FieldLabel
|
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
|
||||||
htmlFor="customer-type-company"
|
|
||||||
>
|
|
||||||
{t("fields.customer_type.options.company")}
|
|
||||||
</FieldLabel>
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<RadioGroupItem
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
id="customer-type-individual"
|
|
||||||
value="individual"
|
|
||||||
/>
|
|
||||||
<FieldLabel
|
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
|
||||||
htmlFor="customer-type-individual"
|
|
||||||
>
|
|
||||||
{t("fields.customer_type.options.individual")}
|
|
||||||
</FieldLabel>
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
<FieldDescription>{t("fields.customer_type.description")}</FieldDescription>
|
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-1"
|
|
||||||
description={t("fields.tin.description")}
|
|
||||||
label={t("fields.tin.label")}
|
|
||||||
name="tin"
|
|
||||||
placeholder={t("fields.tin.placeholder")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-full"
|
|
||||||
description={t("fields.trade_name.description")}
|
|
||||||
label={t("fields.trade_name.label")}
|
|
||||||
name="tradeName"
|
|
||||||
placeholder={t("fields.trade_name.placeholder")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2 lg:col-start-1"
|
|
||||||
description={t("fields.reference.description")}
|
|
||||||
label={t("fields.reference.label")}
|
|
||||||
name="reference"
|
|
||||||
placeholder={t("fields.reference.placeholder")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextAreaField
|
|
||||||
className="lg:col-span-full"
|
|
||||||
description={t("fields.legal_record.description")}
|
|
||||||
label={t("fields.legal_record.label")}
|
|
||||||
name="legalRecord"
|
|
||||||
placeholder={t("fields.legal_record.placeholder")}
|
|
||||||
/>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
import { TextField } from "@repo/rdx-ui/components";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
Field,
|
|
||||||
FieldDescription,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSet,
|
|
||||||
Separator,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import { AtSignIcon, ChevronDown, GlobeIcon, PhoneIcon, SmartphoneIcon } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
|
||||||
|
|
||||||
interface CustomerContactFieldsProps {
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CustomerContactFields = ({ className, ...props }: CustomerContactFieldsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet className={className} {...props}>
|
|
||||||
<FieldLegend>{t("form_groups.contact_info.title")}</FieldLegend>
|
|
||||||
<FieldDescription>{t("form_groups.contact_info.description")}</FieldDescription>
|
|
||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.email_primary.description")}
|
|
||||||
label={t("fields.email_primary.label")}
|
|
||||||
leftIcon={
|
|
||||||
<AtSignIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} />
|
|
||||||
}
|
|
||||||
name="primaryEmail"
|
|
||||||
placeholder={t("fields.email_primary.placeholder")}
|
|
||||||
typePreset="email"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.mobile_primary.description")}
|
|
||||||
label={t("fields.mobile_primary.label")}
|
|
||||||
leftIcon={
|
|
||||||
<SmartphoneIcon
|
|
||||||
className="h-[18px] w-[18px] text-muted-foreground"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="primaryMobile"
|
|
||||||
placeholder={t("fields.mobile_primary.placeholder")}
|
|
||||||
typePreset="phone"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.phone_primary.description")}
|
|
||||||
label={t("fields.phone_primary.label")}
|
|
||||||
leftIcon={
|
|
||||||
<PhoneIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} />
|
|
||||||
}
|
|
||||||
name="primaryPhone"
|
|
||||||
placeholder={t("fields.phone_primary.placeholder")}
|
|
||||||
typePreset="phone"
|
|
||||||
/>
|
|
||||||
</FieldGroup>
|
|
||||||
<Separator className="mt-6" />
|
|
||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2 lg:col-start-1"
|
|
||||||
description={t("fields.email_secondary.description")}
|
|
||||||
label={t("fields.email_secondary.label")}
|
|
||||||
leftIcon={
|
|
||||||
<AtSignIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} />
|
|
||||||
}
|
|
||||||
name="secondaryEmail"
|
|
||||||
placeholder={t("fields.email_secondary.placeholder")}
|
|
||||||
typePreset="email"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.mobile_secondary.description")}
|
|
||||||
label={t("fields.mobile_secondary.label")}
|
|
||||||
leftIcon={
|
|
||||||
<SmartphoneIcon
|
|
||||||
className="h-[18px] w-[18px] text-muted-foreground"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="secondaryMobile"
|
|
||||||
placeholder={t("fields.mobile_secondary.placeholder")}
|
|
||||||
typePreset="phone"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.phone_secondary.description")}
|
|
||||||
label={t("fields.phone_secondary.label")}
|
|
||||||
leftIcon={
|
|
||||||
<PhoneIcon className="h-[18px] w-[18px] text-muted-foreground" strokeWidth={1.5} />
|
|
||||||
}
|
|
||||||
name="secondaryPhone"
|
|
||||||
placeholder={t("fields.phone_secondary.placeholder")}
|
|
||||||
typePreset="phone"
|
|
||||||
/>
|
|
||||||
</FieldGroup>
|
|
||||||
<Separator className="mt-6" />
|
|
||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
|
||||||
<Collapsible
|
|
||||||
className="space-y-8 col-start-1 col-span-full"
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
open={open}
|
|
||||||
>
|
|
||||||
<CollapsibleTrigger className="inline-flex items-center gap-1 text-sm text-primary hover:underline">
|
|
||||||
{t("common.actions.more")}{" "}
|
|
||||||
<ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<FieldGroup className="grid grid-cols-1 gap-6 lg:grid-cols-4">
|
|
||||||
<Field className="lg:col-span-2">
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.website.description")}
|
|
||||||
label={t("fields.website.label")}
|
|
||||||
leftIcon={
|
|
||||||
<GlobeIcon
|
|
||||||
className="h-[18px] w-[18px] text-muted-foreground"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="website"
|
|
||||||
placeholder={t("fields.website.placeholder")}
|
|
||||||
typePreset="text"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field className="lg:col-span-2">
|
|
||||||
<TextField
|
|
||||||
className="lg:col-span-2"
|
|
||||||
description={t("fields.fax.description")}
|
|
||||||
label={t("fields.fax.label")}
|
|
||||||
name="fax"
|
|
||||||
placeholder={t("fields.fax.placeholder")}
|
|
||||||
typePreset="phone"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
|
||||||
|
|
||||||
import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
|
|
||||||
import { CustomerAddressFields } from "./customer-address-fields";
|
|
||||||
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
|
||||||
import { CustomerContactFields } from "./customer-contact-fields";
|
|
||||||
|
|
||||||
type CustomerCreateEditorFormProps = {
|
|
||||||
formId: string;
|
|
||||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CustomerCreateEditorForm = ({
|
|
||||||
formId,
|
|
||||||
onSubmit,
|
|
||||||
className,
|
|
||||||
}: CustomerCreateEditorFormProps) => {
|
|
||||||
return (
|
|
||||||
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
|
|
||||||
<section className={cn("space-y-12 p-6", className)}>
|
|
||||||
<CustomerBasicInfoFields />
|
|
||||||
<CustomerAddressFields />
|
|
||||||
<CustomerContactFields />
|
|
||||||
<CustomerAdditionalConfigFields />
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomerAdditionalConfigEditor,
|
||||||
|
CustomerAddressEditor,
|
||||||
|
CustomerBasicInfoEditor,
|
||||||
|
CustomerContactEditor,
|
||||||
|
CustomerFiscalEditor,
|
||||||
|
} from "../../../update/ui/editor";
|
||||||
|
import type { UseCustomerCreateFiscalControllerResult } from "../../controllers";
|
||||||
|
|
||||||
|
type CustomerCreateEditorProps = {
|
||||||
|
formId: string;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
||||||
|
onReset: () => void;
|
||||||
|
|
||||||
|
fiscalCtrl: UseCustomerCreateFiscalControllerResult;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomerCreateEditorForm = ({
|
||||||
|
formId,
|
||||||
|
isSubmitting,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
|
||||||
|
fiscalCtrl,
|
||||||
|
|
||||||
|
className,
|
||||||
|
}: CustomerCreateEditorProps) => {
|
||||||
|
return (
|
||||||
|
<div className={cn("p-6", className)}>
|
||||||
|
<form
|
||||||
|
className="space-y-6 2xl:space-y-12"
|
||||||
|
id={formId}
|
||||||
|
noValidate
|
||||||
|
onKeyDown={preventEnterKeySubmitForm}
|
||||||
|
onReset={onReset}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<CustomerBasicInfoEditor disabled={isSubmitting} />
|
||||||
|
<CustomerAddressEditor disabled={isSubmitting} />
|
||||||
|
<CustomerContactEditor disabled={isSubmitting} />
|
||||||
|
<CustomerFiscalEditor
|
||||||
|
paymentMethodOptions={fiscalCtrl.paymentMethodOptions}
|
||||||
|
paymentTermOptions={fiscalCtrl.paymentTermOptions}
|
||||||
|
taxRegimeOptions={fiscalCtrl.taxRegimeOptions}
|
||||||
|
/>
|
||||||
|
<CustomerAdditionalConfigEditor disabled={isSubmitting} />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import { SpainTaxCatalogProvider } from "@erp/core";
|
|
||||||
import { MultiSelect } from "@repo/rdx-ui/components";
|
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
|
||||||
import { useCallback, useMemo } from "react";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
|
||||||
|
|
||||||
interface CustomerTaxesMultiSelectProps {
|
|
||||||
value?: string[];
|
|
||||||
onChange: (selectedValues: string[]) => void;
|
|
||||||
className?: string;
|
|
||||||
inputId?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CustomerTaxesMultiSelect = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
className,
|
|
||||||
inputId,
|
|
||||||
...otherProps
|
|
||||||
}: CustomerTaxesMultiSelectProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
|
||||||
const catalogLookup = useMemo(() => taxCatalog.toOptionLookup(), [taxCatalog]);
|
|
||||||
|
|
||||||
const filterSelectedByGroup = useCallback(
|
|
||||||
(selectedValues: string[]) => {
|
|
||||||
const groupMap = new Map<string, string>();
|
|
||||||
|
|
||||||
for (const code of selectedValues) {
|
|
||||||
const item = taxCatalog.findByCode(code).getOrUndefined();
|
|
||||||
const group = item?.group ?? "ungrouped";
|
|
||||||
groupMap.set(group, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(groupMap.values());
|
|
||||||
},
|
|
||||||
[taxCatalog]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("w-full", "max-w-md")}>
|
|
||||||
<MultiSelect
|
|
||||||
animation={0}
|
|
||||||
autoFilter
|
|
||||||
className={cn(
|
|
||||||
"flex w-full -mt-0.5 px-1 py-0.5 rounded-md border border-input min-h-8 h-auto items-center justify-between hover:bg-inherit [&_svg]:pointer-events-auto",
|
|
||||||
"hover:border-ring hover:ring-ring/50 hover:ring-2 font-medium bg-muted/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
filterSelected={filterSelectedByGroup}
|
|
||||||
id={inputId}
|
|
||||||
maxCount={3}
|
|
||||||
onValueChange={onChange}
|
|
||||||
options={catalogLookup}
|
|
||||||
placeholder={t("components.taxes_multi_select.placeholder")}
|
|
||||||
value={value ?? []}
|
|
||||||
variant="secondary"
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./customer-create-editor-form";
|
export * from "./customer-create-editor";
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { ErrorAlert } from "@erp/core/components";
|
import { ErrorAlert } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useCustomerCreatePageController } from "../../controllers";
|
import { useCustomerCreatePageController } from "../../controllers";
|
||||||
@ -10,11 +9,8 @@ import { CustomerCreateEditorForm } from "../editor";
|
|||||||
|
|
||||||
export const CustomerCreatePage = () => {
|
export const CustomerCreatePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const { createCtrl, returnTo } = useCustomerCreatePageController();
|
const { createCtrl, returnTo } = useCustomerCreatePageController();
|
||||||
|
|
||||||
const { form, formId, onSubmit, resetForm, isCreating, isCreateError, createError } = createCtrl;
|
|
||||||
|
|
||||||
const { navigateBack } = useReturnToNavigation({
|
const { navigateBack } = useReturnToNavigation({
|
||||||
fallbackPath: returnTo,
|
fallbackPath: returnTo,
|
||||||
});
|
});
|
||||||
@ -30,15 +26,15 @@ export const CustomerCreatePage = () => {
|
|||||||
hasChanges={createCtrl.form.formState.isDirty}
|
hasChanges={createCtrl.form.formState.isDirty}
|
||||||
isSaving={createCtrl.form.formState.isSubmitting}
|
isSaving={createCtrl.form.formState.isSubmitting}
|
||||||
labels={{
|
labels={{
|
||||||
title: "Nuevo cliente",
|
title: t("create.title"),
|
||||||
back: "Volver",
|
back: t("create.header.back"),
|
||||||
modified: "",
|
modified: t("create.header.modified"),
|
||||||
cancel: "Cancelar",
|
cancel: t("common.actions.cancel"),
|
||||||
save: "Guardar",
|
save: t("common.actions.save"),
|
||||||
saving: "Guardando...",
|
saving: t("common.actions.saving"),
|
||||||
moreActions: "Más acciones",
|
moreActions: t("common.actions.more"),
|
||||||
keyboardShortcuts: "Ver atajos de teclado",
|
keyboardShortcuts: t("create.header.keyboard_shortcuts"),
|
||||||
delete: "Eliminar",
|
delete: t("create.header.delete"),
|
||||||
}}
|
}}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
//onDelete={openDeleteDialog}
|
//onDelete={openDeleteDialog}
|
||||||
@ -46,17 +42,21 @@ export const CustomerCreatePage = () => {
|
|||||||
//onExportPdf={handleExportPdf}
|
//onExportPdf={handleExportPdf}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isCreateError && (
|
{createCtrl.isCreateError && (
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
message={(createError as Error)?.message ?? t("create.errors.message")}
|
message={(createCtrl.createError as Error)?.message ?? t("create.errors.message")}
|
||||||
title={t("create.errors.title")}
|
title={t("create.errors.title")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<CustomerCreateEditorForm
|
<CustomerCreateEditorForm
|
||||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto my-6"
|
className="max-w-5xl mx-auto mb-12"
|
||||||
formId={formId}
|
fiscalCtrl={createCtrl.fiscalCtrl}
|
||||||
onSubmit={onSubmit}
|
formId={createCtrl.formId}
|
||||||
|
isSubmitting={createCtrl.isCreating}
|
||||||
|
onReset={createCtrl.resetForm}
|
||||||
|
onSubmit={createCtrl.onSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
|
|||||||
@ -0,0 +1,91 @@
|
|||||||
|
import type { CreateCustomerParams } from "../../shared";
|
||||||
|
import type { CustomerCreateData } from "../entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construye los params de creación de cliente a partir del ID
|
||||||
|
* generado en cliente y el objeto de creación "CustomerCreateData".
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - el ID se recibe desde fuera para que el controller lo genere antes
|
||||||
|
* de llamar a la mutation.
|
||||||
|
* - el shape del payload debe coincidir con el contrato de la API.
|
||||||
|
* - los selects opcionales sin selección deben omitirse.
|
||||||
|
* - no debe tener transformaciones globales agresivas.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const buildCreateCustomerParams = (
|
||||||
|
id: string,
|
||||||
|
newData: CustomerCreateData
|
||||||
|
): CreateCustomerParams => {
|
||||||
|
const data: CreateCustomerParams["data"] = {
|
||||||
|
id,
|
||||||
|
is_company: newData.isCompany,
|
||||||
|
name: newData.name,
|
||||||
|
language_code: newData.languageCode,
|
||||||
|
currency_code: newData.currencyCode,
|
||||||
|
|
||||||
|
uses_equivalence_surcharge: newData.usesEquivalenceSurcharge,
|
||||||
|
uses_retention: newData.usesRetention,
|
||||||
|
|
||||||
|
address: {
|
||||||
|
country: newData.country,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newData.reference !== undefined) {
|
||||||
|
data.reference = newData.reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newData.tradeName !== undefined) {
|
||||||
|
data.trade_name = newData.tradeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newData.tin !== undefined) {
|
||||||
|
data.tin = newData.tin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newData.legalRecord !== undefined) {
|
||||||
|
data.legal_record = newData.legalRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- legal ---
|
||||||
|
if (newData.paymentMethodId !== undefined) {
|
||||||
|
data.payment_method_id = newData.paymentMethodId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newData.paymentTermId !== undefined) {
|
||||||
|
data.payment_term_id = newData.paymentTermId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newData.taxRegimeCode !== undefined) {
|
||||||
|
data.tax_regime_code = newData.taxRegimeCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- address ---
|
||||||
|
if (newData.street !== undefined) data.address.street = newData.street;
|
||||||
|
if (newData.street2 !== undefined) data.address.street2 = newData.street2;
|
||||||
|
if (newData.city !== undefined) data.address.city = newData.city;
|
||||||
|
if (newData.province !== undefined) data.address.province = newData.province;
|
||||||
|
if (newData.postalCode !== undefined) data.address.postal_code = newData.postalCode;
|
||||||
|
|
||||||
|
// --- contact ---
|
||||||
|
const contact: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (newData.primaryEmail !== undefined) contact.email_primary = newData.primaryEmail;
|
||||||
|
if (newData.secondaryEmail !== undefined) contact.email_secondary = newData.secondaryEmail;
|
||||||
|
if (newData.primaryPhone !== undefined) contact.phone_primary = newData.primaryPhone;
|
||||||
|
if (newData.secondaryPhone !== undefined) contact.phone_secondary = newData.secondaryPhone;
|
||||||
|
if (newData.primaryMobile !== undefined) contact.mobile_primary = newData.primaryMobile;
|
||||||
|
if (newData.secondaryMobile !== undefined) contact.mobile_secondary = newData.secondaryMobile;
|
||||||
|
if (newData.fax !== undefined) contact.fax = newData.fax;
|
||||||
|
if (newData.website !== undefined) contact.website = newData.website;
|
||||||
|
|
||||||
|
if (Object.keys(contact).length > 0) {
|
||||||
|
data.contact = contact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
} satisfies CreateCustomerParams;
|
||||||
|
};
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { toNullableText, toRequiredText } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { CustomerCreateData, CustomerCreateForm } from "../entities";
|
||||||
|
|
||||||
|
import { buildCustomerCreateDefault } from "./build-customer-create-defaults";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construye un objeto de creación de cliente a partir de los datos
|
||||||
|
* del formulario.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - `undefined` = no enviar
|
||||||
|
*
|
||||||
|
* @param formData
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const buildCustomerCreateData = (formData: CustomerCreateForm): CustomerCreateData => {
|
||||||
|
const defaults = buildCustomerCreateDefault();
|
||||||
|
return {
|
||||||
|
reference: toNullableText(formData.reference) ?? undefined,
|
||||||
|
isCompany: formData.isCompany,
|
||||||
|
name: toRequiredText(formData.name),
|
||||||
|
tradeName: toNullableText(formData.tradeName) ?? undefined,
|
||||||
|
tin: toNullableText(formData.tin) ?? undefined,
|
||||||
|
|
||||||
|
street: toNullableText(formData.street) ?? undefined,
|
||||||
|
street2: toNullableText(formData.street2) ?? undefined,
|
||||||
|
city: toNullableText(formData.city) ?? undefined,
|
||||||
|
province: toNullableText(formData.province) ?? undefined,
|
||||||
|
postalCode: toNullableText(formData.postalCode) ?? undefined,
|
||||||
|
country: toRequiredText(formData.country).toUpperCase(),
|
||||||
|
|
||||||
|
primaryEmail: toNullableText(formData.primaryEmail) ?? undefined,
|
||||||
|
secondaryEmail: toNullableText(formData.secondaryEmail) ?? undefined,
|
||||||
|
primaryPhone: toNullableText(formData.primaryPhone) ?? undefined,
|
||||||
|
secondaryPhone: toNullableText(formData.secondaryPhone) ?? undefined,
|
||||||
|
primaryMobile: toNullableText(formData.primaryMobile) ?? undefined,
|
||||||
|
secondaryMobile: toNullableText(formData.secondaryMobile) ?? undefined,
|
||||||
|
|
||||||
|
fax: toNullableText(formData.fax) ?? undefined,
|
||||||
|
website: toNullableText(formData.website) ?? undefined,
|
||||||
|
|
||||||
|
legalRecord: toNullableText(formData.legalRecord) ?? undefined,
|
||||||
|
|
||||||
|
paymentMethodId: toNullableText(formData.paymentMethodId) ?? undefined,
|
||||||
|
paymentTermId: toNullableText(formData.paymentTermId) ?? undefined,
|
||||||
|
taxRegimeCode: toNullableText(formData.taxRegimeCode) ?? undefined,
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge:
|
||||||
|
formData.usesEquivalenceSurcharge ?? defaults.usesEquivalenceSurcharge,
|
||||||
|
usesRetention: formData.usesRetention ?? defaults.usesRetention,
|
||||||
|
|
||||||
|
languageCode: toRequiredText(formData.languageCode).toLowerCase(),
|
||||||
|
currencyCode: toRequiredText(formData.currencyCode).toUpperCase(),
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import type { CustomerCreateForm } from "./customer-create-form.entity";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valor por defecto para el formulario de creación de cliente.
|
* Valor por defecto para el formulario de creación de cliente.
|
||||||
*
|
*
|
||||||
@ -10,7 +8,9 @@ import type { CustomerCreateForm } from "./customer-create-form.entity";
|
|||||||
* - orientado a la UI, no a la API ni al dominio, es decir, debe ser un objeto que se pueda usar directamente para inicializar un formulario en la interfaz de usuario
|
* - orientado a la UI, no a la API ni al dominio, es decir, debe ser un objeto que se pueda usar directamente para inicializar un formulario en la interfaz de usuario
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const defaultCustomerCreateForm: CustomerCreateForm = {
|
import type { CustomerCreateForm } from "../entities";
|
||||||
|
|
||||||
|
export const buildCustomerCreateDefault = (): CustomerCreateForm => ({
|
||||||
reference: "",
|
reference: "",
|
||||||
isCompany: true,
|
isCompany: true,
|
||||||
name: "",
|
name: "",
|
||||||
@ -22,7 +22,7 @@ export const defaultCustomerCreateForm: CustomerCreateForm = {
|
|||||||
city: "",
|
city: "",
|
||||||
province: "",
|
province: "",
|
||||||
postalCode: "",
|
postalCode: "",
|
||||||
country: "es",
|
country: "ES",
|
||||||
|
|
||||||
primaryEmail: "",
|
primaryEmail: "",
|
||||||
secondaryEmail: "",
|
secondaryEmail: "",
|
||||||
@ -36,6 +36,13 @@ export const defaultCustomerCreateForm: CustomerCreateForm = {
|
|||||||
|
|
||||||
legalRecord: "",
|
legalRecord: "",
|
||||||
|
|
||||||
|
paymentMethodId: "",
|
||||||
|
paymentTermId: "",
|
||||||
|
taxRegimeCode: "",
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge: false,
|
||||||
|
usesRetention: false,
|
||||||
|
|
||||||
languageCode: "es",
|
languageCode: "es",
|
||||||
currencyCode: "EUR",
|
currencyCode: "EUR",
|
||||||
};
|
});
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import { UniqueID } from "@repo/rdx-ddd";
|
|
||||||
|
|
||||||
import type { CreateCustomerParams } from "../../shared";
|
|
||||||
import type { CustomerCreateForm } from "../entities";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construye un payload de creación de cliente a partir de los datos
|
|
||||||
* del formulario y los campos sucios.
|
|
||||||
*
|
|
||||||
* Reglas:
|
|
||||||
* - el payload debe ser un objeto con solo las propiedades que han cambiado (campos sucios).
|
|
||||||
* - no debe incluir campos que no han cambiado.
|
|
||||||
* - el shape del payload debe coincidir con el de CustomerCreatePayload,
|
|
||||||
* es decir, orientado a la API.
|
|
||||||
* - no debe tener transformaciones ni campos adicionales, solo los que vienen del
|
|
||||||
* formulario y están sucios.
|
|
||||||
*
|
|
||||||
* @param formData - Los datos del formulario de creación de cliente.
|
|
||||||
* @returns Un objeto que se puede enviar a la API para crear un cliente,
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const buildCreateCustomerParams = (formData: CustomerCreateForm): CreateCustomerParams => {
|
|
||||||
// La API de creación de cliente requiere un ID único para el nuevo cliente
|
|
||||||
const id = UniqueID.generateNewID().toString();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
|
|
||||||
data: {
|
|
||||||
id,
|
|
||||||
reference: formData.reference,
|
|
||||||
is_company: formData.isCompany,
|
|
||||||
name: formData.name,
|
|
||||||
trade_name: formData.tradeName,
|
|
||||||
tin: formData.tin,
|
|
||||||
|
|
||||||
street: formData.street,
|
|
||||||
street2: formData.street2,
|
|
||||||
city: formData.city,
|
|
||||||
province: formData.province,
|
|
||||||
postal_code: formData.postalCode,
|
|
||||||
country: formData.country,
|
|
||||||
|
|
||||||
email_primary: formData.primaryEmail,
|
|
||||||
email_secondary: formData.secondaryEmail,
|
|
||||||
phone_primary: formData.primaryPhone,
|
|
||||||
phone_secondary: formData.secondaryPhone,
|
|
||||||
mobile_primary: formData.primaryMobile,
|
|
||||||
mobile_secondary: formData.secondaryMobile,
|
|
||||||
|
|
||||||
fax: formData.fax,
|
|
||||||
website: formData.website,
|
|
||||||
|
|
||||||
legal_record: formData.legalRecord,
|
|
||||||
|
|
||||||
uses_equivalence_surcharge: false,
|
|
||||||
uses_retention: false,
|
|
||||||
|
|
||||||
language_code: formData.languageCode,
|
|
||||||
currency_code: formData.currencyCode,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from "./build-customer-create-params";
|
export * from "./build-create-customer-params";
|
||||||
|
export * from "./build-customer-create-defaults";
|
||||||
|
|||||||
@ -33,7 +33,17 @@ export const useCustomerCreateMutation = () => {
|
|||||||
|
|
||||||
const result = schema.safeParse(data);
|
const result = schema.safeParse(data);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
console.log(
|
||||||
|
"Error de validación al crear nuevo cliente:",
|
||||||
|
toValidationErrors(result.error)
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorCollection = new ValidationErrorCollection(
|
||||||
|
"Validation failed",
|
||||||
|
toValidationErrors(result.error)
|
||||||
|
);
|
||||||
|
console.log("Errores de validación convertidos:", errorCollection);
|
||||||
|
throw errorCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dto: CreateCustomerResult = await createCustomer(dataSource, params);
|
const dto: CreateCustomerResult = await createCustomer(dataSource, params);
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export const useCustomerUpdateMutation = () => {
|
|||||||
|
|
||||||
mutationFn: async (params) => {
|
mutationFn: async (params) => {
|
||||||
const { id: customerId, data } = params;
|
const { id: customerId, data } = params;
|
||||||
|
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
throw new Error("customerId is required");
|
throw new Error("customerId is required");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,6 @@ import type { CustomerUpdateForm } from "../entities"; /**
|
|||||||
import { buildCustomerUpdateDefault } from "../utils";
|
import { buildCustomerUpdateDefault } from "../utils";
|
||||||
|
|
||||||
export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpdateForm => {
|
export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpdateForm => {
|
||||||
console.log(customer);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reference: customer.reference ?? "",
|
reference: customer.reference ?? "",
|
||||||
isCompany: customer.isCompany ?? buildCustomerUpdateDefault().isCompany,
|
isCompany: customer.isCompany ?? buildCustomerUpdateDefault().isCompany,
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export const CustomerUpdateEditorForm = ({
|
|||||||
id={formId}
|
id={formId}
|
||||||
noValidate
|
noValidate
|
||||||
onKeyDown={preventEnterKeySubmitForm}
|
onKeyDown={preventEnterKeySubmitForm}
|
||||||
|
onReset={onReset}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<CustomerBasicInfoEditor disabled={isSubmitting} />
|
<CustomerBasicInfoEditor disabled={isSubmitting} />
|
||||||
|
|||||||
@ -1 +1,6 @@
|
|||||||
|
export * from "./customer-additional-config-fields";
|
||||||
|
export * from "./customer-address-editor";
|
||||||
|
export * from "./customer-basic-info-editor";
|
||||||
|
export * from "./customer-contact-fields";
|
||||||
export * from "./customer-edit-form";
|
export * from "./customer-edit-form";
|
||||||
|
export * from "./customer-fiscal-editor";
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export const buildCustomerUpdateDefault = (): CustomerUpdateForm => {
|
|||||||
city: "",
|
city: "",
|
||||||
province: "",
|
province: "",
|
||||||
postalCode: "",
|
postalCode: "",
|
||||||
country: "es",
|
country: "ES",
|
||||||
|
|
||||||
primaryEmail: "",
|
primaryEmail: "",
|
||||||
secondaryEmail: "",
|
secondaryEmail: "",
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export * from "./build-customer.update-patch";
|
|
||||||
export * from "./build-customer-update-default";
|
export * from "./build-customer-update-default";
|
||||||
|
export * from "./build-customer-update-patch";
|
||||||
export * from "./build-update-customer-by-id-params";
|
export * from "./build-update-customer-by-id-params";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user