diff --git a/modules/customers/src/web/components/editor/customer-additional-config-fields.tsx b/modules/customers/src/web/components/editor/customer-additional-config-fields.tsx index a39c4b65..38015234 100644 --- a/modules/customers/src/web/components/editor/customer-additional-config-fields.tsx +++ b/modules/customers/src/web/components/editor/customer-additional-config-fields.tsx @@ -9,11 +9,11 @@ import { import { useFormContext } from "react-hook-form"; import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants"; import { useTranslation } from "../../i18n"; -import { CustomerFormData } from "../../schemas"; +import { CreateCustomerFormData } from "../../schemas"; export const CustomerAdditionalConfigFields = () => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { control } = useFormContext(); return ( diff --git a/modules/customers/src/web/components/editor/customer-address-fields.tsx b/modules/customers/src/web/components/editor/customer-address-fields.tsx index 6a0f9e16..30619d5b 100644 --- a/modules/customers/src/web/components/editor/customer-address-fields.tsx +++ b/modules/customers/src/web/components/editor/customer-address-fields.tsx @@ -9,11 +9,11 @@ import { import { useFormContext } from "react-hook-form"; import { COUNTRY_OPTIONS } from "../../constants"; import { useTranslation } from "../../i18n"; -import { CustomerFormData } from "../../schemas"; +import { CreateCustomerFormData } from "../../schemas"; export const CustomerAddressFields = () => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { control } = useFormContext(); return ( diff --git a/modules/customers/src/web/components/editor/customer-basic-info-fields.tsx b/modules/customers/src/web/components/editor/customer-basic-info-fields.tsx index 00375264..b1e0b533 100644 --- a/modules/customers/src/web/components/editor/customer-basic-info-fields.tsx +++ b/modules/customers/src/web/components/editor/customer-basic-info-fields.tsx @@ -15,11 +15,11 @@ import { } from "@repo/shadcn-ui/components"; import { useFormContext, useWatch } from "react-hook-form"; import { useTranslation } from "../../i18n"; -import { CustomerFormData } from "../../schemas"; +import { CreateCustomerFormData } from "../../schemas"; export const CustomerBasicInfoFields = () => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { control } = useFormContext(); const isCompany = useWatch({ control, diff --git a/modules/customers/src/web/components/editor/customer-edit-form.tsx b/modules/customers/src/web/components/editor/customer-edit-form.tsx index ff59335f..d81fcdd6 100644 --- a/modules/customers/src/web/components/editor/customer-edit-form.tsx +++ b/modules/customers/src/web/components/editor/customer-edit-form.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { FieldErrors, FormProvider, useForm } from "react-hook-form"; import { useEffect } from "react"; -import { CreateCustomerFormSchema, CustomerFormData } from "../../schemas"; +import { CreateCustomerFormData, CreateCustomerFormSchema } from "../../schemas"; import { FormDebug } from "../form-debug"; import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields"; import { CustomerAddressFields } from "./customer-address-fields"; @@ -11,9 +11,9 @@ import { CustomerContactFields } from "./customer-contact-fields"; interface CustomerFormProps { formId: string; - initialValues: CustomerFormData; - onSubmit: (data: CustomerFormData) => void; - onError: (errors: FieldErrors) => void; + initialValues: CreateCustomerFormData; + onSubmit: (data: CreateCustomerFormData) => void; + onError: (errors: FieldErrors) => void; disabled?: boolean; onDirtyChange: (isDirty: boolean) => void; } @@ -26,7 +26,7 @@ export function CustomerEditForm({ disabled, onDirtyChange, }: CustomerFormProps) { - const form = useForm({ + const form = useForm({ resolver: zodResolver(CreateCustomerFormSchema), defaultValues: initialValues, disabled, diff --git a/modules/customers/src/web/hooks/use-create-customer-mutation.ts b/modules/customers/src/web/hooks/use-create-customer-mutation.ts index 6bab5c33..78ac3583 100644 --- a/modules/customers/src/web/hooks/use-create-customer-mutation.ts +++ b/modules/customers/src/web/hooks/use-create-customer-mutation.ts @@ -2,11 +2,11 @@ import { useDataSource } from "@erp/core/hooks"; import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { CreateCustomerRequestSchema, CustomerCreationResponseDTO } from "../../common"; -import { CustomerFormData } from "../schemas"; +import { CreateCustomerFormData } from "../schemas"; import { CUSTOMERS_LIST_KEY } from "./use-update-customer-mutation"; type CreateCustomerPayload = { - data: CustomerFormData; + data: CreateCustomerFormData; }; export function useCreateCustomerMutation() { @@ -34,8 +34,6 @@ export function useCreateCustomerMutation() { message: err.message, })); - console.debug(validationErrors); - throw new ValidationErrorCollection("Validation failed", validationErrors); } diff --git a/modules/customers/src/web/hooks/use-update-customer-mutation.ts b/modules/customers/src/web/hooks/use-update-customer-mutation.ts index bb25774c..fee75a95 100644 --- a/modules/customers/src/web/hooks/use-update-customer-mutation.ts +++ b/modules/customers/src/web/hooks/use-update-customer-mutation.ts @@ -1,20 +1,23 @@ import { useDataSource } from "@erp/core/hooks"; +import { ValidationErrorCollection } from "@repo/rdx-ddd"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { CustomerData, CustomerUpdateData } from "../schemas"; +import { UpdateCustomerByIdRequestDTO, UpdateCustomerByIdRequestSchema } from "../../common"; +import { CreateCustomerFormData } from "../schemas"; import { CUSTOMER_QUERY_KEY } from "./use-customer-query"; export const CUSTOMERS_LIST_KEY = ["customers"] as const; type UpdateCustomerPayload = { id: string; - data: CustomerUpdateData; + data: CreateCustomerFormData; }; export function useUpdateCustomerMutation() { const queryClient = useQueryClient(); const dataSource = useDataSource(); + const schema = UpdateCustomerByIdRequestSchema; - return useMutation({ + return useMutation({ mutationKey: ["customer:update"], //, customerId], mutationFn: async (payload) => { @@ -23,14 +26,28 @@ export function useUpdateCustomerMutation() { throw new Error("customerId is required"); } + const result = schema.safeParse(data); + if (!result.success) { + // Construye errores detallados + const validationErrors = result.error.issues.map((err) => ({ + field: err.path.join("."), + message: err.message, + })); + + throw new ValidationErrorCollection("Validation failed", validationErrors); + } + const updated = await dataSource.updateOne("customers", customerId, data); - return updated as CustomerData; + return updated as UpdateCustomerByIdRequestDTO; }, onSuccess: (updated, variables) => { const { id: customerId } = variables; // Refresca inmediatamente el detalle - queryClient.setQueryData(CUSTOMER_QUERY_KEY(customerId), updated); + queryClient.setQueryData( + CUSTOMER_QUERY_KEY(customerId), + updated + ); // Otra opción es invalidar el detalle para forzar refetch: // queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) }); diff --git a/modules/customers/src/web/pages/create/customer-create.tsx b/modules/customers/src/web/pages/create/customer-create.tsx index fa6577f9..86d43fe7 100644 --- a/modules/customers/src/web/pages/create/customer-create.tsx +++ b/modules/customers/src/web/pages/create/customer-create.tsx @@ -8,14 +8,14 @@ import { FieldErrors } from "react-hook-form"; import { CustomerEditForm, ErrorAlert } from "../../components"; import { useCreateCustomerMutation } from "../../hooks"; import { useTranslation } from "../../i18n"; -import { CustomerFormData, defaultCustomerFormData } from "../../schemas"; +import { CreateCustomerFormData, defaultCustomerFormData } from "../../schemas"; export const CustomerCreate = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [isDirty, setIsDirty] = useState(false); - // 2) Estado de creación (mutación) + // 1) Estado de creación (mutación) const { mutate, isPending: isCreating, @@ -23,8 +23,8 @@ export const CustomerCreate = () => { error: createError, } = useCreateCustomerMutation(); - // 3) Submit con navegación condicionada por éxito - const handleSubmit = (formData: CustomerFormData) => { + // 2) Submit con navegación condicionada por éxito + const handleSubmit = (formData: CreateCustomerFormData) => { mutate( { data: formData }, { @@ -48,7 +48,7 @@ export const CustomerCreate = () => { ); }; - const handleError = (errors: FieldErrors) => { + const handleError = (errors: FieldErrors) => { console.error("Errores en el formulario:", errors); // Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario }; diff --git a/modules/customers/src/web/pages/list.tsx b/modules/customers/src/web/pages/list.tsx index e3b99726..d3ec1d9f 100644 --- a/modules/customers/src/web/pages/list.tsx +++ b/modules/customers/src/web/pages/list.tsx @@ -1,7 +1,7 @@ import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components"; import { Button } from "@repo/shadcn-ui/components"; import { PlusIcon } from "lucide-react"; -import { useNavigate } from "react-router-dom"; +import { Outlet, useNavigate } from "react-router-dom"; import { CustomersListGrid } from "../components"; import { useTranslation } from "../i18n"; @@ -28,6 +28,7 @@ export const CustomersList = () => {
+ ); diff --git a/modules/customers/src/web/pages/update/customer-update.tsx b/modules/customers/src/web/pages/update/customer-update.tsx index 0cc03ea0..da84fecc 100644 --- a/modules/customers/src/web/pages/update/customer-update.tsx +++ b/modules/customers/src/web/pages/update/customer-update.tsx @@ -1,9 +1,9 @@ -import { AppBreadcrumb, AppContent, BackHistoryButton, ButtonGroup } from "@repo/rdx-ui/components"; -import { Button } from "@repo/shadcn-ui/components"; +import { AppBreadcrumb, AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; import { useNavigate } from "react-router-dom"; -import { useUrlParamId } from "@erp/core/hooks"; +import { FormCommitButtonGroup, UnsavedChangesProvider, useUrlParamId } from "@erp/core/hooks"; import { showErrorToast, showSuccessToast } from "@repo/shadcn-ui/lib/utils"; +import { useState } from "react"; import { FieldErrors } from "react-hook-form"; import { CustomerEditForm, @@ -13,12 +13,13 @@ import { } from "../../components"; import { useCustomerQuery, useUpdateCustomerMutation } from "../../hooks"; import { useTranslation } from "../../i18n"; -import { CustomerFormData } from "../../schemas"; +import { CreateCustomerFormData } from "../../schemas"; export const CustomerUpdate = () => { const customerId = useUrlParamId(); const { t } = useTranslation(); const navigate = useNavigate(); + const [isDirty, setIsDirty] = useState(false); // 1) Estado de carga del cliente (query) const { @@ -30,30 +31,38 @@ export const CustomerUpdate = () => { // 2) Estado de actualización (mutación) const { - mutateAsync, + mutate, isPending: isUpdating, isError: isUpdateError, error: updateError, } = useUpdateCustomerMutation(); // 3) Submit con navegación condicionada por éxito - const handleSubmit = async (formData: CustomerFormData) => { - console.log(formData); - try { - const result = await mutateAsync({ id: customerId!, data: formData }); - console.log(result); + const handleSubmit = (formData: CreateCustomerFormData) => { + mutate( + { id: customerId!, data: formData }, + { + onSuccess(data) { + setIsDirty(false); + showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg")); - if (result) { - showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg")); - navigate("/customers/list", { relative: "path" }); + // El timeout es para que a React le dé tiempo a procesar + // el cambio de estado de isDirty / setIsDirty. + setTimeout(() => { + navigate("/customers/list", { + state: { customerId: data.id, isNew: true }, + replace: true, + }); + }, 0); + }, + onError(error) { + showErrorToast(t("pages.update.errorTitle"), error.message); + }, } - } catch (e) { - showErrorToast(t("pages.update.errorTitle"), (e as Error).message); - } finally { - } + ); }; - const handleError = (errors: FieldErrors) => { + const handleError = (errors: FieldErrors) => { console.error("Errores en el formulario:", errors); // Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario }; @@ -100,60 +109,48 @@ export const CustomerUpdate = () => { <> -
-
-

- {t("pages.update.title")} -

-

- {t("pages.update.description")} -

-
- - + submit={{ + formId: "customer-create-form", + disabled: isUpdating, + isLoading: isUpdating, + }} + /> +
+ {/* Alerta de error de actualización (si ha fallado el último intento) */} + {isUpdateError && ( + + )} - - - - {/* Alerta de error de actualización (si ha fallado el último intento) */} - {isUpdateError && ( - - )} - -
- -
+
+ +
+
); diff --git a/modules/customers/src/web/schemas/customer.form.schema.ts b/modules/customers/src/web/schemas/customer-create.form.schema.ts similarity index 93% rename from modules/customers/src/web/schemas/customer.form.schema.ts rename to modules/customers/src/web/schemas/customer-create.form.schema.ts index 1a597249..3e9eda7c 100644 --- a/modules/customers/src/web/schemas/customer.form.schema.ts +++ b/modules/customers/src/web/schemas/customer-create.form.schema.ts @@ -55,9 +55,9 @@ export const CreateCustomerFormSchema = z.object({ .default("EUR"), }); -export type CustomerFormData = z.infer; +export type CreateCustomerFormData = z.infer; -export const defaultCustomerFormData: CustomerFormData = { +export const defaultCustomerFormData: CreateCustomerFormData = { reference: "", is_company: "true", diff --git a/modules/customers/src/web/schemas/customer-update.form.schema.ts b/modules/customers/src/web/schemas/customer-update.form.schema.ts new file mode 100644 index 00000000..e9a79629 --- /dev/null +++ b/modules/customers/src/web/schemas/customer-update.form.schema.ts @@ -0,0 +1,15 @@ +import * as z from "zod/v4"; +import { CreateCustomerFormSchema } from "./customer-create.form.schema"; + +export const UpdateCustomerFormSchema = CreateCustomerFormSchema.extend({ + is_company: CreateCustomerFormSchema.shape.is_company.optional(), + name: CreateCustomerFormSchema.shape.name.optional(), + default_taxes: z.array(z.string()).optional(), + + country: CreateCustomerFormSchema.shape.country.optional(), + + language_code: CreateCustomerFormSchema.shape.language_code.optional(), + currency_code: CreateCustomerFormSchema.shape.currency_code.optional(), +}); + +export type UpdateCustomerFormData = z.infer; diff --git a/modules/customers/src/web/schemas/index.ts b/modules/customers/src/web/schemas/index.ts index 6f1fb312..b88d1f32 100644 --- a/modules/customers/src/web/schemas/index.ts +++ b/modules/customers/src/web/schemas/index.ts @@ -1,2 +1,2 @@ +export * from "./customer-create.form.schema"; export * from "./customer.api.schema"; -export * from "./customer.form.schema"; diff --git a/packages/rdx-ui/src/components/full-screen-modal.tsx b/packages/rdx-ui/src/components/full-screen-modal.tsx new file mode 100644 index 00000000..1424a731 --- /dev/null +++ b/packages/rdx-ui/src/components/full-screen-modal.tsx @@ -0,0 +1,47 @@ +import type React from "react"; + +import { Button, Dialog, DialogContent } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { X } from "lucide-react"; + +interface FullscreenModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: React.ReactNode; + className?: string; +} + +export const FullscreenModal = ({ + isOpen, + onClose, + title, + children, + className, +}: FullscreenModalProps) => { + return ( + + + {/* Header fijo */} +
+

{title}

+ +
+ + {/* Contenido scrolleable */} +
{children}
+
+
+ ); +}; diff --git a/packages/rdx-ui/src/components/index.tsx b/packages/rdx-ui/src/components/index.tsx index 8958901d..2cd514b7 100644 --- a/packages/rdx-ui/src/components/index.tsx +++ b/packages/rdx-ui/src/components/index.tsx @@ -4,6 +4,7 @@ export * from "./datatable/index.tsx"; export * from "./dynamics-tabs.tsx"; export * from "./error-overlay.tsx"; export * from "./form/index.tsx"; +export * from "./full-screen-modal.tsx"; export * from "./grid/index.ts"; export * from "./layout/index.tsx"; export * from "./loading-overlay/index.tsx"; diff --git a/packages/rdx-ui/src/components/layout/app-layout.tsx b/packages/rdx-ui/src/components/layout/app-layout.tsx index 8c0211e6..5f36be74 100644 --- a/packages/rdx-ui/src/components/layout/app-layout.tsx +++ b/packages/rdx-ui/src/components/layout/app-layout.tsx @@ -2,7 +2,7 @@ import { SidebarInset, SidebarProvider } from "@repo/shadcn-ui/components"; import { Outlet } from "react-router"; import { AppSidebar } from "./app-sidebar.tsx"; -export const AppLayout: React.FC = () => { +export const AppLayout = () => { return (