Clientes y Facturas de cliente
This commit is contained in:
parent
ab12f5815e
commit
50a19381ce
@ -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";
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 (
|
||||
<div className="p-4 border rounded bg-red-50 mb-6">
|
||||
return <DevTool control={control} placement="top-right" />
|
||||
|
||||
/*return (
|
||||
<div className="absolute right-4 bottom-4 z-50 p-4 border rounded bg-red-50">
|
||||
<p>
|
||||
<strong>¿Formulario modificado?</strong> {isDirty ? "Sí" : "No"}
|
||||
</p>
|
||||
@ -70,5 +74,5 @@ export const FormDebug = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);*/
|
||||
};
|
||||
|
||||
@ -15,30 +15,35 @@ interface PageHeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
export function PageHeader({ backIcon, title, description, rightSlot, className }: PageHeaderProps) {
|
||||
return (
|
||||
<div className={cn("pt-4 pb-6 bg-background flex items-center justify-between", className)}>
|
||||
<div className={cn("pt-6 pb-6 lg:flex lg:items-center lg:justify-between", className)}>
|
||||
{/* Lado izquierdo */}
|
||||
<div className='flex items-center gap-4'>
|
||||
{backIcon && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='cursor-pointer'
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
<ChevronLeftIcon className='size-5' />
|
||||
</Button>
|
||||
)}
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-start gap-4'>
|
||||
{backIcon && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='cursor-pointer'
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
<ChevronLeftIcon className='size-5' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h2 className='text-2xl font-semibold text-foreground'>{title}</h2>
|
||||
{description && <p className='text-base text-muted-foreground'>{description}</p>}
|
||||
<div>
|
||||
<h2 className='h-8 text-xl font-semibold text-foreground sm:truncate sm:tracking-tight'>{title}</h2>
|
||||
{description && <p className='text-sm text-muted-foreground'>{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lado derecho parametrizable */}
|
||||
{rightSlot && <>{rightSlot}</>}
|
||||
<div className="mt-4 flex lg:mt-0 lg:ml-4">
|
||||
{rightSlot}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1
modules/core/src/web/globals.css
Normal file
1
modules/core/src/web/globals.css
Normal file
@ -0,0 +1 @@
|
||||
@source "./components";
|
||||
@ -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<InvoiceFormData>({
|
||||
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={<>
|
||||
<button type="submit" form={formId} onClick={(e) => {
|
||||
e.preventDefault(); const submit = form.handleSubmit(handleSubmit, handleError);
|
||||
void submit(e)
|
||||
}}>Enviar</button>
|
||||
<UpdateCommitButtonGroup
|
||||
isLoading={isPending}
|
||||
|
||||
@ -97,7 +102,7 @@ export const InvoiceUpdateComp = ({
|
||||
cancel={{ formId, to: "/customer-invoices/list" }}
|
||||
onBack={() => navigate(-1)}
|
||||
/>
|
||||
}
|
||||
</>}
|
||||
/>
|
||||
</AppHeader>
|
||||
|
||||
|
||||
@ -21,21 +21,27 @@ export const InvoiceUpdateForm = ({
|
||||
const form = useFormContext<InvoiceFormData>();
|
||||
|
||||
return (
|
||||
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
||||
<section className={cn("p-6 space-y-6", className)}>
|
||||
<div className="w-full p-6 bg-transparent grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<form noValidate id={formId} onSubmit={
|
||||
(event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.stopPropagation();
|
||||
form.handleSubmit(onSubmit, onError)(event)
|
||||
}}>
|
||||
<FormDebug />
|
||||
|
||||
<section className={cn("space-y-6 p-6", className)}>
|
||||
<div className="w-full bg-transparent grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<InvoiceRecipient className="flex flex-col" />
|
||||
<InvoiceBasicInfoFields className="flex flex-col lg:col-span-2" />
|
||||
</div>
|
||||
|
||||
<div className='w-full gap-6 px-6'>
|
||||
<div className='w-full'>
|
||||
<InvoiceItems />
|
||||
</div>
|
||||
<div className="w-full p-6 grid grid-cols-1 lg:grid-cols-2">
|
||||
<div className="w-full grid grid-cols-1 lg:grid-cols-2">
|
||||
<InvoiceTotals className='lg:col-start-2' />
|
||||
</div>
|
||||
<div className="w-full p-6">
|
||||
<FormDebug />
|
||||
<div className="w-full">
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
@ -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 (
|
||||
<UnsavedChangesProvider isDirty={isDirty}>
|
||||
<FormProvider {...form}>
|
||||
<Dialog open={open} onOpenChange={guardClose}>
|
||||
<DialogContent className="bg-card border-border p-0 max-w-[calc(100vw-2rem)] sm:max-w-[min(100vw-3rem,1280px)] h-[calc(100dvh-2rem)]">
|
||||
<DialogHeader className="px-6 pt-6 pb-4 border-b">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Plus className="size-5" /> {t("pages.create.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{t("pages.create.subtitle")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 py-4 overflow-y-auto h:[calc(100%-8rem)]">
|
||||
<Dialog open={open} onOpenChange={guardClose}>
|
||||
<DialogContent className="bg-card border-border p-0 max-w-[calc(100vw-2rem)] sm:max-w-[min(100vw-3rem,1280px)] h-[calc(100dvh-2rem)]">
|
||||
<DialogHeader className="px-6 pt-6 pb-4 border-b">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Plus className="size-5" /> {t("pages.create.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{t("pages.create.subtitle")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 py-4 overflow-y-auto h:[calc(100%-8rem)]">
|
||||
<FormProvider {...form}>
|
||||
<CustomerEditForm
|
||||
formId={formId}
|
||||
onSubmit={(data: CustomerFormData) => handleSubmit(data, () => onOpenChange(false))}
|
||||
onSubmit={handleFormSubmit}
|
||||
onError={handleError}
|
||||
className="max-w-none"
|
||||
/>
|
||||
@ -73,19 +84,20 @@ export function CustomerCreateModal({
|
||||
{(createError as Error)?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FormProvider>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="px-6 py-4 border-t bg-card">
|
||||
<Button type="button" form={formId} variant="outline" className='cursor-pointer' onClick={() => guardClose(false)} disabled={isCreating}>
|
||||
{t('common.cancel', "Cancelar")}
|
||||
</Button>
|
||||
<Button type="submit" form={formId} disabled={isCreating} className='cursor-pointer'>
|
||||
{isCreating ? <span aria-live="polite">{t('common.saving', "Guardando")}</span> : <span>{t('common.save', "Guardar")}</span>}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DialogFooter className="px-6 py-4 border-t bg-card">
|
||||
<Button type="button" form={formId} variant="outline" className='cursor-pointer' onClick={() => guardClose(false)} disabled={isCreating}>
|
||||
{t('common.cancel', "Cancelar")}
|
||||
</Button>
|
||||
<Button type="submit" form={formId} disabled={isCreating} className='cursor-pointer'>
|
||||
{isCreating ? <span aria-live="polite">{t('common.saving', "Guardando")}</span> : <span>{t('common.save', "Guardar")}</span>}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</FormProvider>
|
||||
</UnsavedChangesProvider>
|
||||
);
|
||||
}
|
||||
@ -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<TFormValues extends FieldValues> = {
|
||||
control: Control<TFormValues>;
|
||||
name: FieldPath<TFormValues>;
|
||||
|
||||
label?: string;
|
||||
description?: string;
|
||||
|
||||
orientation?: "vertical" | "horizontal" | "responsive",
|
||||
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
className?: string;
|
||||
initiaCustomer?: unknown;
|
||||
};
|
||||
|
||||
export function CustomerModalSelectorField<TFormValues extends FieldValues>({
|
||||
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<TFormValues>) {
|
||||
const isDisabled = disabled;
|
||||
const isReadOnly = readOnly && !disabled;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => {
|
||||
render={({ field, fieldState }) => {
|
||||
const { name, value, onChange, onBlur, ref } = field;
|
||||
|
||||
return (
|
||||
<FormItem className={className}>
|
||||
<Field
|
||||
data-invalid={fieldState.invalid}
|
||||
orientation={orientation}
|
||||
className={cn("gap-1", className)}
|
||||
>
|
||||
{label && (
|
||||
<FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
)}
|
||||
<CustomerModalSelector
|
||||
value={value as string | undefined}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
disabled={isDisabled}
|
||||
readOnly={isReadOnly}
|
||||
initialCustomer={initiaCustomer as CustomerSummary}
|
||||
/>
|
||||
</FormItem>
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<CustomerFormData>();
|
||||
|
||||
return (
|
||||
<FieldSet>
|
||||
<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'>
|
||||
|
||||
@ -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<CustomerFormData>();
|
||||
|
||||
return (
|
||||
<FieldSet>
|
||||
<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'>
|
||||
|
||||
@ -19,9 +19,10 @@ import { CustomerFormData } from "../../schemas";
|
||||
|
||||
interface CustomerBasicInfoFieldsProps {
|
||||
focusRef?: React.RefObject<HTMLInputElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CustomerBasicInfoFields = ({ focusRef }: CustomerBasicInfoFieldsProps) => {
|
||||
export const CustomerBasicInfoFields = ({ focusRef, className, ...props }: CustomerBasicInfoFieldsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<CustomerFormData>();
|
||||
|
||||
@ -32,7 +33,7 @@ export const CustomerBasicInfoFields = ({ focusRef }: CustomerBasicInfoFieldsPro
|
||||
|
||||
|
||||
return (
|
||||
<FieldSet>
|
||||
<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'>
|
||||
|
||||
@ -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 (
|
||||
<FieldSet>
|
||||
<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'>
|
||||
|
||||
@ -20,19 +20,16 @@ export const CustomerEditForm = ({ formId, onSubmit, onError, className, focusRe
|
||||
const form = useFormContext<CustomerFormData>();
|
||||
|
||||
return (
|
||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
||||
<section className={cn("p-6", className)}>
|
||||
<div className='xl:flex xl:flex-row-reverse xl:items-start'>
|
||||
<div className='w-full xl:w-6/12'>
|
||||
<FormDebug />
|
||||
</div>
|
||||
<div className='w-full xl:grow space-y-6'>
|
||||
<CustomerBasicInfoFields focusRef={focusRef} />
|
||||
<CustomerContactFields />
|
||||
<CustomerAddressFields />
|
||||
<CustomerAdditionalConfigFields />
|
||||
</div>
|
||||
</div>
|
||||
<form noValidate id={formId} onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.stopPropagation();
|
||||
form.handleSubmit(onSubmit, onError)(event)
|
||||
}}>
|
||||
<FormDebug />
|
||||
<section className={cn("space-y-6 p-6 xl:grid-cols-2 xl:grid xl:gap-6", className)}>
|
||||
<CustomerBasicInfoFields focusRef={focusRef} />
|
||||
<CustomerAddressFields />
|
||||
<CustomerContactFields />
|
||||
<CustomerAdditionalConfigFields />
|
||||
</section>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -29,7 +29,7 @@ export function useCreateCustomer() {
|
||||
return useMutation<Customer, DefaultError, CreateCustomerPayload>({
|
||||
mutationKey: CUSTOMER_CREATE_KEY,
|
||||
|
||||
mutationFn: async (data) => {
|
||||
mutationFn: async ({ data }, context) => {
|
||||
const id = UniqueID.generateNewID().toString();
|
||||
const payload = { ...data, id };
|
||||
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CustomerEditForm
|
||||
formId='customer-create-form'
|
||||
formId={formId}
|
||||
onSubmit={(data) =>
|
||||
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"
|
||||
|
||||
/>
|
||||
</FormProvider>
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -9,7 +9,7 @@ export const AppContent = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"app-content flex flex-1 flex-col gap-4 p-6 pt-8 bg-primary/5 min-h-screen",
|
||||
"app-content flex flex-1 flex-col gap-4 p-4 pt-6 bg-primary/5 min-h-screen",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -7,7 +7,7 @@ export const AppHeader = ({
|
||||
...props
|
||||
}: PropsWithChildren<{ className?: string }>) => {
|
||||
return (
|
||||
<div className={cn("app-header gap-4 px-6 pt-0 border-b bg-background", className)} {...props}>
|
||||
<div className={cn("app-header gap-4 px-4 pt-0 border-b bg-background", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user