From 50a19381ceed28334a68769fb329ef17cd42340b Mon Sep 17 00:00:00 2001 From: david Date: Tue, 21 Oct 2025 20:05:12 +0200 Subject: [PATCH] Clientes y Facturas de cliente --- apps/web/src/global.css | 1 + modules/core/package.json | 2 + .../src/web/components/form/form-debug.tsx | 16 +++-- .../core/src/web/components/page-header.tsx | 37 +++++----- modules/core/src/web/globals.css | 1 + .../web/pages/update/invoice-update-comp.tsx | 15 ++-- .../web/pages/update/invoice-update-form.tsx | 20 ++++-- .../customer-create-modal.tsx | 70 +++++++++++-------- .../customer-modal-selector-field.tsx | 50 ++++++++++--- .../customer-additional-config-fields.tsx | 11 ++- .../editor/customer-address-fields.tsx | 8 ++- .../editor/customer-basic-info-fields.tsx | 5 +- .../editor/customer-contact-fields.tsx | 8 ++- .../components/editor/customer-edit-form.tsx | 23 +++--- .../web/hooks/use-create-customer-mutation.ts | 2 +- .../web/pages/create/customer-create-page.tsx | 8 ++- .../create/use-customer-create-controller.ts | 29 +++++--- .../src/components/layout/app-content.tsx | 2 +- .../src/components/layout/app-header.tsx | 2 +- pnpm-lock.yaml | 3 + 20 files changed, 205 insertions(+), 108 deletions(-) create mode 100644 modules/core/src/web/globals.css diff --git a/apps/web/src/global.css b/apps/web/src/global.css index ec47efe9..a089b78b 100644 --- a/apps/web/src/global.css +++ b/apps/web/src/global.css @@ -1,5 +1,6 @@ @import "@repo/shadcn-ui/globals.css"; @import "@repo/rdx-ui/globals.css"; +@import "@erp/core/globals.css"; @import "@erp/customers/globals.css"; @import "@erp/customer-invoices/globals.css"; diff --git a/modules/core/package.json b/modules/core/package.json index 3e8c8817..67882b28 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -5,6 +5,7 @@ ".": "./src/common/index.ts", "./api": "./src/api/index.ts", "./client": "./src/web/manifest.ts", + "./globals.css": "./src/web/globals.css", "./components": "./src/web/components/index.ts", "./hooks": "./src/web/hooks/index.ts" }, @@ -13,6 +14,7 @@ "react": "^19.1.0" }, "devDependencies": { + "@hookform/devtools": "^4.4.0", "@types/axios": "^0.14.4", "@types/dinero.js": "^1.9.4", "@types/express": "^4.17.21", diff --git a/modules/core/src/web/components/form/form-debug.tsx b/modules/core/src/web/components/form/form-debug.tsx index 0e9daeaf..744c90f7 100644 --- a/modules/core/src/web/components/form/form-debug.tsx +++ b/modules/core/src/web/components/form/form-debug.tsx @@ -1,3 +1,4 @@ +import { DevTool } from '@hookform/devtools'; import { useState } from "react"; import { useFormContext } from "react-hook-form"; @@ -43,12 +44,15 @@ function DebugField({ label, oldValue, newValue }: { label?: string; oldValue: a } export const FormDebug = () => { - const { watch, formState } = useFormContext(); - const { isDirty, dirtyFields, defaultValues } = formState; - const currentValues = watch(); + const { control } = useFormContext(); + //const { watch, formState } = useFormContext(); + //const { isDirty, dirtyFields, defaultValues } = formState; + //const currentValues = watch(); - return ( -
+ return + + /*return ( +

¿Formulario modificado? {isDirty ? "Sí" : "No"}

@@ -70,5 +74,5 @@ export const FormDebug = () => { )}
- ); + );*/ }; diff --git a/modules/core/src/web/components/page-header.tsx b/modules/core/src/web/components/page-header.tsx index 814bc0b1..a268b2a1 100644 --- a/modules/core/src/web/components/page-header.tsx +++ b/modules/core/src/web/components/page-header.tsx @@ -15,30 +15,35 @@ interface PageHeaderProps { className?: string; } + export function PageHeader({ backIcon, title, description, rightSlot, className }: PageHeaderProps) { return ( -
+
{/* Lado izquierdo */} -
- {backIcon && ( - - )} +
+
+ {backIcon && ( + + )} -
-

{title}

- {description &&

{description}

} +
+

{title}

+ {description &&

{description}

} +
{/* Lado derecho parametrizable */} - {rightSlot && <>{rightSlot}} +
+ {rightSlot} +
); } diff --git a/modules/core/src/web/globals.css b/modules/core/src/web/globals.css new file mode 100644 index 00000000..2b014f0c --- /dev/null +++ b/modules/core/src/web/globals.css @@ -0,0 +1 @@ +@source "./components"; diff --git a/modules/customer-invoices/src/web/pages/update/invoice-update-comp.tsx b/modules/customer-invoices/src/web/pages/update/invoice-update-comp.tsx index 1aaf4413..d027b2f3 100644 --- a/modules/customer-invoices/src/web/pages/update/invoice-update-comp.tsx +++ b/modules/customer-invoices/src/web/pages/update/invoice-update-comp.tsx @@ -31,10 +31,11 @@ export const InvoiceUpdateComp = ({ }: InvoiceUpdateCompProps) => { const { t } = useTranslation(); const navigate = useNavigate(); - const { invoice_id } = useInvoiceContext(); // ahora disponible desde el inicio - const context = useInvoiceContext(); const formId = useId(); + const context = useInvoiceContext(); + const { invoice_id } = context; + const isPending = !invoiceData; const { @@ -54,7 +55,7 @@ export const InvoiceUpdateComp = ({ const form = useHookForm({ resolverSchema: InvoiceFormSchema, initialValues, - disabled: !invoiceData || isUpdating + disabled: !invoiceData || isUpdating, }); const handleSubmit = (formData: InvoiceFormData) => { @@ -89,7 +90,11 @@ export const InvoiceUpdateComp = ({ backIcon title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`} description={t("pages.edit.description")} - rightSlot={ + rightSlot={<> + navigate(-1)} /> - } + } /> diff --git a/modules/customer-invoices/src/web/pages/update/invoice-update-form.tsx b/modules/customer-invoices/src/web/pages/update/invoice-update-form.tsx index 94bb3652..9cbfdcae 100644 --- a/modules/customer-invoices/src/web/pages/update/invoice-update-form.tsx +++ b/modules/customer-invoices/src/web/pages/update/invoice-update-form.tsx @@ -21,21 +21,27 @@ export const InvoiceUpdateForm = ({ const form = useFormContext(); return ( -
-
-
+ ) => { + event.stopPropagation(); + form.handleSubmit(onSubmit, onError)(event) + }}> + + +
+
-
+
-
+
-
- +
+
diff --git a/modules/customers/src/web/components/customer-modal-selector/customer-create-modal.tsx b/modules/customers/src/web/components/customer-modal-selector/customer-create-modal.tsx index 0a13619c..21791a87 100644 --- a/modules/customers/src/web/components/customer-modal-selector/customer-create-modal.tsx +++ b/modules/customers/src/web/components/customer-modal-selector/customer-create-modal.tsx @@ -10,7 +10,7 @@ import { DialogTitle, } from "@repo/shadcn-ui/components"; import { Plus } from "lucide-react"; -import { useId } from 'react'; +import { useCallback, useId } from 'react'; import { useTranslation } from "../../i18n"; import { useCustomerCreateController } from '../../pages/create/use-customer-create-controller'; import { CustomerFormData } from "../../schemas"; @@ -32,6 +32,8 @@ export function CustomerCreateModal({ const { t } = useTranslation(); const formId = useId(); + const { requestConfirm } = useUnsavedChangesContext(); + const { form, isCreating, isCreateError, createError, handleSubmit, handleError, FormProvider @@ -39,31 +41,40 @@ export function CustomerCreateModal({ const { isDirty } = form.formState; - const guardClose = async (nextOpen: boolean) => { + const guardClose = useCallback(async (nextOpen: boolean) => { if (nextOpen) return onOpenChange(true); + if (isCreating) return; - const { requestConfirm } = useUnsavedChangesContext(); - const ok = await requestConfirm(); - if (ok) onOpenChange(false); - }; + + if (!isDirty) { + return onOpenChange(false); + } + + if (await requestConfirm()) { + return onOpenChange(false); + } + }, [requestConfirm, isCreating, onOpenChange, isDirty]); + + + const handleFormSubmit = (data: CustomerFormData) => handleSubmit(data /*, () => onOpenChange(false)*/); return ( - - - - - - {t("pages.create.title")} - - {t("pages.create.subtitle")} - -
+ + + + + {t("pages.create.title")} + + {t("pages.create.subtitle")} + +
+ handleSubmit(data, () => onOpenChange(false))} + onSubmit={handleFormSubmit} onError={handleError} className="max-w-none" /> @@ -73,19 +84,20 @@ export function CustomerCreateModal({ {(createError as Error)?.message}

)} -
+ +
+ + + + + +
+
- - - - - - -
); } \ No newline at end of file diff --git a/modules/customers/src/web/components/customer-modal-selector/customer-modal-selector-field.tsx b/modules/customers/src/web/components/customer-modal-selector/customer-modal-selector-field.tsx index e024582f..78828f4a 100644 --- a/modules/customers/src/web/components/customer-modal-selector/customer-modal-selector-field.tsx +++ b/modules/customers/src/web/components/customer-modal-selector/customer-modal-selector-field.tsx @@ -1,43 +1,73 @@ -import { FormField, FormItem } from "@repo/shadcn-ui/components"; +import { Field, FieldLabel } from "@repo/shadcn-ui/components"; -import { Control, FieldPath, FieldValues } from "react-hook-form"; +import { cn } from '@repo/shadcn-ui/lib/utils'; +import { Control, Controller, FieldPath, FieldValues } from "react-hook-form"; +import { CustomerSummary } from '../../schemas'; import { CustomerModalSelector } from "./customer-modal-selector"; type CustomerModalSelectorFieldProps = { control: Control; name: FieldPath; + + label?: string; + description?: string; + + orientation?: "vertical" | "horizontal" | "responsive", + disabled?: boolean; + required?: boolean; readOnly?: boolean; className?: string; + initiaCustomer?: unknown; }; export function CustomerModalSelectorField({ control, name, - disabled = false, // Solo lectura y sin botones - readOnly = false, // Solo se puede ver la ficha del cliente + + label, + description, + + orientation = 'vertical', + + + disabled = false, + required = false, + readOnly = false, className, + initiaCustomer = {}, }: CustomerModalSelectorFieldProps) { const isDisabled = disabled; const isReadOnly = readOnly && !disabled; return ( - { + render={({ field, fieldState }) => { const { name, value, onChange, onBlur, ref } = field; + return ( - + + {label && ( + + {label} + + )} - + ); }} /> ); -} +} \ No newline at end of file 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 e186f566..a9a5b5d9 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 @@ -6,12 +6,19 @@ import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants"; import { useTranslation } from "../../i18n"; import { CustomerFormData } from "../../schemas"; -export const CustomerAdditionalConfigFields = () => { +interface CustomerAdditionalConfigFieldsProps { + className?: string; +} + + +export const CustomerAdditionalConfigFields = ({ + className, ...props +}: CustomerAdditionalConfigFieldsProps) => { const { t } = useTranslation(); const { control } = useFormContext(); return ( -
+
{t("form_groups.preferences.title")} {t("form_groups.preferences.description")} 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 4cc9ccf3..8be4d7c3 100644 --- a/modules/customers/src/web/components/editor/customer-address-fields.tsx +++ b/modules/customers/src/web/components/editor/customer-address-fields.tsx @@ -8,12 +8,16 @@ import { COUNTRY_OPTIONS } from "../../constants"; import { useTranslation } from "../../i18n"; import { CustomerFormData } from "../../schemas"; -export const CustomerAddressFields = () => { +interface CustomerAddressFieldsProps { + className?: string; +} + +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/components/editor/customer-basic-info-fields.tsx b/modules/customers/src/web/components/editor/customer-basic-info-fields.tsx index 88d9d1c2..453b3420 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 @@ -19,9 +19,10 @@ import { CustomerFormData } from "../../schemas"; interface CustomerBasicInfoFieldsProps { focusRef?: React.RefObject; + className?: string; } -export const CustomerBasicInfoFields = ({ focusRef }: CustomerBasicInfoFieldsProps) => { +export const CustomerBasicInfoFields = ({ focusRef, className, ...props }: CustomerBasicInfoFieldsProps) => { const { t } = useTranslation(); const { control } = useFormContext(); @@ -32,7 +33,7 @@ export const CustomerBasicInfoFields = ({ focusRef }: CustomerBasicInfoFieldsPro return ( -
+
{t("form_groups.basic_info.title")} {t("form_groups.basic_info.description")} diff --git a/modules/customers/src/web/components/editor/customer-contact-fields.tsx b/modules/customers/src/web/components/editor/customer-contact-fields.tsx index 9b43619e..08420764 100644 --- a/modules/customers/src/web/components/editor/customer-contact-fields.tsx +++ b/modules/customers/src/web/components/editor/customer-contact-fields.tsx @@ -12,13 +12,17 @@ import { useState } from "react"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "../../i18n"; -export const CustomerContactFields = () => { +interface CustomerContactFieldsProps { + className?: string; +} + +export const CustomerContactFields = ({ className, ...props }: CustomerContactFieldsProps) => { const { t } = useTranslation(); const [open, setOpen] = useState(true); const { control } = useFormContext(); return ( -
+
{t("form_groups.contact_info.title")} {t("form_groups.contact_info.description")} 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 a932ee0f..d7f08a78 100644 --- a/modules/customers/src/web/components/editor/customer-edit-form.tsx +++ b/modules/customers/src/web/components/editor/customer-edit-form.tsx @@ -20,19 +20,16 @@ export const CustomerEditForm = ({ formId, onSubmit, onError, className, focusRe const form = useFormContext(); return ( -
-
-
-
- -
-
- - - - -
-
+ ) => { + event.stopPropagation(); + form.handleSubmit(onSubmit, onError)(event) + }}> + +
+ + + +
); 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 b9e55dfd..6826f55c 100644 --- a/modules/customers/src/web/hooks/use-create-customer-mutation.ts +++ b/modules/customers/src/web/hooks/use-create-customer-mutation.ts @@ -29,7 +29,7 @@ export function useCreateCustomer() { return useMutation({ mutationKey: CUSTOMER_CREATE_KEY, - mutationFn: async (data) => { + mutationFn: async ({ data }, context) => { const id = UniqueID.generateNewID().toString(); const payload = { ...data, id }; diff --git a/modules/customers/src/web/pages/create/customer-create-page.tsx b/modules/customers/src/web/pages/create/customer-create-page.tsx index 9f67e379..a245a5d4 100644 --- a/modules/customers/src/web/pages/create/customer-create-page.tsx +++ b/modules/customers/src/web/pages/create/customer-create-page.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom"; import { PageHeader } from '@erp/core/components'; import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks"; +import { useId } from 'react'; import { CustomerEditForm, ErrorAlert } from "../../components"; import { useTranslation } from "../../i18n"; import { useCustomerCreateController } from './use-customer-create-controller'; @@ -10,6 +11,7 @@ import { useCustomerCreateController } from './use-customer-create-controller'; export const CustomerCreatePage = () => { const { t } = useTranslation(); const navigate = useNavigate(); + const formId = useId(); const { form, isCreating, isCreateError, createError, @@ -37,7 +39,7 @@ export const CustomerCreatePage = () => { disabled: isCreating, }} submit={{ - formId: "customer-create-form", + formId: formId, disabled: isCreating, }} onBack={() => handleBack()} @@ -59,14 +61,14 @@ export const CustomerCreatePage = () => { handleSubmit(data, ({ id }) => navigate("/customers/list", { state: { customerId: id, isNew: true }, replace: true }) ) } onError={handleError} - className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto" + /> diff --git a/modules/customers/src/web/pages/create/use-customer-create-controller.ts b/modules/customers/src/web/pages/create/use-customer-create-controller.ts index 09859d5a..e264bf3a 100644 --- a/modules/customers/src/web/pages/create/use-customer-create-controller.ts +++ b/modules/customers/src/web/pages/create/use-customer-create-controller.ts @@ -1,12 +1,19 @@ import { useHookForm } from "@erp/core/hooks"; import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers"; +import { useId } from "react"; import { FieldErrors, FormProvider } from "react-hook-form"; import { useCreateCustomer } from "../../hooks"; import { useTranslation } from "../../i18n"; import { CustomerFormData, CustomerFormSchema, defaultCustomerFormData } from "../../schemas"; -export const useCustomerCreateController = () => { +export interface UseCustomerCreateControllerOptions { + onCreated?(created: CustomerFormData): void; // navegación, cierre modal, etc. + successToasts?: boolean; // permite desactivar toasts si el host los gestiona +} + +export const useCustomerCreateController = (options?: UseCustomerCreateControllerOptions) => { const { t } = useTranslation(); + const formId = useId(); // id único por instancia // 1) Estado de creación (mutación) const { @@ -23,19 +30,24 @@ export const useCustomerCreateController = () => { disabled: isCreating, }); - const handleSubmit = (formData: CustomerFormData, onSuccess?: (data: { id: string }) => void) => { + const handleSubmit = (formData: CustomerFormData) => { console.log(formData); mutate( - { data: formData }, + { + data: formData, + }, { onSuccess(data) { form.reset(defaultCustomerFormData); - showSuccessToast( - t("pages.create.successTitle", "Cliente creado"), - t("pages.create.successMsg", "Se ha creado correctamente.") - ); - onSuccess?.({ id: data.id }); + if (options?.successToasts !== false) { + showSuccessToast( + t("pages.create.successTitle", "Cliente creado"), + t("pages.create.successMsg", "Se ha creado correctamente.") + ); + } + options?.onCreated?.(data); }, + onError(err: unknown) { console.log("No se pudo crear el cliente."); const msg = @@ -63,6 +75,7 @@ export const useCustomerCreateController = () => { return { form, + formId, isCreating, isCreateError, createError, diff --git a/packages/rdx-ui/src/components/layout/app-content.tsx b/packages/rdx-ui/src/components/layout/app-content.tsx index cda1356c..ff0f5098 100644 --- a/packages/rdx-ui/src/components/layout/app-content.tsx +++ b/packages/rdx-ui/src/components/layout/app-content.tsx @@ -9,7 +9,7 @@ export const AppContent = ({ return (
) => { return ( -
+
{children}
); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 378b25cc..e3c9a69e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -429,6 +429,9 @@ importers: specifier: ^4.1.11 version: 4.1.12 devDependencies: + '@hookform/devtools': + specifier: ^4.4.0 + version: 4.4.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/axios': specifier: ^0.14.4 version: 0.14.4