diff --git a/modules/customers/src/web/_archived/pages/update/customer-update-page.tsx b/modules/customers/src/web/_archived/pages/update/customer-update-page.tsx index fd802640..822cdfa0 100644 --- a/modules/customers/src/web/_archived/pages/update/customer-update-page.tsx +++ b/modules/customers/src/web/_archived/pages/update/customer-update-page.tsx @@ -3,7 +3,7 @@ import { UnsavedChangesProvider, UpdateCommitButtonGroup, useUrlParamId } from " import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components"; import { useTranslation } from "../../../i18n"; -import { useCustomerUpdateController } from "../../../update/controllers/use-customer-update-page.controller"; +import { useCustomerUpdatePageController } from "../../../update/controllers/use-customer-update.controller"; import { CustomerEditForm, CustomerEditorSkeleton, @@ -31,7 +31,7 @@ export const CustomerUpdatePage = () => { updateError, FormProvider, - } = useCustomerUpdateController(customerId, {}); + } = useCustomerUpdatePageController(customerId, {}); if (isLoading) { return ; diff --git a/modules/customers/src/web/update/adapters/customer-to-customer-update-form.adapter.ts b/modules/customers/src/web/update/adapters/customer-to-customer-update-form.adapter.ts new file mode 100644 index 00000000..1e96f903 --- /dev/null +++ b/modules/customers/src/web/update/adapters/customer-to-customer-update-form.adapter.ts @@ -0,0 +1,45 @@ +import type { Customer } from "../../shared"; +import type { CustomerUpdateForm } from "../entities"; + +/** + * Mapea un cliente a un formulario de actualizació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 actualización. + * + * @param customer + * @returns + */ + +export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpdateForm => { + 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", + }; +}; diff --git a/modules/customers/src/web/update/adapters/index.ts b/modules/customers/src/web/update/adapters/index.ts new file mode 100644 index 00000000..aeb1fcdc --- /dev/null +++ b/modules/customers/src/web/update/adapters/index.ts @@ -0,0 +1 @@ +export * from "./customer-to-customer-update-form.adapter"; diff --git a/modules/customers/src/web/update/controllers/index.ts b/modules/customers/src/web/update/controllers/index.ts index debbf28e..5dbb7701 100644 --- a/modules/customers/src/web/update/controllers/index.ts +++ b/modules/customers/src/web/update/controllers/index.ts @@ -1 +1,2 @@ +export * from "./use-customer-update.controller"; export * from "./use-customer-update-page.controller"; diff --git a/modules/customers/src/web/update/controllers/use-customer-update-page.controller.ts b/modules/customers/src/web/update/controllers/use-customer-update-page.controller.ts index bc46e7b1..3754cec7 100644 --- a/modules/customers/src/web/update/controllers/use-customer-update-page.controller.ts +++ b/modules/customers/src/web/update/controllers/use-customer-update-page.controller.ts @@ -1,145 +1,18 @@ -import { formHasAnyDirty, pickFormDirtyValues } from "@erp/core/client"; -import { useHookForm } from "@erp/core/hooks"; -import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers"; -import { useEffect, useId, useMemo } from "react"; -import { type FieldErrors, FormProvider } from "react-hook-form"; +import { useUrlParamId } from "@erp/core/hooks"; +import { useCustomerUpdateController } from "./use-customer-update.controller"; -import { useTranslation } from "../../i18n"; -import { type Customer, useCustomerGetQuery, useCustomerUpdateMutation } from "../../shared"; -import { type CustomerFormData, CustomerFormSchema, defaultCustomerFormData } from "../types"; +/** + * Hook para gestionar el controlador de la página de actualización de cliente. + * + * @returns + */ -export interface UseCustomerUpdateControllerOptions { - onUpdated?(updated: Customer): void; - successToasts?: boolean; // mostrar o no toast automáticcamente +export const useCustomerUpdatePageController = () => { + const customerId = useUrlParamId(); - onError?(error: Error, patchData: ReturnType): void; - errorToasts?: boolean; // mostrar o no toast automáticcamente -} - -export const useCustomerUpdateController = ( - customerId?: string, - options?: UseCustomerUpdateControllerOptions -) => { - const { t } = useTranslation(); - const formId = useId(); // id único por instancia - - // 1) Estado de carga del cliente (query) - const { - data: customerData, - isLoading, - isError: isLoadError, - error: loadError, - } = useCustomerGetQuery(customerId, { enabled: Boolean(customerId) }); - - // 2) Estado de creación (mutación) - const { - mutateAsync, - isPending: isUpdating, - isError: isUpdateError, - error: updateError, - } = useCustomerUpdateMutation(); - - const initialValues = useMemo(() => customerData ?? defaultCustomerFormData, [customerData]); - - // 3) Form hook - const form = useHookForm({ - resolverSchema: CustomerFormSchema, - initialValues, - disabled: isLoading || isUpdating, - }); - - /** Reiniciar el form al recibir datos */ - useEffect(() => { - // keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render. - if (customerData) { - console.log("Reseteando form con datos del cliente:", customerData); - form.reset(customerData, { keepDirty: false }); - } - }, [customerData, form]); - - /** Handlers */ - - const resetForm = () => form.reset(customerData ?? defaultCustomerFormData); - - // Versión sincronizada - const submitHandler = form.handleSubmit( - async (formData) => { - if (!customerId) { - showErrorToast(t("pages.update.error.title"), "Falta el ID del cliente"); - return; - } - - const { dirtyFields } = form.formState; - if (!formHasAnyDirty(dirtyFields)) { - showWarningToast(t("pages.update.error.no_changes"), "No hay cambios para guardar"); - return; - } - - const patchData = pickFormDirtyValues(formData, dirtyFields); - const previousData = customerData; - - try { - // Enviamos cambios al servidor - const updated = await mutateAsync({ id: customerId, data: patchData }); - - // Ha ido bien -> actualizamos form con datos reales - // keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render. - form.reset(updated, { keepDirty: false }); - - if (options?.successToasts !== false) { - showSuccessToast( - t("pages.update.success.title", "Cliente modificado"), - t("pages.update.success.message", "Se ha modificado correctamente.") - ); - } - options?.onUpdated?.(updated); - } catch (error: any) { - // Algo ha fallado -> revertimos cambios - form.reset(previousData ?? defaultCustomerFormData); - if (options?.errorToasts !== false) { - showErrorToast(t("pages.update.error.title"), error.message); - } - options?.onError?.(error, patchData); - } - }, - (errors: FieldErrors) => { - const firstKey = Object.keys(errors)[0] as keyof CustomerFormData | undefined; - if (firstKey) document.querySelector(`[name="${String(firstKey)}"]`)?.focus(); - - showWarningToast( - 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
- const onSubmit = (event: React.FormEvent) => { - event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM - submitHandler(event); - }; + const updateCtrl = useCustomerUpdateController(customerId); return { - // form - form, - formId, - - // handlers del form - onSubmit, - resetForm, - - // carga de datos - customerData, - isLoading, - isLoadError, - loadError, - - // mutation - isUpdating, - isUpdateError, - updateError, - - // Por comodidad - FormProvider, + updateCtrl, }; -}; +}; \ No newline at end of file diff --git a/modules/customers/src/web/update/controllers/use-customer-update.controller.ts b/modules/customers/src/web/update/controllers/use-customer-update.controller.ts new file mode 100644 index 00000000..6314de9a --- /dev/null +++ b/modules/customers/src/web/update/controllers/use-customer-update.controller.ts @@ -0,0 +1,177 @@ +import { formHasAnyDirty } from "@erp/core/client"; +import { useHookForm } from "@erp/core/hooks"; +import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers"; +import { useEffect, useId, useMemo } from "react"; +import type { FieldErrors } from "react-hook-form"; + +import { useTranslation } from "../../i18n"; +import { type Customer, useCustomerGetQuery, useCustomerUpdateMutation } from "../../shared"; +import { mapCustomerToCustomerUpdateForm } from "../adapters"; +import { + type CustomerUpdateForm, + CustomerUpdateFormSchema, + type CustomerUpdatePatch, + defaultCustomerUpdateForm, +} from "../entities"; +import { buildCustomerUpdatePatch } from "../utils"; + +export interface UseCustomerUpdateControllerOptions { + onUpdated?(updated: Customer): void; + successToasts?: boolean; // mostrar o no toast automáticamente + + onError?(error: Error, patchData: CustomerUpdatePatch): void; + errorToasts?: boolean; // mostrar o no toast automáticamente +} + +export const useCustomerUpdateController = ( + customerId?: string, + options?: UseCustomerUpdateControllerOptions +) => { + const { t } = useTranslation(); + const formId = useId(); // id único por instancia + + // 1) Estado de carga del cliente (query) + const { + data: customerData, + isLoading, + isError: isLoadError, + error: loadError, + } = useCustomerGetQuery(customerId, { enabled: Boolean(customerId) }); + + // 2) Estado de creación (mutación) + const { + mutateAsync, + isPending: isUpdating, + isError: isUpdateError, + error: updateError, + } = useCustomerUpdateMutation(); + + const initialValues = useMemo(() => { + if (!customerData) return defaultCustomerUpdateForm; + + return mapCustomerToCustomerUpdateForm(customerData); + }, [customerData]); + + // 3) Form hook + const form = useHookForm({ + resolverSchema: CustomerUpdateFormSchema, + initialValues, + disabled: isLoading || isUpdating, + }); + + /** Reiniciar el form al recibir datos */ + useEffect(() => { + if (!customerData) return; + + console.log("Reseteando form con datos del cliente:", customerData); + form.reset(mapCustomerToCustomerUpdateForm(customerData), { + keepDirty: false, // <-- importante: no marca el form como "dirty" al cargar los datos reales + }); + }, [customerData, form]); + + /** Handlers */ + + const resetForm = () => { + const initialData = customerData + ? mapCustomerToCustomerUpdateForm(customerData) + : defaultCustomerUpdateForm; + + form.reset(initialData, { keepDirty: false }); + }; + + const submitHandler = form.handleSubmit( + async (formData) => { + if (!customerId) { + showErrorToast(t("pages.update.error.title"), "Falta el ID del cliente"); + return; + } + + const { dirtyFields } = form.formState; + + if (!formHasAnyDirty(dirtyFields)) { + showWarningToast(t("pages.update.error.no_changes"), "No hay cambios para guardar"); + return; + } + + const patchData: CustomerUpdatePatch = buildCustomerUpdatePatch(formData, dirtyFields); + const previousData = customerData; + + try { + // Enviamos cambios al servidor + const updated = await mutateAsync({ id: customerId, data: patchData }); + + // Ha ido bien -> actualizamos form con datos reales + // keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render. + form.reset(mapCustomerToCustomerUpdateForm(updated), { + keepDirty: false, + }); + + if (options?.successToasts !== false) { + showSuccessToast( + t("pages.update.success.title", "Cliente modificado"), + t("pages.update.success.message", "Se ha modificado correctamente.") + ); + } + + options?.onUpdated?.(updated); + } catch (error: unknown) { + const normalizedError = + error instanceof Error ? error : new Error(t("pages.update.error.unknown")); + + form.reset( + previousData ? mapCustomerToCustomerUpdateForm(previousData) : defaultCustomerUpdateForm, + { keepDirty: false } + ); + + if (options?.errorToasts !== false) { + showErrorToast(t("pages.update.error.title"), normalizedError.message); + } + + options?.onError?.(normalizedError, patchData); + } + }, + (errors: FieldErrors) => { + const firstKey = Object.keys(errors)[0] as keyof CustomerUpdateForm | undefined; + + if (firstKey) { + document.querySelector(`[name="${String(firstKey)}"]`)?.focus(); + } + + showWarningToast( + 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 + const onSubmit = (event: React.FormEvent) => { + event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM + submitHandler(event); + }; + + return { + // form + form, + formId, + + // handlers del form + onSubmit, + resetForm, + + // carga de datos + customerData, + isLoading, + isLoadError, + loadError, + + // mutation + isUpdating, + isUpdateError, + updateError, + + // No devolver FormProvider, así el controller es más + // flexible y reusable (p.ej. para un modal) + // FormProvider, + }; +}; diff --git a/modules/customers/src/web/update/entities/customer-update-form-defaults.ts b/modules/customers/src/web/update/entities/customer-update-form-defaults.ts new file mode 100644 index 00000000..5b31227f --- /dev/null +++ b/modules/customers/src/web/update/entities/customer-update-form-defaults.ts @@ -0,0 +1,33 @@ +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", +}; diff --git a/modules/customers/src/web/update/entities/customer-update-form.entity.ts b/modules/customers/src/web/update/entities/customer-update-form.entity.ts new file mode 100644 index 00000000..fc8d6236 --- /dev/null +++ b/modules/customers/src/web/update/entities/customer-update-form.entity.ts @@ -0,0 +1,46 @@ +/** + * CustomerUpdateForm representa el shape de datos del formulario de actualización de cliente. + * Es decir, los campos que se muestran en el formulario y que el usuario puede editar. + * + * Este shape es específico para la UI y no tiene por qué coincidir + * con el shape del dominio ni con el de la API. + * + * Debe cumplir las siguientes reglas: + * - nombres en camelCase + * - tipos orientados a UI/form + * - sin campos de solo lectura que no se editen + * - sin shape DTO + * - sin detalles impuestos por el widget + */ + +export interface CustomerUpdateForm { + reference: string; + isCompany: boolean; + name: string; + tradeName: string; + tin: string; + + defaultTaxes: 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; + + languageCode: string; + currencyCode: string; +} diff --git a/modules/customers/src/web/update/entities/customer-update-form.schema.ts b/modules/customers/src/web/update/entities/customer-update-form.schema.ts new file mode 100644 index 00000000..9f8a9771 --- /dev/null +++ b/modules/customers/src/web/update/entities/customer-update-form.schema.ts @@ -0,0 +1,47 @@ +import { z } from "zod/v4"; + +/** + * Este esquema es para validar los datos del formulario de actualización de cliente. + * No tiene por qué coincidir con el shape de la entidad ni con el de la API. + * Solo define los campos que se muestran en el formulario y sus validaciones. + * + * Reglas: + * - no meter transformaciones silenciosas raras en el esquema (ej: .toUpperCase()) + * - nombres en camelCase + * - tipos orientados a UI/form + * - sin campos de solo lectura que no se editen + * - sin shape DTO + * - sin detalles impuestos por el widget + */ + +export const CustomerUpdateFormSchema = z.object({ + reference: z.string(), + isCompany: z.boolean(), + name: z.string().min(1, "El nombre es obligatorio"), + tradeName: z.string(), + tin: z.string(), + + defaultTaxes: z.array(z.string()), + + street: z.string(), + street2: z.string(), + city: z.string(), + province: z.string(), + postalCode: z.string(), + country: z.string().min(1, "El país es obligatorio"), + + primaryEmail: z.string(), + secondaryEmail: z.string(), + primaryPhone: z.string(), + secondaryPhone: z.string(), + primaryMobile: z.string(), + secondaryMobile: z.string(), + + fax: z.string(), + website: z.string(), + + legalRecord: z.string(), + + languageCode: z.string().min(1, "El idioma es obligatorio"), + currencyCode: z.string().min(1, "La moneda es obligatoria"), +}); diff --git a/modules/customers/src/web/update/entities/customer-update-patch.entity.ts b/modules/customers/src/web/update/entities/customer-update-patch.entity.ts new file mode 100644 index 00000000..46a5670d --- /dev/null +++ b/modules/customers/src/web/update/entities/customer-update-patch.entity.ts @@ -0,0 +1,18 @@ +import type { CustomerUpdateForm } from "./customer-update-form.entity"; + +/** + * CustomerUpdatePatch es un tipo que representa un objeto con las mismas + * propiedades que CustomerUpdateForm, pero todas ellas son opcionales. + * + * Esto es útil para representar los datos que se van a enviar a la API para actualizar un cliente, + * ya que en una actualización parcial (PATCH) no es necesario enviar todos los campos, + * sino solo aquellos que se quieren modificar. + * + * Reglas: + * - debe ser un Partial de CustomerUpdateForm + * - 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 + */ + +export type CustomerUpdatePatch = Partial; diff --git a/modules/customers/src/web/update/entities/index.ts b/modules/customers/src/web/update/entities/index.ts new file mode 100644 index 00000000..e235049a --- /dev/null +++ b/modules/customers/src/web/update/entities/index.ts @@ -0,0 +1,4 @@ +export * from "./customer-update-form.entity"; +export * from "./customer-update-form.schema"; +export * from "./customer-update-form-defaults"; +export * from "./customer-update-patch.entity"; diff --git a/modules/customers/src/web/update/hooks/index.ts b/modules/customers/src/web/update/hooks/index.ts new file mode 100644 index 00000000..486e525c --- /dev/null +++ b/modules/customers/src/web/update/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./use-customer-update-form"; +export * from "./use-customer-update-patch"; diff --git a/modules/customers/src/web/update/hooks/use-customer-update-form.ts b/modules/customers/src/web/update/hooks/use-customer-update-form.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/customers/src/web/update/hooks/use-customer-update-patch.ts b/modules/customers/src/web/update/hooks/use-customer-update-patch.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/customers/src/web/update/types/index.ts b/modules/customers/src/web/update/types/index.ts deleted file mode 100644 index eea524d6..00000000 --- a/modules/customers/src/web/update/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./types"; diff --git a/modules/customers/src/web/update/types/types.ts b/modules/customers/src/web/update/types/types.ts deleted file mode 100644 index 97eb0ddb..00000000 --- a/modules/customers/src/web/update/types/types.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { z } from "zod/v4"; - -export const CustomerFormSchema = z.object({ - reference: z.string().optional(), - - is_company: z.string().default("true"), - name: z - .string({ - error: "El nombre es obligatorio", - }) - .min(1, "El nombre no puede estar vacío"), - trade_name: z.string().optional(), - tin: z.string().optional(), - default_taxes: z.array(z.string()).default([]), - - street: z.string().optional(), - street2: z.string().optional(), - city: z.string().optional(), - province: z.string().optional(), - postal_code: z.string().optional(), - country: z - .string({ - error: "El país es obligatorio", - }) - .min(1, "El país no puede estar vacío") - .toLowerCase() // asegura minúsculas - .default("es"), - - email_primary: z.string().optional(), - email_secondary: z.string().optional(), - phone_primary: z.string().optional(), - phone_secondary: z.string().optional(), - mobile_primary: z.string().optional(), - mobile_secondary: z.string().optional(), - - fax: z.string().optional(), - website: z.string().optional(), - - legal_record: z.string().optional(), - - language_code: z - .string({ - error: "El idioma es obligatorio", - }) - .min(1, "Debe indicar un idioma") - .toUpperCase() // asegura mayúsculas - .default("es"), - - currency_code: z - .string({ - error: "La moneda es obligatoria", - }) - .min(1, "La moneda no puede estar vacía") - .toUpperCase() // asegura mayúsculas - .default("EUR"), -}); - -export type CustomerFormData = z.infer; - -export const defaultCustomerFormData: CustomerFormData = { - reference: "", - - is_company: "", - name: "", - trade_name: "", - tin: "", - default_taxes: [], - - street: "", - street2: "", - city: "", - province: "", - postal_code: "", - country: "es", - - email_primary: "", - email_secondary: "", - phone_primary: "", - phone_secondary: "", - mobile_primary: "", - mobile_secondary: "", - - fax: "", - website: "", - - legal_record: "", - language_code: "", - currency_code: "", -}; diff --git a/modules/customers/src/web/update/ui/editor/customer-additional-config-fields.tsx b/modules/customers/src/web/update/ui/editor/customer-additional-config-fields.tsx index d87a1509..daaab3f1 100644 --- a/modules/customers/src/web/update/ui/editor/customer-additional-config-fields.tsx +++ b/modules/customers/src/web/update/ui/editor/customer-additional-config-fields.tsx @@ -6,11 +6,9 @@ import { FieldLegend, FieldSet, } from "@repo/shadcn-ui/components"; -import { useFormContext } from "react-hook-form"; import { useTranslation } from "../../../i18n"; import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../../shared"; -import type { CustomerFormData } from "../../types"; interface CustomerAdditionalConfigFieldsProps { className?: string; @@ -21,19 +19,19 @@ export const CustomerAdditionalConfigFields = ({ ...props }: CustomerAdditionalConfigFieldsProps) => { const { t } = useTranslation(); - const { control } = useFormContext(); return (
{t("form_groups.preferences.title")} {t("form_groups.preferences.description")} + @@ -44,7 +42,7 @@ export const CustomerAdditionalConfigFields = ({ description={t("form_fields.currency_code.description")} items={[...CURRENCY_OPTIONS]} label={t("form_fields.currency_code.label")} - name="currency_code" + name="currencyCode" placeholder={t("form_fields.currency_code.placeholder")} required /> diff --git a/modules/customers/src/web/update/ui/editor/customer-address-fields.tsx b/modules/customers/src/web/update/ui/editor/customer-address-fields.tsx index 55e6d065..6f27d192 100644 --- a/modules/customers/src/web/update/ui/editor/customer-address-fields.tsx +++ b/modules/customers/src/web/update/ui/editor/customer-address-fields.tsx @@ -6,11 +6,9 @@ import { FieldLegend, FieldSet, } from "@repo/shadcn-ui/components"; -import { useFormContext } from "react-hook-form"; import { useTranslation } from "../../../i18n"; import { COUNTRY_OPTIONS } from "../../../shared"; -import type { CustomerFormData } from "../../types"; interface CustomerAddressFieldsProps { className?: string; @@ -18,12 +16,12 @@ interface CustomerAddressFieldsProps { export const CustomerAddressFields = ({ className, ...props }: CustomerAddressFieldsProps) => { const { t } = useTranslation(); - const { control } = useFormContext(); return (
{t("form_groups.address.title")} {t("form_groups.address.description")} + diff --git a/modules/customers/src/web/update/ui/editor/customer-basic-info-fields.tsx b/modules/customers/src/web/update/ui/editor/customer-basic-info-fields.tsx index 5450817e..0c58a0c3 100644 --- a/modules/customers/src/web/update/ui/editor/customer-basic-info-fields.tsx +++ b/modules/customers/src/web/update/ui/editor/customer-basic-info-fields.tsx @@ -12,7 +12,7 @@ import { useEffect } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "../../../i18n"; -import type { CustomerFormData } from "../../types"; +import type { CustomerUpdateForm } from "../../entities"; import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select"; @@ -26,9 +26,8 @@ export const CustomerBasicInfoFields = ({ ...props }: CustomerBasicInfoFieldsProps) => { const { t } = useTranslation(); - const { control, setFocus } = useFormContext(); + const { control, setFocus } = useFormContext(); - // Enfoca el primer campo recibido useEffect(() => { setFocus("name"); }, [setFocus]); @@ -37,6 +36,7 @@ export const CustomerBasicInfoFields = ({
{t("form_groups.basic_info.title")} {t("form_groups.basic_info.description")} + - + + ( + + field.onChange(value === "true")} + value={String(field.value)} + /> + + + )} + /> + @@ -92,10 +104,10 @@ export const CustomerBasicInfoFields = ({ ( - + {t("form_fields.default_taxes.label")} @@ -107,17 +119,19 @@ export const CustomerBasicInfoFields = ({ required value={field.value} /> + {t("form_fields.default_taxes.description")} )} /> + diff --git a/modules/customers/src/web/update/ui/editor/customer-contact-fields.tsx b/modules/customers/src/web/update/ui/editor/customer-contact-fields.tsx index b35a10c6..a477ce23 100644 --- a/modules/customers/src/web/update/ui/editor/customer-contact-fields.tsx +++ b/modules/customers/src/web/update/ui/editor/customer-contact-fields.tsx @@ -12,7 +12,6 @@ import { } from "@repo/shadcn-ui/components"; import { AtSignIcon, ChevronDown, GlobeIcon, PhoneIcon, SmartphoneIcon } from "lucide-react"; import { useState } from "react"; -import { useFormContext } from "react-hook-form"; import { useTranslation } from "../../../i18n"; @@ -23,7 +22,6 @@ interface CustomerContactFieldsProps { export const CustomerContactFields = ({ className, ...props }: CustomerContactFieldsProps) => { const { t } = useTranslation(); const [open, setOpen] = useState(true); - const { control } = useFormContext(); return (
@@ -37,7 +35,7 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi leftIcon={ } - name="email_primary" + name="primaryEmail" placeholder={t("form_fields.email_primary.placeholder")} required typePreset="email" @@ -53,7 +51,7 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi strokeWidth={1.5} /> } - name="mobile_primary" + name="primaryMobile" placeholder={t("form_fields.mobile_primary.placeholder")} typePreset="phone" /> @@ -65,7 +63,7 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi leftIcon={ } - name="phone_primary" + name="primaryPhone" placeholder={t("form_fields.phone_primary.placeholder")} typePreset="phone" /> @@ -79,7 +77,7 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi leftIcon={ } - name="email_secondary" + name="secondaryEmail" placeholder={t("form_fields.email_secondary.placeholder")} typePreset="email" /> @@ -94,7 +92,7 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi strokeWidth={1.5} /> } - name="mobile_secondary" + name="secondaryMobile" placeholder={t("form_fields.mobile_secondary.placeholder")} typePreset="phone" /> @@ -105,7 +103,7 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi leftIcon={ } - name="phone_secondary" + name="secondaryPhone" placeholder={t("form_fields.phone_secondary.placeholder")} typePreset="phone" /> diff --git a/modules/customers/src/web/update/ui/editor/customer-edit-form.tsx b/modules/customers/src/web/update/ui/editor/customer-edit-form.tsx index 8a87def9..5ae88c01 100644 --- a/modules/customers/src/web/update/ui/editor/customer-edit-form.tsx +++ b/modules/customers/src/web/update/ui/editor/customer-edit-form.tsx @@ -1,4 +1,3 @@ -import { FormDebug } from "@erp/core/components"; import { cn } from "@repo/shadcn-ui/lib/utils"; import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields"; @@ -16,8 +15,7 @@ type CustomerFormProps = { export const CustomerEditForm = ({ formId, onSubmit, className, focusRef }: CustomerFormProps) => { return ( - -
+
diff --git a/modules/customers/src/web/update/ui/editor/customer-taxes-multi-select.tsx b/modules/customers/src/web/update/ui/editor/customer-taxes-multi-select.tsx index 98ada56d..35146c25 100644 --- a/modules/customers/src/web/update/ui/editor/customer-taxes-multi-select.tsx +++ b/modules/customers/src/web/update/ui/editor/customer-taxes-multi-select.tsx @@ -46,7 +46,7 @@ export const CustomerTaxesMultiSelect = (props: CustomerTaxesMultiSelect) => { autoFilter={true} 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-[2px] font-medium bg-muted/50", + "hover:border-ring hover:ring-ring/50 hover:ring-2 font-medium bg-muted/50", className )} defaultValue={value} diff --git a/modules/customers/src/web/update/ui/pages/customer-update-page.tsx b/modules/customers/src/web/update/ui/pages/customer-update-page.tsx index 25c0e17d..e500f219 100644 --- a/modules/customers/src/web/update/ui/pages/customer-update-page.tsx +++ b/modules/customers/src/web/update/ui/pages/customer-update-page.tsx @@ -1,16 +1,18 @@ import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components"; -import { UnsavedChangesProvider, UpdateCommitButtonGroup, useUrlParamId } from "@erp/core/hooks"; +import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks"; import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components"; +import { FormProvider } from "react-hook-form"; import { useTranslation } from "../../../i18n"; -import { useCustomerUpdateController } from "../../controllers"; +import { useCustomerUpdatePageController } from "../../controllers"; import { CustomerEditorSkeleton } from "../components"; import { CustomerEditForm } from "../editor"; export const CustomerUpdatePage = () => { - const initialCustomerId = useUrlParamId(); const { t } = useTranslation(); + const { updateCtrl } = useCustomerUpdatePageController(); + const { form, formId, @@ -25,9 +27,9 @@ export const CustomerUpdatePage = () => { isUpdating, isUpdateError, updateError, + } = updateCtrl; - FormProvider, - } = useCustomerUpdateController(initialCustomerId, {}); + const isDirty = form.formState.isDirty; if (isLoading) { return ; @@ -66,7 +68,7 @@ export const CustomerUpdatePage = () => { ); return ( - + +): CustomerUpdatePatch => { + return pickFormDirtyValues(formData, dirtyFields) as CustomerUpdatePatch; +}; diff --git a/modules/customers/src/web/update/utils/index.ts b/modules/customers/src/web/update/utils/index.ts new file mode 100644 index 00000000..5ecc5358 --- /dev/null +++ b/modules/customers/src/web/update/utils/index.ts @@ -0,0 +1 @@ +export * from "./build-customer-update-patch";