Uecko_ERP/packages/rdx-ui/src/components/form/DatePickerInputField.tsx

187 lines
6.4 KiB
TypeScript
Raw Normal View History

2025-07-09 17:56:15 +00:00
import {
Calendar,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Popover,
PopoverContent,
PopoverTrigger,
} from "@repo/shadcn-ui/components";
2025-07-10 10:45:59 +00:00
import { CalendarIcon, LockIcon, XIcon } from "lucide-react";
2025-07-09 17:56:15 +00:00
import { cn } from "@repo/shadcn-ui/lib/utils";
import { format, isValid, parse } from "date-fns";
import { useState } from "react";
import { Control, FieldPath, FieldValues } from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts";
type DatePickerInputFieldProps<TFormValues extends FieldValues> = {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label: string;
placeholder?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
className?: string;
formatDateFn?: (iso: string) => string;
parseDateFormat?: string; // e.g. "dd/MM/yyyy"
};
export function DatePickerInputField<TFormValues extends FieldValues>({
control,
name,
label,
placeholder,
description,
disabled = false,
required = false,
readOnly = false,
className,
formatDateFn = (iso) => format(new Date(iso), "dd/MM/yyyy"),
parseDateFormat = "dd/MM/yyyy",
}: DatePickerInputFieldProps<TFormValues>) {
const { t } = useTranslation();
const isDisabled = disabled;
const isReadOnly = readOnly && !disabled;
return (
<FormField
control={control}
name={name}
render={({ field }) => {
const [inputValue, setInputValue] = useState<string>(
field.value ? formatDateFn(field.value) : ""
);
const [inputError, setInputError] = useState<string | null>(null);
const handleInputChange = (value: string) => {
setInputValue(value);
2025-07-10 10:45:59 +00:00
setInputError(null);
2025-07-09 17:56:15 +00:00
};
const validateAndSetDate = () => {
2025-07-10 10:45:59 +00:00
const trimmed = inputValue.trim();
if (!trimmed) {
field.onChange(undefined);
setInputError(null);
return;
}
const parsed = parse(trimmed, parseDateFormat, new Date());
2025-07-09 17:56:15 +00:00
if (isValid(parsed)) {
field.onChange(parsed.toISOString());
setInputError(null);
} else {
2025-07-10 10:45:59 +00:00
setInputError(t("common.invalidDate") || "Fecha no válida");
2025-07-09 17:56:15 +00:00
}
};
return (
<FormItem className={cn("space-y-2", className)}>
<div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>}
</div>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<div className='relative'>
<input
type='text'
value={inputValue}
onChange={(e) => handleInputChange(e.target.value)}
onBlur={validateAndSetDate}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
validateAndSetDate();
}
}}
readOnly={isReadOnly}
disabled={isDisabled}
className={cn(
"w-full rounded-md border px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring",
isDisabled && "bg-muted text-muted-foreground cursor-not-allowed",
isReadOnly && "bg-muted text-foreground cursor-default",
!isDisabled && !isReadOnly && "bg-white text-foreground",
inputError && "border-destructive ring-destructive"
)}
placeholder={placeholder}
/>
2025-07-10 10:45:59 +00:00
<div className='absolute inset-y-0 right-2 flex items-center gap-2 pr-1'>
{!isReadOnly && !required && inputValue && (
<button
type='button'
onClick={() => {
setInputValue("");
field.onChange(undefined);
setInputError(null);
}}
aria-label={t("common.clearDate") || "Limpiar fecha"}
className='text-muted-foreground hover:text-foreground focus:outline-none'
>
<XIcon className='w-4 h-4' />
</button>
)}
2025-07-09 17:56:15 +00:00
{isReadOnly ? (
2025-07-10 10:45:59 +00:00
<LockIcon className='w-4 h-4 text-muted-foreground' />
2025-07-09 17:56:15 +00:00
) : (
2025-07-10 10:45:59 +00:00
<CalendarIcon className='w-4 h-4 text-muted-foreground' />
2025-07-09 17:56:15 +00:00
)}
</div>
</div>
</FormControl>
</PopoverTrigger>
{!isDisabled && !isReadOnly && (
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={field.value ? new Date(field.value) : undefined}
onSelect={(date) => {
if (date) {
const iso = date.toISOString();
field.onChange(iso);
setInputValue(formatDateFn(iso));
setInputError(null);
2025-07-10 10:45:59 +00:00
} else {
field.onChange(undefined);
setInputValue("");
2025-07-09 17:56:15 +00:00
}
}}
initialFocus
/>
</PopoverContent>
)}
</Popover>
2025-07-10 10:45:59 +00:00
{isReadOnly && (
<p className='text-xs text-muted-foreground italic mt-1 flex items-center gap-1'>
<LockIcon className='w-3 h-3' /> {t("common.readOnly") || "Solo lectura"}
</p>
)}
{(inputError || description) && (
<p
className={cn(
"text-xs mt-1",
inputError ? "text-destructive" : "text-muted-foreground"
)}
>
{inputError || description}
</p>
)}
2025-07-09 17:56:15 +00:00
<FormMessage />
</FormItem>
);
}}
/>
);
}