Clientes y facturas de cliente

This commit is contained in:
David Arranz 2025-09-24 13:49:17 +02:00
parent 8d0c0b88de
commit 7e700bdf22
27 changed files with 317 additions and 251 deletions

View File

@ -45,6 +45,7 @@
"@erp/core": "workspace:*", "@erp/core": "workspace:*",
"@erp/customer-invoices": "workspace:*", "@erp/customer-invoices": "workspace:*",
"@erp/customers": "workspace:*", "@erp/customers": "workspace:*",
"@erp/verifactu": "workspace:*",
"@repo/rdx-logger": "workspace:*", "@repo/rdx-logger": "workspace:*",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cls-rtracer": "^2.6.3", "cls-rtracer": "^2.6.3",

View File

@ -1,6 +1,6 @@
import customerInvoicesAPIModule from "@erp/customer-invoices/api"; import customerInvoicesAPIModule from "@erp/customer-invoices/api";
import customersAPIModule from "@erp/customers/api"; import customersAPIModule from "@erp/customers/api";
import verifactuAPIModule from "@erp/verifactu/api"; //import verifactuAPIModule from "@erp/verifactu/api";
import { registerModule } from "./lib"; import { registerModule } from "./lib";
@ -8,5 +8,5 @@ export const registerModules = () => {
//registerModule(authAPIModule); //registerModule(authAPIModule);
registerModule(customersAPIModule); registerModule(customersAPIModule);
registerModule(customerInvoicesAPIModule); registerModule(customerInvoicesAPIModule);
registerModule(verifactuAPIModule); // registerModule(verifactuAPIModule);
}; };

View File

@ -2,6 +2,7 @@
"common": { "common": {
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"saving": "Saving...",
"required": "•" "required": "•"
}, },
"components": { "components": {

View File

@ -47,7 +47,7 @@ export function TaxesMultiSelectField<TFormValues extends FieldValues>({
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-0", className)}> <FormItem className={cn("space-y-0", className)}>
{label && ( {label && (
<div className='mb-1 flex justify-between gap-2'> <div className='mb-1 flex justify-between'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<FormLabel htmlFor={name} className='m-0'> <FormLabel htmlFor={name} className='m-0'>
{label} {label}

View File

@ -1,4 +1,5 @@
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { XIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -34,7 +35,6 @@ export const CancelFormButton = ({
const handleClick = useCallback(async () => { const handleClick = useCallback(async () => {
const ok = requestConfirm ? await requestConfirm() : true; const ok = requestConfirm ? await requestConfirm() : true;
console.log("ok => ", ok);
if (!ok) return; if (!ok) return;
if (onCancel) { if (onCancel) {
@ -43,7 +43,6 @@ export const CancelFormButton = ({
} }
if (to) { if (to) {
console.log("navego => ", to);
navigate(to); navigate(to);
} }
// si no hay ni onCancel ni to → no hace nada // si no hay ni onCancel ni to → no hace nada
@ -60,6 +59,7 @@ export const CancelFormButton = ({
aria-disabled={disabled} aria-disabled={disabled}
data-testid={dataTestId} data-testid={dataTestId}
> >
<XIcon className='mr-2 h-3 w-3' />
<span>{label ?? defaultLabel}</span> <span>{label ?? defaultLabel}</span>
</Button> </Button>
); );

View File

@ -1,16 +1,46 @@
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import {
ArrowLeftIcon,
CopyIcon,
EyeIcon,
MoreHorizontalIcon,
RotateCcwIcon,
Trash2Icon,
} from "lucide-react";
import { useFormContext } from "react-hook-form";
import { CancelFormButton, CancelFormButtonProps } from "./cancel-form-button"; import { CancelFormButton, CancelFormButtonProps } from "./cancel-form-button";
import { SubmitButtonProps, SubmitFormButton } from "./submit-form-button"; import { SubmitButtonProps, SubmitFormButton } from "./submit-form-button";
type Align = "start" | "center" | "end" | "between"; type Align = "start" | "center" | "end" | "between";
type GroupSubmitButtonProps = Omit<SubmitButtonProps, "isLoading" | "preventDoubleSubmit">;
export type FormCommitButtonGroupProps = { export type FormCommitButtonGroupProps = {
className?: string; className?: string;
align?: Align; // default "end" align?: Align; // default "end"
gap?: string; // default "gap-2" gap?: string; // default "gap-2"
reverseOrderOnMobile?: boolean; // default true (Cancel debajo en móvil) reverseOrderOnMobile?: boolean; // default true (Cancel debajo en móvil)
isLoading?: boolean;
disabled?: boolean;
preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading
cancel?: CancelFormButtonProps & { show?: boolean }; cancel?: CancelFormButtonProps & { show?: boolean };
submit?: SubmitButtonProps; // props directas a SubmitButton submit?: GroupSubmitButtonProps; // props directas a SubmitButton
onReset?: () => void;
onDelete?: () => void;
onPreview?: () => void;
onDuplicate?: () => void;
onBack?: () => void;
}; };
const alignToJustify: Record<Align, string> = { const alignToJustify: Record<Align, string> = {
@ -25,10 +55,33 @@ export const FormCommitButtonGroup = ({
align = "end", align = "end",
gap = "gap-2", gap = "gap-2",
reverseOrderOnMobile = true, reverseOrderOnMobile = true,
isLoading,
disabled = false,
preventDoubleSubmit = true,
cancel, cancel,
submit, submit,
onReset,
onDelete,
onPreview,
onDuplicate,
onBack,
}: FormCommitButtonGroupProps) => { }: FormCommitButtonGroupProps) => {
const showCancel = cancel?.show ?? true; const showCancel = cancel?.show ?? true;
const hasSecondaryActions = onReset || onPreview || onDuplicate || onBack || onDelete;
// ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading
let rhfIsSubmitting = false;
try {
const ctx = useFormContext();
rhfIsSubmitting = !!ctx?.formState?.isSubmitting;
} catch {
// No hay provider de RHF; ignorar
}
const busy = isLoading ?? rhfIsSubmitting;
const computedDisabled = !!(disabled || (preventDoubleSubmit && busy));
return ( return (
<div <div
@ -40,8 +93,62 @@ export const FormCommitButtonGroup = ({
className className
)} )}
> >
{showCancel && <CancelFormButton {...cancel} />}
{submit && <SubmitFormButton {...submit} />} {submit && <SubmitFormButton {...submit} />}
{showCancel && <CancelFormButton {...cancel} />}
{/* Menú de acciones adicionales */}
{hasSecondaryActions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' size='sm' disabled={computedDisabled} className='px-2'>
<MoreHorizontalIcon className='h-4 w-4' />
<span className='sr-only'>Más acciones</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-48'>
{onReset && (
<DropdownMenuItem
onClick={onReset}
disabled={computedDisabled}
className='text-muted-foreground'
>
<RotateCcwIcon className='mr-2 h-4 w-4' />
Deshacer cambios
</DropdownMenuItem>
)}
{onPreview && (
<DropdownMenuItem onClick={onPreview} className='text-muted-foreground'>
<EyeIcon className='mr-2 h-4 w-4' />
Vista previa
</DropdownMenuItem>
)}
{onDuplicate && (
<DropdownMenuItem onClick={onDuplicate} className='text-muted-foreground'>
<CopyIcon className='mr-2 h-4 w-4' />
Duplicar
</DropdownMenuItem>
)}
{onBack && (
<DropdownMenuItem onClick={onBack} className='text-muted-foreground'>
<ArrowLeftIcon className='mr-2 h-4 w-4' />
Volver
</DropdownMenuItem>
)}
{onDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onDelete}
className='text-destructive focus:text-destructive'
>
<Trash2Icon className='mr-2 h-4 w-4' />
Eliminar
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div> </div>
); );
}; };

View File

@ -1,5 +1,6 @@
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { LoaderCircleIcon } from "lucide-react"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { LoaderCircleIcon, SaveIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { useTranslation } from "../../../i18n.ts"; import { useTranslation } from "../../../i18n.ts";
@ -8,12 +9,15 @@ export type SubmitButtonProps = {
formId?: string; formId?: string;
isLoading?: boolean; isLoading?: boolean;
label?: string; label?: string;
labelIsLoading?: string;
variant?: React.ComponentProps<typeof Button>["variant"]; variant?: React.ComponentProps<typeof Button>["variant"];
size?: React.ComponentProps<typeof Button>["size"]; size?: React.ComponentProps<typeof Button>["size"];
className?: string; className?: string;
preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading
hasChanges?: boolean;
onClick?: React.MouseEventHandler<HTMLButtonElement>; onClick?: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean; disabled?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
@ -24,10 +28,12 @@ export const SubmitFormButton = ({
formId, formId,
isLoading, isLoading,
label, label,
labelIsLoading,
variant = "default", variant = "default",
size = "default", size = "default",
className, className,
preventDoubleSubmit = true, preventDoubleSubmit = true,
hasChanges = false,
onClick, onClick,
disabled, disabled,
children, children,
@ -35,6 +41,7 @@ export const SubmitFormButton = ({
}: SubmitButtonProps) => { }: SubmitButtonProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const defaultLabel = t ? t("common.save") : "Save"; const defaultLabel = t ? t("common.save") : "Save";
const defaultLabelIsLoading = t ? t("common.saving") : "Saving...";
// ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading // ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading
let rhfIsSubmitting = false; let rhfIsSubmitting = false;
@ -65,20 +72,30 @@ export const SubmitFormButton = ({
form={formId} form={formId}
variant={variant} variant={variant}
size={size} size={size}
className={className}
disabled={computedDisabled} disabled={computedDisabled}
aria-busy={busy} aria-busy={busy}
aria-disabled={computedDisabled} aria-disabled={computedDisabled}
data-state={dataState} data-state={dataState}
onClick={handleClick} onClick={handleClick}
data-testid={dataTestId} data-testid={dataTestId}
className={cn("min-w-[100px] font-medium", hasChanges && "ring-2 ring-primary/20", className)}
> >
{children ? ( {children ? (
children children
) : ( ) : (
<span className='inline-flex items-center gap-2'> <span className='inline-flex items-center gap-2'>
{busy && <LoaderCircleIcon className='h-4 w-4 animate-spin' aria-hidden='true' />} {busy && (
<span>{label ?? defaultLabel}</span> <>
<LoaderCircleIcon className='mr-2 h-3 w-3 animate-spin' aria-hidden='true' />
<span>{labelIsLoading ?? defaultLabelIsLoading}</span>
</>
)}
{!busy && (
<>
<SaveIcon className='mr-2 h-3 w-3' />
<span>{label ?? defaultLabel}</span>
</>
)}
</span> </span>
)} )}
</Button> </Button>

View File

@ -1,11 +1,6 @@
import { Description, Field, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
import { SelectField } from "@repo/rdx-ui/components"; import { SelectField } from "@repo/rdx-ui/components";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/shadcn-ui/components";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants"; import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
import { useTranslation } from "../../i18n"; import { useTranslation } from "../../i18n";
@ -16,15 +11,12 @@ export const CustomerAdditionalConfigFields = () => {
const { control } = useFormContext<CustomerFormData>(); const { control } = useFormContext<CustomerFormData>();
return ( return (
<Card className='border-0 shadow-none'> <Fieldset>
<CardHeader> <Legend>{t("form_groups.preferences.title")}</Legend>
<CardTitle>{t("form_groups.preferences.title")}</CardTitle> <Description>{t("form_groups.preferences.description")}</Description>
<CardDescription>{t("form_groups.preferences.description")}</CardDescription> <FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
</CardHeader> <Field className='lg:col-span-2'>
<CardContent>
<div className='grid grid-cols-1 gap-8 lg:grid-cols-4 mb-12 '>
<SelectField <SelectField
className='lg:col-span-2'
control={control} control={control}
name='language_code' name='language_code'
required required
@ -33,6 +25,8 @@ export const CustomerAdditionalConfigFields = () => {
description={t("form_fields.language_code.description")} description={t("form_fields.language_code.description")}
items={[...LANGUAGE_OPTIONS]} items={[...LANGUAGE_OPTIONS]}
/> />
</Field>
<Field className='lg:col-span-2'>
<SelectField <SelectField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -43,8 +37,8 @@ export const CustomerAdditionalConfigFields = () => {
description={t("form_fields.currency_code.description")} description={t("form_fields.currency_code.description")}
items={[...CURRENCY_OPTIONS]} items={[...CURRENCY_OPTIONS]}
/> />
</div> </Field>
</CardContent> </FieldGroup>
</Card> </Fieldset>
); );
}; };

View File

@ -1,18 +1,12 @@
import { import {
Description, Description,
Field,
FieldGroup, FieldGroup,
Fieldset, Fieldset,
Legend, Legend,
SelectField, SelectField,
TextField, TextField,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/shadcn-ui/components";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { COUNTRY_OPTIONS } from "../../constants"; import { COUNTRY_OPTIONS } from "../../constants";
import { useTranslation } from "../../i18n"; import { useTranslation } from "../../i18n";
@ -26,7 +20,7 @@ export const CustomerAddressFields = () => {
<Fieldset> <Fieldset>
<Legend>{t("form_groups.address.title")}</Legend> <Legend>{t("form_groups.address.title")}</Legend>
<Description>{t("form_groups.address.description")}</Description> <Description>{t("form_groups.address.description")}</Description>
<FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
<TextField <TextField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -60,77 +54,16 @@ export const CustomerAddressFields = () => {
description={t("form_fields.postal_code.description")} description={t("form_fields.postal_code.description")}
/> />
<TextField <Field className='lg:col-span-2 lg:col-start-1'>
className='lg:col-span-2 lg:col-start-1'
control={control}
name='province'
label={t("form_fields.province.label")}
placeholder={t("form_fields.province.placeholder")}
description={t("form_fields.province.description")}
/>
<SelectField
control={control}
name='country'
required
label={t("form_fields.country.label")}
placeholder={t("form_fields.country.placeholder")}
description={t("form_fields.country.description")}
items={[...COUNTRY_OPTIONS]}
/>
</FieldGroup>
</Fieldset>
);
return (
<Card className='border-0 shadow-none'>
<CardHeader>
<CardTitle>{t("form_groups.address.title")}</CardTitle>
<CardDescription>{t("form_groups.address.description")}</CardDescription>
</CardHeader>
<CardContent>
<div className='grid grid-cols-1 gap-8 lg:grid-cols-4 mb-6 '>
<TextField <TextField
className='lg:col-span-2'
control={control}
name='street'
label={t("form_fields.street.label")}
placeholder={t("form_fields.street.placeholder")}
description={t("form_fields.street.description")}
/>
<TextField
className='lg:col-span-2'
control={control}
name='street2'
label={t("form_fields.street2.label")}
placeholder={t("form_fields.street2.placeholder")}
description={t("form_fields.street2.description")}
/>
<TextField
className='lg:col-span-2'
control={control}
name='city'
label={t("form_fields.city.label")}
placeholder={t("form_fields.city.placeholder")}
description={t("form_fields.city.description")}
/>
<TextField
control={control}
name='postal_code'
label={t("form_fields.postal_code.label")}
placeholder={t("form_fields.postal_code.placeholder")}
description={t("form_fields.postal_code.description")}
/>
</div>
<div className='grid grid-cols-1 gap-8 lg:grid-cols-4 mb-0 '>
<TextField
className='lg:col-span-2'
control={control} control={control}
name='province' name='province'
label={t("form_fields.province.label")} label={t("form_fields.province.label")}
placeholder={t("form_fields.province.placeholder")} placeholder={t("form_fields.province.placeholder")}
description={t("form_fields.province.description")} description={t("form_fields.province.description")}
/> />
</Field>
<Field className='lg:col-span-2'>
<SelectField <SelectField
control={control} control={control}
name='country' name='country'
@ -140,8 +73,8 @@ export const CustomerAddressFields = () => {
description={t("form_fields.country.description")} description={t("form_fields.country.description")}
items={[...COUNTRY_OPTIONS]} items={[...COUNTRY_OPTIONS]}
/> />
</div> </Field>
</CardContent> </FieldGroup>
</Card> </Fieldset>
); );
}; };

View File

@ -35,7 +35,7 @@ export const CustomerBasicInfoFields = () => {
<Fieldset> <Fieldset>
<Legend>Identificación</Legend> <Legend>Identificación</Legend>
<Description>descripción</Description> <Description>descripción</Description>
<FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
<Field className='lg:col-span-2'> <Field className='lg:col-span-2'>
<TextField <TextField
control={control} control={control}
@ -59,7 +59,7 @@ export const CustomerBasicInfoFields = () => {
field.onChange(value === "false" ? "false" : "true"); field.onChange(value === "false" ? "false" : "true");
}} }}
defaultValue={field.value ? "true" : "false"} defaultValue={field.value ? "true" : "false"}
className='flex items-center gap-8' className='flex items-center gap-6'
> >
<FormItem className='flex items-center space-x-2'> <FormItem className='flex items-center space-x-2'>
<FormControl> <FormControl>
@ -106,16 +106,16 @@ export const CustomerBasicInfoFields = () => {
placeholder={t("form_fields.reference.placeholder")} placeholder={t("form_fields.reference.placeholder")}
description={t("form_fields.reference.description")} description={t("form_fields.reference.description")}
/> />
<TaxesMultiSelectField <Field className='lg:col-span-2'>
className='lg:col-span-2' <TaxesMultiSelectField
control={control} control={control}
name='default_taxes' name='default_taxes'
required required
label={t("form_fields.default_taxes.label")} label={t("form_fields.default_taxes.label")}
placeholder={t("form_fields.default_taxes.placeholder")} placeholder={t("form_fields.default_taxes.placeholder")}
description={t("form_fields.default_taxes.description")} description={t("form_fields.default_taxes.description")}
/> />
</Field>
<TextAreaField <TextAreaField
className='lg:col-span-full' className='lg:col-span-full'
control={control} control={control}

View File

@ -1,4 +1,11 @@
import { Description, FieldGroup, Fieldset, Legend, TextField } from "@repo/rdx-ui/components"; import {
Description,
Field,
FieldGroup,
Fieldset,
Legend,
TextField,
} from "@repo/rdx-ui/components";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@repo/shadcn-ui/components"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@repo/shadcn-ui/components";
import { ChevronDown, MailIcon, PhoneIcon, SmartphoneIcon } from "lucide-react"; import { ChevronDown, MailIcon, PhoneIcon, SmartphoneIcon } from "lucide-react";
@ -15,7 +22,7 @@ export const CustomerContactFields = () => {
<Fieldset> <Fieldset>
<Legend>{t("form_groups.contact_info.title")}</Legend> <Legend>{t("form_groups.contact_info.title")}</Legend>
<Description>{t("form_groups.contact_info.description")}</Description> <Description>{t("form_groups.contact_info.description")}</Description>
<FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
<TextField <TextField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -27,6 +34,7 @@ export const CustomerContactFields = () => {
typePreset='email' typePreset='email'
required required
/> />
<TextField <TextField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -90,29 +98,37 @@ export const CustomerContactFields = () => {
} }
/> />
<Collapsible open={open} onOpenChange={setOpen} className='space-y-4'> <Collapsible
open={open}
onOpenChange={setOpen}
className='space-y-8 col-start-1 col-span-full'
>
<CollapsibleTrigger className='inline-flex items-center gap-1 text-sm text-primary hover:underline'> <CollapsibleTrigger className='inline-flex items-center gap-1 text-sm text-primary hover:underline'>
{t("common.more_details")}{" "} {t("common.more_details")}{" "}
<ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} /> <ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
<TextField <Field className='lg:col-span-2'>
className='lg:col-span-2' <TextField
control={control} className='lg:col-span-2'
name='website' control={control}
label={t("form_fields.website.label")} name='website'
placeholder={t("form_fields.website.placeholder")} label={t("form_fields.website.label")}
description={t("form_fields.website.description")} placeholder={t("form_fields.website.placeholder")}
/> description={t("form_fields.website.description")}
<TextField />
className='lg:col-span-2' </Field>
control={control} <Field className='lg:col-span-2'>
name='fax' <TextField
label={t("form_fields.fax.label")} className='lg:col-span-2'
placeholder={t("form_fields.fax.placeholder")} control={control}
description={t("form_fields.fax.description")} name='fax'
/> label={t("form_fields.fax.label")}
placeholder={t("form_fields.fax.placeholder")}
description={t("form_fields.fax.description")}
/>
</Field>
</FieldGroup> </FieldGroup>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>

View File

@ -22,7 +22,7 @@ export const CustomerEditForm = ({ formId, onSubmit, onError }: CustomerFormProp
<div className='w-full xl:w-6/12'> <div className='w-full xl:w-6/12'>
<FormDebug /> <FormDebug />
</div> </div>
<div className='w-full xl:grow'> <div className='w-full xl:grow space-y-6'>
<CustomerBasicInfoFields /> <CustomerBasicInfoFields />
<CustomerContactFields /> <CustomerContactFields />
<CustomerAddressFields /> <CustomerAddressFields />

View File

@ -54,6 +54,10 @@ export const CustomerCreate = () => {
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario // Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
}; };
const handleBack = () => {
navigate(-1);
};
return ( return (
<> <>
<AppBreadcrumb /> <AppBreadcrumb />
@ -69,14 +73,17 @@ export const CustomerCreate = () => {
</p> </p>
</div> </div>
<FormCommitButtonGroup <FormCommitButtonGroup
isLoading={isCreating}
disabled={isCreating}
cancel={{ cancel={{
to: "/customers/list", to: "/customers/list",
disabled: isCreating,
}} }}
submit={{ submit={{
formId: "customer-create-form", formId: "customer-create-form",
disabled: isCreating, disabled: isCreating,
isLoading: isCreating,
}} }}
onBack={() => handleBack()}
/> />
</div> </div>
{/* Alerta de error de actualización (si ha fallado el último intento) */} {/* Alerta de error de actualización (si ha fallado el último intento) */}

View File

@ -67,6 +67,12 @@ export const CustomerUpdate = () => {
); );
}; };
const handleReset = () => form.reset(customerData ?? defaultCustomerFormData);
const handleBack = () => {
navigate(-1);
};
const handleError = (errors: FieldErrors<CustomerFormData>) => { const handleError = (errors: FieldErrors<CustomerFormData>) => {
console.error("Errores en el formulario:", errors); console.error("Errores en el formulario:", errors);
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario // Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
@ -125,14 +131,18 @@ export const CustomerUpdate = () => {
</p> </p>
</div> </div>
<FormCommitButtonGroup <FormCommitButtonGroup
isLoading={isUpdating}
disabled={isUpdating}
cancel={{ cancel={{
to: "/customers/list", to: "/customers/list",
disabled: isUpdating,
}} }}
submit={{ submit={{
formId: "customer-update-form", formId: "customer-update-form",
disabled: isUpdating, disabled: isUpdating,
isLoading: isUpdating,
}} }}
onBack={() => handleBack()}
onReset={() => handleReset()}
/> />
</div> </div>
{/* Alerta de error de actualización (si ha fallado el último intento) */} {/* Alerta de error de actualización (si ha fallado el último intento) */}

View File

@ -1,3 +1,3 @@
//export * from "./mappers"; //export * from "./mappers";
//export * from "./sequelize";
export * from "./express"; export * from "./express";
export * from "./sequelize";

View File

@ -1,6 +1,7 @@
import { import {
Calendar, Calendar,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -166,16 +167,9 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
</p> </p>
)} )}
{(inputError || description) && ( <FormDescription className={cn("text-xs truncate", !description && "invisible")}>
<p {description || "\u00A0"}
className={cn( </FormDescription>
"text-xs mt-1",
inputError ? "text-destructive" : "text-muted-foreground"
)}
>
{inputError || description}
</p>
)}
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@ -2,6 +2,7 @@
import { import {
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -53,9 +54,9 @@ export function NumberField<TFormValues extends FieldValues>({
<Input disabled={isDisabled} placeholder={placeholder} {...field} /> <Input disabled={isDisabled} placeholder={placeholder} {...field} />
</FormControl> </FormControl>
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}> <FormDescription className={cn("text-xs truncate", !description && "invisible")}>
{description || "\u00A0"} {description || "\u00A0"}
</p> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@ -55,7 +55,7 @@ export function SelectField<TFormValues extends FieldValues>({
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-0", className)}> <FormItem className={cn("space-y-0", className)}>
{label && ( {label && (
<div className='mb-1 flex justify-between gap-2'> <div className='mb-1 flex justify-between'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<FormLabel htmlFor={name} className='m-0'> <FormLabel htmlFor={name} className='m-0'>
{label} {label}
@ -72,7 +72,7 @@ export function SelectField<TFormValues extends FieldValues>({
)} )}
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={isDisabled}> <Select onValueChange={field.onChange} defaultValue={field.value} disabled={isDisabled}>
<FormControl> <FormControl>
<SelectTrigger className='w-full'> <SelectTrigger className='w-full bg-background h-8'>
<SelectValue placeholder={placeholder} /> <SelectValue placeholder={placeholder} />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
@ -85,9 +85,7 @@ export function SelectField<TFormValues extends FieldValues>({
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription <FormDescription className={cn("text-xs truncate", !description && "invisible")}>
className={cn("text-xs text-muted-foreground", !description && "invisible")}
>
{description || "\u00A0"} {description || "\u00A0"}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />

View File

@ -2,6 +2,7 @@
import { import {
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -58,12 +59,17 @@ export function TextAreaField<TFormValues extends FieldValues>({
</div> </div>
)} )}
<FormControl> <FormControl>
<Textarea disabled={isDisabled} placeholder={placeholder} {...field} /> <Textarea
disabled={isDisabled}
placeholder={placeholder}
className={"bg-background"}
{...field}
/>
</FormControl> </FormControl>
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}> <FormDescription className={cn("text-xs truncate", !description && "invisible")}>
{description || "\u00A0"} {description || "\u00A0"}
</p> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@ -1,5 +1,6 @@
import { import {
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -393,7 +394,7 @@ export function TextField<TFormValues extends FieldValues>({
maxLength={maxLength} maxLength={maxLength}
{...rest} {...rest}
className={cn( className={cn(
"placeholder:font-normal placeholder:italic", "placeholder:font-normal placeholder:italic bg-background",
inputPadding, inputPadding,
invalid && "border-destructive focus-visible:ring-destructive", invalid && "border-destructive focus-visible:ring-destructive",
valid && showSuccessWhenValid && "border-green-500 focus-visible:ring-green-500", valid && showSuccessWhenValid && "border-green-500 focus-visible:ring-green-500",
@ -448,12 +449,12 @@ export function TextField<TFormValues extends FieldValues>({
</FormControl> </FormControl>
<div className='mt-1 flex items-start justify-between'> <div className='mt-1 flex items-start justify-between'>
<p <FormDescription
id={describedById} id={describedById}
className={cn("text-xs text-muted-foreground", !description && "invisible")} className={cn("text-xs truncate", !description && "invisible")}
> >
{description || "\u00A0"} {description || "\u00A0"}
</p> </FormDescription>
{showCounter && typeof maxLength === "number" && ( {showCounter && typeof maxLength === "number" && (
<p className='text-xs text-muted-foreground'> <p className='text-xs text-muted-foreground'>

View File

@ -4,7 +4,10 @@ import * as React from "react";
export const Fieldset = ({ className, children, ...props }: React.ComponentProps<"fieldset">) => ( export const Fieldset = ({ className, children, ...props }: React.ComponentProps<"fieldset">) => (
<fieldset <fieldset
data-slot='fieldset' data-slot='fieldset'
className={cn("*:data-[slot=text]:mt-1 [&>*+[data-slot=control]]:mt-6", className)} className={cn(
"*:data-[slot=text]:mt-1 [&>*+[data-slot=control]]:mt-6 bg-gray-50/50 rounded-xl p-6",
className
)}
{...props} {...props}
> >
{children} {children}
@ -12,7 +15,7 @@ export const Fieldset = ({ className, children, ...props }: React.ComponentProps
); );
export const FieldGroup = ({ className, children, ...props }: React.ComponentProps<"div">) => ( export const FieldGroup = ({ className, children, ...props }: React.ComponentProps<"div">) => (
<div data-slot='control' className={cn("space-y-8", className)} {...props}> <div data-slot='control' className={cn("space-y-6", className)} {...props}>
{children} {children}
</div> </div>
); );

View File

@ -1,11 +0,0 @@
import { cn } from "@repo/shadcn-ui/lib/utils";
export function FormContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot='form-content'
className={cn("grid grid-cols-1 gap-6 md:grid-cols-4 space-y-6", className)}
{...props}
/>
);
}

View File

@ -1,7 +1,6 @@
export * from "./DatePickerField.tsx"; export * from "./DatePickerField.tsx";
export * from "./DatePickerInputField.tsx"; export * from "./DatePickerInputField.tsx";
export * from "./fieldset.tsx"; export * from "./fieldset.tsx";
export * from "./form-content.tsx";
export * from "./multi-select-field.tsx"; export * from "./multi-select-field.tsx";
export * from "./SelectField.tsx"; export * from "./SelectField.tsx";
export * from "./TextAreaField.tsx"; export * from "./TextAreaField.tsx";

View File

@ -9,6 +9,7 @@ import {
CommandList, CommandList,
CommandSeparator, CommandSeparator,
FormControl, FormControl,
FormDescription,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
@ -438,12 +439,12 @@ export const MultiSelectFieldInner = React.forwardRef(
</FormControl> </FormControl>
<div className='mt-1 flex items-start justify-between'> <div className='mt-1 flex items-start justify-between'>
<p <FormDescription
id={describedById} id={describedById}
className={cn("text-xs text-muted-foreground", !description && "invisible")} className={cn("text-xs truncate", !description && "invisible")}
> >
{description || "\u00A0"} {description || "\u00A0"}
</p> </FormDescription>
</div> </div>
<FormMessage id={errorId} /> <FormMessage id={errorId} />

View File

@ -1,5 +1,5 @@
import { type VariantProps, cva } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority";
import { CheckIcon, ChevronDown, WandSparkles, XCircle } from "lucide-react"; import { CheckIcon, ChevronDown, WandSparkles, XCircleIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { import {
@ -219,7 +219,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
{...props} {...props}
onClick={handleTogglePopover} onClick={handleTogglePopover}
className={cn( className={cn(
"flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto", "flex w-full -mt-0.5 px-1 py-0.5 rounded-md border min-h-8 h-auto items-center justify-between bg-background hover:bg-inherit [&_svg]:pointer-events-auto",
className className
)} )}
> >
@ -260,7 +260,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
style={{ animationDuration: `${animation}s` }} style={{ animationDuration: `${animation}s` }}
> >
{`+ ${selectedValues.length - maxCount} more`} {`+ ${selectedValues.length - maxCount} more`}
<XCircle <XCircleIcon
className='ml-2 h-4 w-4 cursor-pointer' className='ml-2 h-4 w-4 cursor-pointer'
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();

View File

@ -1,33 +1,31 @@
"use client" "use client";
import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label";
import * as LabelPrimitive from "@radix-ui/react-label" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import * as React from "react";
import { import {
Controller, Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps, type ControllerProps,
type FieldPath, type FieldPath,
type FieldValues, type FieldValues,
} from "react-hook-form" FormProvider,
useFormContext,
useFormState,
} from "react-hook-form";
import { cn } from "@repo/shadcn-ui/lib/utils" import { Label } from "@repo/shadcn-ui/components/label";
import { Label } from "@repo/shadcn-ui/components/label" import { cn } from "@repo/shadcn-ui/lib/utils";
const Form = FormProvider const Form = FormProvider;
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
{} as FormFieldContextValue
)
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@ -39,21 +37,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext() const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name }) const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error("useFormField should be used within <FormField>");
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
@ -62,106 +60,93 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) { function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId() const id = React.useId();
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div <div data-slot='form-item' className={cn("grid gap-2", className)} {...props} />
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider> </FormItemContext.Provider>
) );
} }
function FormLabel({ function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
className, const { error, formItemId } = useFormField();
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return ( return (
<Label <Label
data-slot="form-label" data-slot='form-label'
data-error={!!error} data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)} className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
} }
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return ( return (
<Slot <Slot
data-slot="form-control" data-slot='form-control'
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) );
} }
function FormDescription({ className, ...props }: React.ComponentProps<"p">) { function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField();
return ( return (
<p <p
data-slot="form-description" data-slot='form-description'
id={formDescriptionId} id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
function FormMessage({ className, ...props }: React.ComponentProps<"p">) { function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : props.children const body = error ? String(error?.message ?? "") : props.children;
if (!body) { if (!body) {
return null return null;
} }
return ( return (
<p <p
data-slot="form-message" data-slot='form-message'
id={formMessageId} id={formMessageId}
className={cn("text-destructive text-sm", className)} className={cn("text-destructive text-sm", className)}
{...props} {...props}
> >
{body} {body}
</p> </p>
) );
} }
export { export {
useFormField,
Form, Form,
FormItem,
FormLabel,
FormControl, FormControl,
FormDescription, FormDescription,
FormMessage,
FormField, FormField,
} FormItem,
FormLabel,
FormMessage,
useFormField,
};

View File

@ -41,6 +41,9 @@ importers:
'@erp/customers': '@erp/customers':
specifier: workspace:* specifier: workspace:*
version: link:../../modules/customers version: link:../../modules/customers
'@erp/verifactu':
specifier: workspace:*
version: link:../../modules/verifactu
'@repo/rdx-logger': '@repo/rdx-logger':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/rdx-logger version: link:../../packages/rdx-logger