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/customer-invoices": "workspace:*",
"@erp/customers": "workspace:*",
"@erp/verifactu": "workspace:*",
"@repo/rdx-logger": "workspace:*",
"bcrypt": "^5.1.1",
"cls-rtracer": "^2.6.3",

View File

@ -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);
};

View File

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

View File

@ -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}

View File

@ -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>
);

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 {
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>
);
};

View File

@ -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>

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 {
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>
);
};

View File

@ -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>
);
};

View File

@ -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}

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 { 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>

View File

@ -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 />

View File

@ -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) */}

View File

@ -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) */}

View File

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

View File

@ -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>

View File

@ -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>
)}

View File

@ -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 />

View File

@ -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>
)}

View File

@ -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'>

View File

@ -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>
);

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 "./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";

View File

@ -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} />

View File

@ -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();

View File

@ -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,
};

View File

@ -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