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/shadcn-ui/globals.css";
|
||||||
@import "@repo/rdx-ui/globals.css";
|
@import "@repo/rdx-ui/globals.css";
|
||||||
|
@import "@erp/core/globals.css";
|
||||||
@import "@erp/customers/globals.css";
|
@import "@erp/customers/globals.css";
|
||||||
@import "@erp/customer-invoices/globals.css";
|
@import "@erp/customer-invoices/globals.css";
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
".": "./src/common/index.ts",
|
".": "./src/common/index.ts",
|
||||||
"./api": "./src/api/index.ts",
|
"./api": "./src/api/index.ts",
|
||||||
"./client": "./src/web/manifest.ts",
|
"./client": "./src/web/manifest.ts",
|
||||||
|
"./globals.css": "./src/web/globals.css",
|
||||||
"./components": "./src/web/components/index.ts",
|
"./components": "./src/web/components/index.ts",
|
||||||
"./hooks": "./src/web/hooks/index.ts"
|
"./hooks": "./src/web/hooks/index.ts"
|
||||||
},
|
},
|
||||||
@ -13,6 +14,7 @@
|
|||||||
"react": "^19.1.0"
|
"react": "^19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@hookform/devtools": "^4.4.0",
|
||||||
"@types/axios": "^0.14.4",
|
"@types/axios": "^0.14.4",
|
||||||
"@types/dinero.js": "^1.9.4",
|
"@types/dinero.js": "^1.9.4",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { DevTool } from '@hookform/devtools';
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
@ -43,12 +44,15 @@ function DebugField({ label, oldValue, newValue }: { label?: string; oldValue: a
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FormDebug = () => {
|
export const FormDebug = () => {
|
||||||
const { watch, formState } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const { isDirty, dirtyFields, defaultValues } = formState;
|
//const { watch, formState } = useFormContext();
|
||||||
const currentValues = watch();
|
//const { isDirty, dirtyFields, defaultValues } = formState;
|
||||||
|
//const currentValues = watch();
|
||||||
|
|
||||||
return (
|
return <DevTool control={control} placement="top-right" />
|
||||||
<div className="p-4 border rounded bg-red-50 mb-6">
|
|
||||||
|
/*return (
|
||||||
|
<div className="absolute right-4 bottom-4 z-50 p-4 border rounded bg-red-50">
|
||||||
<p>
|
<p>
|
||||||
<strong>¿Formulario modificado?</strong> {isDirty ? "Sí" : "No"}
|
<strong>¿Formulario modificado?</strong> {isDirty ? "Sí" : "No"}
|
||||||
</p>
|
</p>
|
||||||
@ -70,5 +74,5 @@ export const FormDebug = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);*/
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,30 +15,35 @@ interface PageHeaderProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function PageHeader({ backIcon, title, description, rightSlot, className }: PageHeaderProps) {
|
export function PageHeader({ backIcon, title, description, rightSlot, className }: PageHeaderProps) {
|
||||||
return (
|
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 */}
|
{/* Lado izquierdo */}
|
||||||
<div className='flex items-center gap-4'>
|
<div className='min-w-0 flex-1'>
|
||||||
{backIcon && (
|
<div className='flex items-start gap-4'>
|
||||||
<Button
|
{backIcon && (
|
||||||
variant='ghost'
|
<Button
|
||||||
size='icon'
|
variant='ghost'
|
||||||
className='cursor-pointer'
|
size='icon'
|
||||||
onClick={() => window.history.back()}
|
className='cursor-pointer'
|
||||||
>
|
onClick={() => window.history.back()}
|
||||||
<ChevronLeftIcon className='size-5' />
|
>
|
||||||
</Button>
|
<ChevronLeftIcon className='size-5' />
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-semibold text-foreground'>{title}</h2>
|
<h2 className='h-8 text-xl font-semibold text-foreground sm:truncate sm:tracking-tight'>{title}</h2>
|
||||||
{description && <p className='text-base text-muted-foreground'>{description}</p>}
|
{description && <p className='text-sm text-muted-foreground'>{description}</p>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lado derecho parametrizable */}
|
{/* Lado derecho parametrizable */}
|
||||||
{rightSlot && <>{rightSlot}</>}
|
<div className="mt-4 flex lg:mt-0 lg:ml-4">
|
||||||
|
{rightSlot}
|
||||||
|
</div>
|
||||||
</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) => {
|
}: InvoiceUpdateCompProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { invoice_id } = useInvoiceContext(); // ahora disponible desde el inicio
|
|
||||||
const context = useInvoiceContext();
|
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
|
|
||||||
|
const context = useInvoiceContext();
|
||||||
|
const { invoice_id } = context;
|
||||||
|
|
||||||
const isPending = !invoiceData;
|
const isPending = !invoiceData;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -54,7 +55,7 @@ export const InvoiceUpdateComp = ({
|
|||||||
const form = useHookForm<InvoiceFormData>({
|
const form = useHookForm<InvoiceFormData>({
|
||||||
resolverSchema: InvoiceFormSchema,
|
resolverSchema: InvoiceFormSchema,
|
||||||
initialValues,
|
initialValues,
|
||||||
disabled: !invoiceData || isUpdating
|
disabled: !invoiceData || isUpdating,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (formData: InvoiceFormData) => {
|
const handleSubmit = (formData: InvoiceFormData) => {
|
||||||
@ -89,7 +90,11 @@ export const InvoiceUpdateComp = ({
|
|||||||
backIcon
|
backIcon
|
||||||
title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`}
|
title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`}
|
||||||
description={t("pages.edit.description")}
|
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
|
<UpdateCommitButtonGroup
|
||||||
isLoading={isPending}
|
isLoading={isPending}
|
||||||
|
|
||||||
@ -97,7 +102,7 @@ export const InvoiceUpdateComp = ({
|
|||||||
cancel={{ formId, to: "/customer-invoices/list" }}
|
cancel={{ formId, to: "/customer-invoices/list" }}
|
||||||
onBack={() => navigate(-1)}
|
onBack={() => navigate(-1)}
|
||||||
/>
|
/>
|
||||||
}
|
</>}
|
||||||
/>
|
/>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
|
|
||||||
|
|||||||
@ -21,21 +21,27 @@ export const InvoiceUpdateForm = ({
|
|||||||
const form = useFormContext<InvoiceFormData>();
|
const form = useFormContext<InvoiceFormData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
<form noValidate id={formId} onSubmit={
|
||||||
<section className={cn("p-6 space-y-6", className)}>
|
(event: React.FormEvent<HTMLFormElement>) => {
|
||||||
<div className="w-full p-6 bg-transparent grid grid-cols-1 lg:grid-cols-3 gap-6">
|
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" />
|
<InvoiceRecipient className="flex flex-col" />
|
||||||
<InvoiceBasicInfoFields className="flex flex-col lg:col-span-2" />
|
<InvoiceBasicInfoFields className="flex flex-col lg:col-span-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='w-full gap-6 px-6'>
|
<div className='w-full'>
|
||||||
<InvoiceItems />
|
<InvoiceItems />
|
||||||
</div>
|
</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' />
|
<InvoiceTotals className='lg:col-start-2' />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full p-6">
|
<div className="w-full">
|
||||||
<FormDebug />
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { useId } from 'react';
|
import { useCallback, useId } from 'react';
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { useCustomerCreateController } from '../../pages/create/use-customer-create-controller';
|
import { useCustomerCreateController } from '../../pages/create/use-customer-create-controller';
|
||||||
import { CustomerFormData } from "../../schemas";
|
import { CustomerFormData } from "../../schemas";
|
||||||
@ -32,6 +32,8 @@ export function CustomerCreateModal({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
|
|
||||||
|
const { requestConfirm } = useUnsavedChangesContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
form, isCreating, isCreateError, createError,
|
form, isCreating, isCreateError, createError,
|
||||||
handleSubmit, handleError, FormProvider
|
handleSubmit, handleError, FormProvider
|
||||||
@ -39,31 +41,40 @@ export function CustomerCreateModal({
|
|||||||
|
|
||||||
const { isDirty } = form.formState;
|
const { isDirty } = form.formState;
|
||||||
|
|
||||||
const guardClose = async (nextOpen: boolean) => {
|
const guardClose = useCallback(async (nextOpen: boolean) => {
|
||||||
if (nextOpen) return onOpenChange(true);
|
if (nextOpen) return onOpenChange(true);
|
||||||
|
|
||||||
if (isCreating) return;
|
if (isCreating) return;
|
||||||
const { requestConfirm } = useUnsavedChangesContext();
|
|
||||||
const ok = await requestConfirm();
|
if (!isDirty) {
|
||||||
if (ok) onOpenChange(false);
|
return onOpenChange(false);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if (await requestConfirm()) {
|
||||||
|
return onOpenChange(false);
|
||||||
|
}
|
||||||
|
}, [requestConfirm, isCreating, onOpenChange, isDirty]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleFormSubmit = (data: CustomerFormData) => handleSubmit(data /*, () => onOpenChange(false)*/);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnsavedChangesProvider isDirty={isDirty}>
|
<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
|
<CustomerEditForm
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSubmit={(data: CustomerFormData) => handleSubmit(data, () => onOpenChange(false))}
|
onSubmit={handleFormSubmit}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
className="max-w-none"
|
className="max-w-none"
|
||||||
/>
|
/>
|
||||||
@ -73,19 +84,20 @@ export function CustomerCreateModal({
|
|||||||
{(createError as Error)?.message}
|
{(createError as Error)?.message}
|
||||||
</p>
|
</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>
|
</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";
|
import { CustomerModalSelector } from "./customer-modal-selector";
|
||||||
|
|
||||||
type CustomerModalSelectorFieldProps<TFormValues extends FieldValues> = {
|
type CustomerModalSelectorFieldProps<TFormValues extends FieldValues> = {
|
||||||
control: Control<TFormValues>;
|
control: Control<TFormValues>;
|
||||||
name: FieldPath<TFormValues>;
|
name: FieldPath<TFormValues>;
|
||||||
|
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
orientation?: "vertical" | "horizontal" | "responsive",
|
||||||
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
initiaCustomer?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CustomerModalSelectorField<TFormValues extends FieldValues>({
|
export function CustomerModalSelectorField<TFormValues extends FieldValues>({
|
||||||
control,
|
control,
|
||||||
name,
|
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,
|
className,
|
||||||
|
initiaCustomer = {},
|
||||||
}: CustomerModalSelectorFieldProps<TFormValues>) {
|
}: CustomerModalSelectorFieldProps<TFormValues>) {
|
||||||
const isDisabled = disabled;
|
const isDisabled = disabled;
|
||||||
const isReadOnly = readOnly && !disabled;
|
const isReadOnly = readOnly && !disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => {
|
render={({ field, fieldState }) => {
|
||||||
const { name, value, onChange, onBlur, ref } = field;
|
const { name, value, onChange, onBlur, ref } = field;
|
||||||
|
|
||||||
return (
|
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
|
<CustomerModalSelector
|
||||||
value={value as string | undefined}
|
value={value}
|
||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
|
initialCustomer={initiaCustomer as CustomerSummary}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</Field>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -6,12 +6,19 @@ import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
|
|||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerFormData } from "../../schemas";
|
import { CustomerFormData } from "../../schemas";
|
||||||
|
|
||||||
export const CustomerAdditionalConfigFields = () => {
|
interface CustomerAdditionalConfigFieldsProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const CustomerAdditionalConfigFields = ({
|
||||||
|
className, ...props
|
||||||
|
}: CustomerAdditionalConfigFieldsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerFormData>();
|
const { control } = useFormContext<CustomerFormData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldSet>
|
<FieldSet className={className} {...props}>
|
||||||
<FieldLegend>{t("form_groups.preferences.title")}</FieldLegend>
|
<FieldLegend>{t("form_groups.preferences.title")}</FieldLegend>
|
||||||
<FieldDescription>{t("form_groups.preferences.description")}</FieldDescription>
|
<FieldDescription>{t("form_groups.preferences.description")}</FieldDescription>
|
||||||
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
|
<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 { useTranslation } from "../../i18n";
|
||||||
import { CustomerFormData } from "../../schemas";
|
import { CustomerFormData } from "../../schemas";
|
||||||
|
|
||||||
export const CustomerAddressFields = () => {
|
interface CustomerAddressFieldsProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomerAddressFields = ({ className, ...props }: CustomerAddressFieldsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerFormData>();
|
const { control } = useFormContext<CustomerFormData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldSet>
|
<FieldSet className={className} {...props}>
|
||||||
<FieldLegend>{t("form_groups.address.title")}</FieldLegend>
|
<FieldLegend>{t("form_groups.address.title")}</FieldLegend>
|
||||||
<FieldDescription>{t("form_groups.address.description")}</FieldDescription>
|
<FieldDescription>{t("form_groups.address.description")}</FieldDescription>
|
||||||
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
|
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
|
||||||
|
|||||||
@ -19,9 +19,10 @@ import { CustomerFormData } from "../../schemas";
|
|||||||
|
|
||||||
interface CustomerBasicInfoFieldsProps {
|
interface CustomerBasicInfoFieldsProps {
|
||||||
focusRef?: React.RefObject<HTMLInputElement>;
|
focusRef?: React.RefObject<HTMLInputElement>;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomerBasicInfoFields = ({ focusRef }: CustomerBasicInfoFieldsProps) => {
|
export const CustomerBasicInfoFields = ({ focusRef, className, ...props }: CustomerBasicInfoFieldsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerFormData>();
|
const { control } = useFormContext<CustomerFormData>();
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ export const CustomerBasicInfoFields = ({ focusRef }: CustomerBasicInfoFieldsPro
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldSet>
|
<FieldSet className={className} {...props}>
|
||||||
<FieldLegend>{t("form_groups.basic_info.title")}</FieldLegend>
|
<FieldLegend>{t("form_groups.basic_info.title")}</FieldLegend>
|
||||||
<FieldDescription>{t("form_groups.basic_info.description")}</FieldDescription>
|
<FieldDescription>{t("form_groups.basic_info.description")}</FieldDescription>
|
||||||
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
|
<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 { useFormContext } from "react-hook-form";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
export const CustomerContactFields = () => {
|
interface CustomerContactFieldsProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomerContactFields = ({ className, ...props }: CustomerContactFieldsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldSet>
|
<FieldSet className={className} {...props}>
|
||||||
<FieldLegend>{t("form_groups.contact_info.title")}</FieldLegend>
|
<FieldLegend>{t("form_groups.contact_info.title")}</FieldLegend>
|
||||||
<FieldDescription>{t("form_groups.contact_info.description")}</FieldDescription>
|
<FieldDescription>{t("form_groups.contact_info.description")}</FieldDescription>
|
||||||
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
|
<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>();
|
const form = useFormContext<CustomerFormData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
<form noValidate id={formId} onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
|
||||||
<section className={cn("p-6", className)}>
|
event.stopPropagation();
|
||||||
<div className='xl:flex xl:flex-row-reverse xl:items-start'>
|
form.handleSubmit(onSubmit, onError)(event)
|
||||||
<div className='w-full xl:w-6/12'>
|
}}>
|
||||||
<FormDebug />
|
<FormDebug />
|
||||||
</div>
|
<section className={cn("space-y-6 p-6 xl:grid-cols-2 xl:grid xl:gap-6", className)}>
|
||||||
<div className='w-full xl:grow space-y-6'>
|
<CustomerBasicInfoFields focusRef={focusRef} />
|
||||||
<CustomerBasicInfoFields focusRef={focusRef} />
|
<CustomerAddressFields />
|
||||||
<CustomerContactFields />
|
<CustomerContactFields />
|
||||||
<CustomerAddressFields />
|
<CustomerAdditionalConfigFields />
|
||||||
<CustomerAdditionalConfigFields />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export function useCreateCustomer() {
|
|||||||
return useMutation<Customer, DefaultError, CreateCustomerPayload>({
|
return useMutation<Customer, DefaultError, CreateCustomerPayload>({
|
||||||
mutationKey: CUSTOMER_CREATE_KEY,
|
mutationKey: CUSTOMER_CREATE_KEY,
|
||||||
|
|
||||||
mutationFn: async (data) => {
|
mutationFn: async ({ data }, context) => {
|
||||||
const id = UniqueID.generateNewID().toString();
|
const id = UniqueID.generateNewID().toString();
|
||||||
const payload = { ...data, id };
|
const payload = { ...data, id };
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import { PageHeader } from '@erp/core/components';
|
import { PageHeader } from '@erp/core/components';
|
||||||
import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks";
|
||||||
|
import { useId } from 'react';
|
||||||
import { CustomerEditForm, ErrorAlert } from "../../components";
|
import { CustomerEditForm, ErrorAlert } from "../../components";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { useCustomerCreateController } from './use-customer-create-controller';
|
import { useCustomerCreateController } from './use-customer-create-controller';
|
||||||
@ -10,6 +11,7 @@ import { useCustomerCreateController } from './use-customer-create-controller';
|
|||||||
export const CustomerCreatePage = () => {
|
export const CustomerCreatePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const formId = useId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
form, isCreating, isCreateError, createError,
|
form, isCreating, isCreateError, createError,
|
||||||
@ -37,7 +39,7 @@ export const CustomerCreatePage = () => {
|
|||||||
disabled: isCreating,
|
disabled: isCreating,
|
||||||
}}
|
}}
|
||||||
submit={{
|
submit={{
|
||||||
formId: "customer-create-form",
|
formId: formId,
|
||||||
disabled: isCreating,
|
disabled: isCreating,
|
||||||
}}
|
}}
|
||||||
onBack={() => handleBack()}
|
onBack={() => handleBack()}
|
||||||
@ -59,14 +61,14 @@ export const CustomerCreatePage = () => {
|
|||||||
|
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<CustomerEditForm
|
<CustomerEditForm
|
||||||
formId='customer-create-form'
|
formId={formId}
|
||||||
onSubmit={(data) =>
|
onSubmit={(data) =>
|
||||||
handleSubmit(data, ({ id }) =>
|
handleSubmit(data, ({ id }) =>
|
||||||
navigate("/customers/list", { state: { customerId: id, isNew: true }, replace: true })
|
navigate("/customers/list", { state: { customerId: id, isNew: true }, replace: true })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto"
|
|
||||||
/>
|
/>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
import { useHookForm } from "@erp/core/hooks";
|
import { useHookForm } from "@erp/core/hooks";
|
||||||
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
||||||
|
import { useId } from "react";
|
||||||
import { FieldErrors, FormProvider } from "react-hook-form";
|
import { FieldErrors, FormProvider } from "react-hook-form";
|
||||||
import { useCreateCustomer } from "../../hooks";
|
import { useCreateCustomer } from "../../hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerFormData, CustomerFormSchema, defaultCustomerFormData } from "../../schemas";
|
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 { t } = useTranslation();
|
||||||
|
const formId = useId(); // id único por instancia
|
||||||
|
|
||||||
// 1) Estado de creación (mutación)
|
// 1) Estado de creación (mutación)
|
||||||
const {
|
const {
|
||||||
@ -23,19 +30,24 @@ export const useCustomerCreateController = () => {
|
|||||||
disabled: isCreating,
|
disabled: isCreating,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (formData: CustomerFormData, onSuccess?: (data: { id: string }) => void) => {
|
const handleSubmit = (formData: CustomerFormData) => {
|
||||||
console.log(formData);
|
console.log(formData);
|
||||||
mutate(
|
mutate(
|
||||||
{ data: formData },
|
{
|
||||||
|
data: formData,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
form.reset(defaultCustomerFormData);
|
form.reset(defaultCustomerFormData);
|
||||||
showSuccessToast(
|
if (options?.successToasts !== false) {
|
||||||
t("pages.create.successTitle", "Cliente creado"),
|
showSuccessToast(
|
||||||
t("pages.create.successMsg", "Se ha creado correctamente.")
|
t("pages.create.successTitle", "Cliente creado"),
|
||||||
);
|
t("pages.create.successMsg", "Se ha creado correctamente.")
|
||||||
onSuccess?.({ id: data.id });
|
);
|
||||||
|
}
|
||||||
|
options?.onCreated?.(data);
|
||||||
},
|
},
|
||||||
|
|
||||||
onError(err: unknown) {
|
onError(err: unknown) {
|
||||||
console.log("No se pudo crear el cliente.");
|
console.log("No se pudo crear el cliente.");
|
||||||
const msg =
|
const msg =
|
||||||
@ -63,6 +75,7 @@ export const useCustomerCreateController = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
|
formId,
|
||||||
isCreating,
|
isCreating,
|
||||||
isCreateError,
|
isCreateError,
|
||||||
createError,
|
createError,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export const AppContent = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const AppHeader = ({
|
|||||||
...props
|
...props
|
||||||
}: PropsWithChildren<{ className?: string }>) => {
|
}: PropsWithChildren<{ className?: string }>) => {
|
||||||
return (
|
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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -429,6 +429,9 @@ importers:
|
|||||||
specifier: ^4.1.11
|
specifier: ^4.1.11
|
||||||
version: 4.1.12
|
version: 4.1.12
|
||||||
devDependencies:
|
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':
|
'@types/axios':
|
||||||
specifier: ^0.14.4
|
specifier: ^0.14.4
|
||||||
version: 0.14.4
|
version: 0.14.4
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user