Clientes y facturas de cliente
This commit is contained in:
parent
8d0c0b88de
commit
7e700bdf22
@ -45,6 +45,7 @@
|
||||
"@erp/core": "workspace:*",
|
||||
"@erp/customer-invoices": "workspace:*",
|
||||
"@erp/customers": "workspace:*",
|
||||
"@erp/verifactu": "workspace:*",
|
||||
"@repo/rdx-logger": "workspace:*",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cls-rtracer": "^2.6.3",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
|
||||
import customersAPIModule from "@erp/customers/api";
|
||||
import verifactuAPIModule from "@erp/verifactu/api";
|
||||
//import verifactuAPIModule from "@erp/verifactu/api";
|
||||
|
||||
import { registerModule } from "./lib";
|
||||
|
||||
@ -8,5 +8,5 @@ export const registerModules = () => {
|
||||
//registerModule(authAPIModule);
|
||||
registerModule(customersAPIModule);
|
||||
registerModule(customerInvoicesAPIModule);
|
||||
registerModule(verifactuAPIModule);
|
||||
// registerModule(verifactuAPIModule);
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"required": "•"
|
||||
},
|
||||
"components": {
|
||||
|
||||
@ -47,7 +47,7 @@ export function TaxesMultiSelectField<TFormValues extends FieldValues>({
|
||||
render={({ field }) => (
|
||||
<FormItem className={cn("space-y-0", className)}>
|
||||
{label && (
|
||||
<div className='mb-1 flex justify-between gap-2'>
|
||||
<div className='mb-1 flex justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FormLabel htmlFor={name} className='m-0'>
|
||||
{label}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { XIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -34,7 +35,6 @@ export const CancelFormButton = ({
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
const ok = requestConfirm ? await requestConfirm() : true;
|
||||
console.log("ok => ", ok);
|
||||
if (!ok) return;
|
||||
|
||||
if (onCancel) {
|
||||
@ -43,7 +43,6 @@ export const CancelFormButton = ({
|
||||
}
|
||||
|
||||
if (to) {
|
||||
console.log("navego => ", to);
|
||||
navigate(to);
|
||||
}
|
||||
// si no hay ni onCancel ni to → no hace nada
|
||||
@ -60,6 +59,7 @@ export const CancelFormButton = ({
|
||||
aria-disabled={disabled}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<XIcon className='mr-2 h-3 w-3' />
|
||||
<span>{label ?? defaultLabel}</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
ArrowLeftIcon,
|
||||
CopyIcon,
|
||||
EyeIcon,
|
||||
MoreHorizontalIcon,
|
||||
RotateCcwIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { CancelFormButton, CancelFormButtonProps } from "./cancel-form-button";
|
||||
import { SubmitButtonProps, SubmitFormButton } from "./submit-form-button";
|
||||
|
||||
type Align = "start" | "center" | "end" | "between";
|
||||
|
||||
type GroupSubmitButtonProps = Omit<SubmitButtonProps, "isLoading" | "preventDoubleSubmit">;
|
||||
|
||||
export type FormCommitButtonGroupProps = {
|
||||
className?: string;
|
||||
align?: Align; // default "end"
|
||||
gap?: string; // default "gap-2"
|
||||
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 };
|
||||
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> = {
|
||||
@ -25,10 +55,33 @@ export const FormCommitButtonGroup = ({
|
||||
align = "end",
|
||||
gap = "gap-2",
|
||||
reverseOrderOnMobile = true,
|
||||
|
||||
isLoading,
|
||||
disabled = false,
|
||||
preventDoubleSubmit = true,
|
||||
|
||||
cancel,
|
||||
submit,
|
||||
|
||||
onReset,
|
||||
onDelete,
|
||||
onPreview,
|
||||
onDuplicate,
|
||||
onBack,
|
||||
}: FormCommitButtonGroupProps) => {
|
||||
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 (
|
||||
<div
|
||||
@ -40,8 +93,62 @@ export const FormCommitButtonGroup = ({
|
||||
className
|
||||
)}
|
||||
>
|
||||
{showCancel && <CancelFormButton {...cancel} />}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
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 { useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "../../../i18n.ts";
|
||||
@ -8,12 +9,15 @@ export type SubmitButtonProps = {
|
||||
formId?: string;
|
||||
isLoading?: boolean;
|
||||
label?: string;
|
||||
labelIsLoading?: string;
|
||||
|
||||
variant?: React.ComponentProps<typeof Button>["variant"];
|
||||
size?: React.ComponentProps<typeof Button>["size"];
|
||||
className?: string;
|
||||
|
||||
preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading
|
||||
hasChanges?: boolean;
|
||||
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
@ -24,10 +28,12 @@ export const SubmitFormButton = ({
|
||||
formId,
|
||||
isLoading,
|
||||
label,
|
||||
labelIsLoading,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
className,
|
||||
preventDoubleSubmit = true,
|
||||
hasChanges = false,
|
||||
onClick,
|
||||
disabled,
|
||||
children,
|
||||
@ -35,6 +41,7 @@ export const SubmitFormButton = ({
|
||||
}: SubmitButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultLabel = t ? t("common.save") : "Save";
|
||||
const defaultLabelIsLoading = t ? t("common.saving") : "Saving...";
|
||||
|
||||
// ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading
|
||||
let rhfIsSubmitting = false;
|
||||
@ -65,20 +72,30 @@ export const SubmitFormButton = ({
|
||||
form={formId}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={className}
|
||||
disabled={computedDisabled}
|
||||
aria-busy={busy}
|
||||
aria-disabled={computedDisabled}
|
||||
data-state={dataState}
|
||||
onClick={handleClick}
|
||||
data-testid={dataTestId}
|
||||
className={cn("min-w-[100px] font-medium", hasChanges && "ring-2 ring-primary/20", className)}
|
||||
>
|
||||
{children ? (
|
||||
children
|
||||
) : (
|
||||
<span className='inline-flex items-center gap-2'>
|
||||
{busy && <LoaderCircleIcon className='h-4 w-4 animate-spin' aria-hidden='true' />}
|
||||
<span>{label ?? defaultLabel}</span>
|
||||
{busy && (
|
||||
<>
|
||||
<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>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
import { Description, Field, FieldGroup, Fieldset, Legend } 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 { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
|
||||
import { useTranslation } from "../../i18n";
|
||||
@ -16,15 +11,12 @@ export const CustomerAdditionalConfigFields = () => {
|
||||
const { control } = useFormContext<CustomerFormData>();
|
||||
|
||||
return (
|
||||
<Card className='border-0 shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("form_groups.preferences.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.preferences.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='grid grid-cols-1 gap-8 lg:grid-cols-4 mb-12 '>
|
||||
<Fieldset>
|
||||
<Legend>{t("form_groups.preferences.title")}</Legend>
|
||||
<Description>{t("form_groups.preferences.description")}</Description>
|
||||
<FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
|
||||
<Field className='lg:col-span-2'>
|
||||
<SelectField
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
name='language_code'
|
||||
required
|
||||
@ -33,6 +25,8 @@ export const CustomerAdditionalConfigFields = () => {
|
||||
description={t("form_fields.language_code.description")}
|
||||
items={[...LANGUAGE_OPTIONS]}
|
||||
/>
|
||||
</Field>
|
||||
<Field className='lg:col-span-2'>
|
||||
<SelectField
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
@ -43,8 +37,8 @@ export const CustomerAdditionalConfigFields = () => {
|
||||
description={t("form_fields.currency_code.description")}
|
||||
items={[...CURRENCY_OPTIONS]}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,18 +1,12 @@
|
||||
import {
|
||||
Description,
|
||||
Field,
|
||||
FieldGroup,
|
||||
Fieldset,
|
||||
Legend,
|
||||
SelectField,
|
||||
TextField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { COUNTRY_OPTIONS } from "../../constants";
|
||||
import { useTranslation } from "../../i18n";
|
||||
@ -26,7 +20,7 @@ export const CustomerAddressFields = () => {
|
||||
<Fieldset>
|
||||
<Legend>{t("form_groups.address.title")}</Legend>
|
||||
<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
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
@ -60,77 +54,16 @@ export const CustomerAddressFields = () => {
|
||||
description={t("form_fields.postal_code.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
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 '>
|
||||
<Field className='lg:col-span-2 lg:col-start-1'>
|
||||
<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}
|
||||
name='province'
|
||||
label={t("form_fields.province.label")}
|
||||
placeholder={t("form_fields.province.placeholder")}
|
||||
description={t("form_fields.province.description")}
|
||||
/>
|
||||
</Field>
|
||||
<Field className='lg:col-span-2'>
|
||||
<SelectField
|
||||
control={control}
|
||||
name='country'
|
||||
@ -140,8 +73,8 @@ export const CustomerAddressFields = () => {
|
||||
description={t("form_fields.country.description")}
|
||||
items={[...COUNTRY_OPTIONS]}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
@ -35,7 +35,7 @@ export const CustomerBasicInfoFields = () => {
|
||||
<Fieldset>
|
||||
<Legend>Identificación</Legend>
|
||||
<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'>
|
||||
<TextField
|
||||
control={control}
|
||||
@ -59,7 +59,7 @@ export const CustomerBasicInfoFields = () => {
|
||||
field.onChange(value === "false" ? "false" : "true");
|
||||
}}
|
||||
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'>
|
||||
<FormControl>
|
||||
@ -106,16 +106,16 @@ export const CustomerBasicInfoFields = () => {
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
description={t("form_fields.reference.description")}
|
||||
/>
|
||||
<TaxesMultiSelectField
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
name='default_taxes'
|
||||
required
|
||||
label={t("form_fields.default_taxes.label")}
|
||||
placeholder={t("form_fields.default_taxes.placeholder")}
|
||||
description={t("form_fields.default_taxes.description")}
|
||||
/>
|
||||
|
||||
<Field className='lg:col-span-2'>
|
||||
<TaxesMultiSelectField
|
||||
control={control}
|
||||
name='default_taxes'
|
||||
required
|
||||
label={t("form_fields.default_taxes.label")}
|
||||
placeholder={t("form_fields.default_taxes.placeholder")}
|
||||
description={t("form_fields.default_taxes.description")}
|
||||
/>
|
||||
</Field>
|
||||
<TextAreaField
|
||||
className='lg:col-span-full'
|
||||
control={control}
|
||||
|
||||
@ -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 { ChevronDown, MailIcon, PhoneIcon, SmartphoneIcon } from "lucide-react";
|
||||
@ -15,7 +22,7 @@ export const CustomerContactFields = () => {
|
||||
<Fieldset>
|
||||
<Legend>{t("form_groups.contact_info.title")}</Legend>
|
||||
<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
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
@ -27,6 +34,7 @@ export const CustomerContactFields = () => {
|
||||
typePreset='email'
|
||||
required
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className='lg:col-span-2'
|
||||
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'>
|
||||
{t("common.more_details")}{" "}
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'>
|
||||
<TextField
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
name='website'
|
||||
label={t("form_fields.website.label")}
|
||||
placeholder={t("form_fields.website.placeholder")}
|
||||
description={t("form_fields.website.description")}
|
||||
/>
|
||||
<TextField
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
name='fax'
|
||||
label={t("form_fields.fax.label")}
|
||||
placeholder={t("form_fields.fax.placeholder")}
|
||||
description={t("form_fields.fax.description")}
|
||||
/>
|
||||
<FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
|
||||
<Field className='lg:col-span-2'>
|
||||
<TextField
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
name='website'
|
||||
label={t("form_fields.website.label")}
|
||||
placeholder={t("form_fields.website.placeholder")}
|
||||
description={t("form_fields.website.description")}
|
||||
/>
|
||||
</Field>
|
||||
<Field className='lg:col-span-2'>
|
||||
<TextField
|
||||
className='lg:col-span-2'
|
||||
control={control}
|
||||
name='fax'
|
||||
label={t("form_fields.fax.label")}
|
||||
placeholder={t("form_fields.fax.placeholder")}
|
||||
description={t("form_fields.fax.description")}
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
@ -22,7 +22,7 @@ export const CustomerEditForm = ({ formId, onSubmit, onError }: CustomerFormProp
|
||||
<div className='w-full xl:w-6/12'>
|
||||
<FormDebug />
|
||||
</div>
|
||||
<div className='w-full xl:grow'>
|
||||
<div className='w-full xl:grow space-y-6'>
|
||||
<CustomerBasicInfoFields />
|
||||
<CustomerContactFields />
|
||||
<CustomerAddressFields />
|
||||
|
||||
@ -54,6 +54,10 @@ export const CustomerCreate = () => {
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBreadcrumb />
|
||||
@ -69,14 +73,17 @@ export const CustomerCreate = () => {
|
||||
</p>
|
||||
</div>
|
||||
<FormCommitButtonGroup
|
||||
isLoading={isCreating}
|
||||
disabled={isCreating}
|
||||
cancel={{
|
||||
to: "/customers/list",
|
||||
disabled: isCreating,
|
||||
}}
|
||||
submit={{
|
||||
formId: "customer-create-form",
|
||||
disabled: isCreating,
|
||||
isLoading: isCreating,
|
||||
}}
|
||||
onBack={() => handleBack()}
|
||||
/>
|
||||
</div>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
|
||||
@ -67,6 +67,12 @@ export const CustomerUpdate = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleReset = () => form.reset(customerData ?? defaultCustomerFormData);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const handleError = (errors: FieldErrors<CustomerFormData>) => {
|
||||
console.error("Errores en el formulario:", errors);
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
@ -125,14 +131,18 @@ export const CustomerUpdate = () => {
|
||||
</p>
|
||||
</div>
|
||||
<FormCommitButtonGroup
|
||||
isLoading={isUpdating}
|
||||
disabled={isUpdating}
|
||||
cancel={{
|
||||
to: "/customers/list",
|
||||
disabled: isUpdating,
|
||||
}}
|
||||
submit={{
|
||||
formId: "customer-update-form",
|
||||
disabled: isUpdating,
|
||||
isLoading: isUpdating,
|
||||
}}
|
||||
onBack={() => handleBack()}
|
||||
onReset={() => handleReset()}
|
||||
/>
|
||||
</div>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
//export * from "./mappers";
|
||||
//export * from "./sequelize";
|
||||
export * from "./express";
|
||||
export * from "./sequelize";
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
Calendar,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@ -166,16 +167,9 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{(inputError || description) && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs mt-1",
|
||||
inputError ? "text-destructive" : "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{inputError || description}
|
||||
</p>
|
||||
)}
|
||||
<FormDescription className={cn("text-xs truncate", !description && "invisible")}>
|
||||
{description || "\u00A0"}
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@ -53,9 +54,9 @@ export function NumberField<TFormValues extends FieldValues>({
|
||||
<Input disabled={isDisabled} placeholder={placeholder} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
|
||||
<FormDescription className={cn("text-xs truncate", !description && "invisible")}>
|
||||
{description || "\u00A0"}
|
||||
</p>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@ -55,7 +55,7 @@ export function SelectField<TFormValues extends FieldValues>({
|
||||
render={({ field }) => (
|
||||
<FormItem className={cn("space-y-0", className)}>
|
||||
{label && (
|
||||
<div className='mb-1 flex justify-between gap-2'>
|
||||
<div className='mb-1 flex justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FormLabel htmlFor={name} className='m-0'>
|
||||
{label}
|
||||
@ -72,7 +72,7 @@ export function SelectField<TFormValues extends FieldValues>({
|
||||
)}
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={isDisabled}>
|
||||
<FormControl>
|
||||
<SelectTrigger className='w-full'>
|
||||
<SelectTrigger className='w-full bg-background h-8'>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
@ -85,9 +85,7 @@ export function SelectField<TFormValues extends FieldValues>({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormDescription
|
||||
className={cn("text-xs text-muted-foreground", !description && "invisible")}
|
||||
>
|
||||
<FormDescription className={cn("text-xs truncate", !description && "invisible")}>
|
||||
{description || "\u00A0"}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@ -58,12 +59,17 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
||||
</div>
|
||||
)}
|
||||
<FormControl>
|
||||
<Textarea disabled={isDisabled} placeholder={placeholder} {...field} />
|
||||
<Textarea
|
||||
disabled={isDisabled}
|
||||
placeholder={placeholder}
|
||||
className={"bg-background"}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
|
||||
<FormDescription className={cn("text-xs truncate", !description && "invisible")}>
|
||||
{description || "\u00A0"}
|
||||
</p>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@ -393,7 +394,7 @@ export function TextField<TFormValues extends FieldValues>({
|
||||
maxLength={maxLength}
|
||||
{...rest}
|
||||
className={cn(
|
||||
"placeholder:font-normal placeholder:italic",
|
||||
"placeholder:font-normal placeholder:italic bg-background",
|
||||
inputPadding,
|
||||
invalid && "border-destructive focus-visible:ring-destructive",
|
||||
valid && showSuccessWhenValid && "border-green-500 focus-visible:ring-green-500",
|
||||
@ -448,12 +449,12 @@ export function TextField<TFormValues extends FieldValues>({
|
||||
</FormControl>
|
||||
|
||||
<div className='mt-1 flex items-start justify-between'>
|
||||
<p
|
||||
<FormDescription
|
||||
id={describedById}
|
||||
className={cn("text-xs text-muted-foreground", !description && "invisible")}
|
||||
className={cn("text-xs truncate", !description && "invisible")}
|
||||
>
|
||||
{description || "\u00A0"}
|
||||
</p>
|
||||
</FormDescription>
|
||||
|
||||
{showCounter && typeof maxLength === "number" && (
|
||||
<p className='text-xs text-muted-foreground'>
|
||||
|
||||
@ -4,7 +4,10 @@ import * as React from "react";
|
||||
export const Fieldset = ({ className, children, ...props }: React.ComponentProps<"fieldset">) => (
|
||||
<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}
|
||||
>
|
||||
{children}
|
||||
@ -12,7 +15,7 @@ export const Fieldset = ({ className, children, ...props }: React.ComponentProps
|
||||
);
|
||||
|
||||
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}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
export * from "./DatePickerField.tsx";
|
||||
export * from "./DatePickerInputField.tsx";
|
||||
export * from "./fieldset.tsx";
|
||||
export * from "./form-content.tsx";
|
||||
export * from "./multi-select-field.tsx";
|
||||
export * from "./SelectField.tsx";
|
||||
export * from "./TextAreaField.tsx";
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
@ -438,12 +439,12 @@ export const MultiSelectFieldInner = React.forwardRef(
|
||||
</FormControl>
|
||||
|
||||
<div className='mt-1 flex items-start justify-between'>
|
||||
<p
|
||||
<FormDescription
|
||||
id={describedById}
|
||||
className={cn("text-xs text-muted-foreground", !description && "invisible")}
|
||||
className={cn("text-xs truncate", !description && "invisible")}
|
||||
>
|
||||
{description || "\u00A0"}
|
||||
</p>
|
||||
</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormMessage id={errorId} />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
@ -219,7 +219,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
{...props}
|
||||
onClick={handleTogglePopover}
|
||||
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
|
||||
)}
|
||||
>
|
||||
@ -260,7 +260,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
style={{ animationDuration: `${animation}s` }}
|
||||
>
|
||||
{`+ ${selectedValues.length - maxCount} more`}
|
||||
<XCircle
|
||||
<XCircleIcon
|
||||
className='ml-2 h-4 w-4 cursor-pointer'
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
@ -1,33 +1,31 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import * as React from "react";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
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<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
@ -39,21 +37,21 @@ const FormField = <
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState } = useFormContext()
|
||||
const formState = useFormState({ name: fieldContext.name })
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState } = useFormContext();
|
||||
const formState = useFormState({ name: fieldContext.name });
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
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 {
|
||||
id,
|
||||
@ -62,106 +60,93 @@ const useFormField = () => {
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId()
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
<div data-slot='form-item' className={cn("grid gap-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField()
|
||||
function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-slot='form-label'
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
data-slot='form-control'
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField()
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
data-slot='form-description'
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : props.children
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message ?? "") : props.children;
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
data-slot='form-message'
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useFormField,
|
||||
};
|
||||
|
||||
@ -41,6 +41,9 @@ importers:
|
||||
'@erp/customers':
|
||||
specifier: workspace:*
|
||||
version: link:../../modules/customers
|
||||
'@erp/verifactu':
|
||||
specifier: workspace:*
|
||||
version: link:../../modules/verifactu
|
||||
'@repo/rdx-logger':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/rdx-logger
|
||||
|
||||
Loading…
Reference in New Issue
Block a user