Facturas de cliente
This commit is contained in:
parent
3da0d3858f
commit
dc49094f00
@ -1 +1 @@
|
||||
export * from "./customer-invoice-edit-form";
|
||||
export * from "./invoice-edit-form";
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { FieldErrors, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormDebug } from '@erp/core/components';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { InvoiceFormData } from "../../schemas";
|
||||
import { InvoiceBasicInfoFields } from "./invoice-basic-info-fields";
|
||||
@ -14,7 +15,7 @@ interface CustomerInvoiceFormProps {
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const CustomerInvoiceEditForm = ({
|
||||
export const InvoiceEditForm = ({
|
||||
formId,
|
||||
onSubmit,
|
||||
onError,
|
||||
@ -27,18 +28,18 @@ export const CustomerInvoiceEditForm = ({
|
||||
<section className={cn("space-y-6", className)}>
|
||||
<div className="w-full border p-6 bg-background">
|
||||
<InvoiceBasicInfoFields className="flex flex-col" />
|
||||
<InvoiceRecipient className='lg:col-span-1 border p-6 bg-background' />
|
||||
</div>
|
||||
|
||||
<div className='w-full grid grid-cols-1 lg:grid-cols-4 gap-6'>
|
||||
|
||||
<InvoiceItems className="col-start-1 lg:col-span-3 border p-6 bg-background -p-6" />
|
||||
<InvoiceRecipient className='lg:col-span-1 border p-6 bg-background' />
|
||||
|
||||
<div className='w-full gap-6'>
|
||||
<InvoiceItems className="border p-6 bg-background -p-6" />
|
||||
</div>
|
||||
<div className="w-full border p-6 bg-background">
|
||||
<InvoiceTotals />
|
||||
</div>
|
||||
|
||||
<div className="w-full border p-6 bg-background">
|
||||
<FormDebug />
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</form>
|
||||
@ -1,7 +1,7 @@
|
||||
import { Button, Checkbox, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { Control, Controller, FieldValues } from "react-hook-form";
|
||||
import { useInvoiceContext } from '../../../context';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
||||
@ -10,9 +10,9 @@ import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
||||
import { PercentageInputField } from './percentage-input-field';
|
||||
import { QuantityInputField } from './quantity-input-field';
|
||||
|
||||
export type ItemRowProps = {
|
||||
export type ItemRowProps<TFieldValues extends FieldValues = FieldValues> = {
|
||||
|
||||
control: Control,
|
||||
control: Control<TFieldValues>,
|
||||
rowIndex: number;
|
||||
isSelected: boolean;
|
||||
isFirst: boolean;
|
||||
@ -26,8 +26,7 @@ export type ItemRowProps = {
|
||||
}
|
||||
|
||||
|
||||
export const ItemRow = ({
|
||||
|
||||
export const ItemRow = <TFieldValues extends FieldValues = FieldValues>({
|
||||
control,
|
||||
rowIndex,
|
||||
isSelected,
|
||||
@ -38,7 +37,7 @@ export const ItemRow = ({
|
||||
onDuplicate,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onRemove, }: ItemRowProps) => {
|
||||
onRemove, }: ItemRowProps<TFieldValues>) => {
|
||||
const { t } = useTranslation();
|
||||
const { currency_code, language_code } = useInvoiceContext();
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import * as z from "zod";
|
||||
import { CustomerModalSelector } from "@erp/customers/components";
|
||||
|
||||
import { DevTool } from "@hookform/devtools";
|
||||
import { DatePickerInputField, TextAreaField, TextField } from "@repo/rdx-ui/components";
|
||||
import { TextAreaField, TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
|
||||
@ -10,7 +10,7 @@ import { useMemo } from 'react';
|
||||
import { FieldErrors, FormProvider } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CustomerInvoiceEditForm,
|
||||
InvoiceEditForm,
|
||||
PageHeader
|
||||
} from "../../components";
|
||||
import { useInvoiceContext } from '../../context';
|
||||
@ -104,7 +104,7 @@ export const InvoiceUpdateComp = ({
|
||||
<AppContent>
|
||||
<FormProvider {...form}>
|
||||
|
||||
<CustomerInvoiceEditForm
|
||||
<InvoiceEditForm
|
||||
formId="invoice-update-form"
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { ModuleClientParams } from "@erp/core/client";
|
||||
import { lazy } from "react";
|
||||
import { Outlet, RouteObject } from "react-router-dom";
|
||||
import { CustomerUpdatePage } from "./pages/update";
|
||||
|
||||
// Lazy load components
|
||||
const CustomersLayout = lazy(() =>
|
||||
@ -22,11 +21,11 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
</CustomersLayout>
|
||||
),
|
||||
children: [
|
||||
{ path: "", index: true, element: <CustomersList /> }, // index
|
||||
/*{ path: "", index: true, element: <CustomersList /> }, // index
|
||||
{ path: "list", element: <CustomersList /> },
|
||||
{ path: "create", element: <CustomerAdd /> },
|
||||
{ path: ":id", element: <CustomerView /> },
|
||||
{ path: ":id/edit", element: <CustomerUpdatePage /> },
|
||||
{ path: ":id/edit", element: <CustomerUpdatePage /> },*/
|
||||
|
||||
//
|
||||
/*{ path: "create", element: <CustomersList /> },
|
||||
|
||||
@ -48,7 +48,7 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
||||
name={name}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<Field data-invalid={fieldState.invalid} orientation={orientation} className={className}>
|
||||
<Field data-invalid={fieldState.invalid} orientation={orientation} className={cn("gap-1", className)}>
|
||||
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>{label}</FieldLabel>}
|
||||
|
||||
<Textarea
|
||||
|
||||
@ -56,7 +56,7 @@ export function TextField<TFormValues extends FieldValues>({
|
||||
name={name}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<Field data-invalid={fieldState.invalid} orientation={orientation} className={className}>
|
||||
<Field data-invalid={fieldState.invalid} orientation={orientation} className={cn("gap-1", className)}>
|
||||
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>{label}</FieldLabel>}
|
||||
|
||||
<Input
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
import { Button, Input } from '@repo/shadcn-ui/components';
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
// DateInputField.tsx
|
||||
import { CalendarIcon, LockIcon, XIcon } from "lucide-react";
|
||||
|
||||
export function DateInputField({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
onBlurConfirm,
|
||||
onClear,
|
||||
placeholder,
|
||||
disabled,
|
||||
readOnly,
|
||||
required,
|
||||
hasError,
|
||||
describedBy,
|
||||
onOpenRequest,
|
||||
triggerButton, // ← PopoverTrigger se inyecta aquí
|
||||
}: {
|
||||
id: string;
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
onBlurConfirm: () => void; // valida+normaliza en blur/Enter
|
||||
onClear: () => void;
|
||||
placeholder?: string;
|
||||
disabled: boolean;
|
||||
readOnly: boolean;
|
||||
required: boolean;
|
||||
hasError: boolean;
|
||||
describedBy?: string;
|
||||
onOpenRequest?: () => void;
|
||||
triggerButton?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
id={id}
|
||||
type="text"
|
||||
pattern="\d{2}/\d{2}/\d{4}"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onBlur={onBlurConfirm}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onBlurConfirm();
|
||||
}
|
||||
if ((e.altKey || e.metaKey) && e.key === "ArrowDown") {
|
||||
onOpenRequest?.();
|
||||
}
|
||||
}}
|
||||
readOnly={readOnly}
|
||||
disabled={disabled}
|
||||
aria-invalid={hasError || undefined}
|
||||
aria-describedby={describedBy}
|
||||
className={cn(
|
||||
"text-ellipsis pr-12",
|
||||
disabled && "bg-muted text-muted-foreground cursor-not-allowed",
|
||||
readOnly && "bg-muted text-foreground cursor-default",
|
||||
!disabled && !readOnly && "bg-white text-foreground",
|
||||
hasError && "border-destructive ring-destructive"
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-2 flex items-center gap-2 pr-1">
|
||||
{!readOnly && !required && value && (
|
||||
<Button
|
||||
variant={'link'}
|
||||
type='button'
|
||||
size={"icon-sm"}
|
||||
onClick={onClear}
|
||||
aria-label="Clear date"
|
||||
className="text-muted-foreground hover:text-destructive transition cursor-pointer -mr-3"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
{readOnly ? (
|
||||
<LockIcon className="size-4" />
|
||||
) : (
|
||||
triggerButton ?? <CalendarIcon className="size-4" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,30 +1,22 @@
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
FormControl,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { CalendarIcon, LockIcon, XIcon } from "lucide-react";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { format, isValid, parse } from "date-fns";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ControllerFieldState, ControllerRenderProps, FieldValues, Path, UseFormStateReturn } from "react-hook-form";
|
||||
import { useTranslation } from "../../../locales/i18n.ts";
|
||||
import { DateInputField } from './date-input-field.tsx';
|
||||
import { DatePopoverCalendar } from './date-popover-calendar.tsx';
|
||||
|
||||
|
||||
export type SUICalendarProps = Omit<React.ComponentProps<
|
||||
@ -35,8 +27,6 @@ type DatePickerInputCompProps<TFormValues extends FieldValues = FieldValues> = S
|
||||
fieldState: ControllerFieldState;
|
||||
formState: UseFormStateReturn<TFormValues>;
|
||||
|
||||
htmlFor: string,
|
||||
|
||||
displayDateFormat: string; // e.g. "dd/MM/yyyy"
|
||||
parseDateFormat: string; // e.g. "yyyy/MM/dd"
|
||||
|
||||
@ -54,13 +44,11 @@ type DatePickerInputCompProps<TFormValues extends FieldValues = FieldValues> = S
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function DatePickerInputComp<TFormValues>({
|
||||
export function DatePickerInputComp<TFormValues extends FieldValues = FieldValues>({
|
||||
field,
|
||||
fieldState,
|
||||
formState,
|
||||
|
||||
htmlFor,
|
||||
|
||||
parseDateFormat,
|
||||
displayDateFormat,
|
||||
|
||||
@ -78,172 +66,138 @@ export function DatePickerInputComp<TFormValues>({
|
||||
...calendarProps
|
||||
}: DatePickerInputCompProps<TFormValues>) {
|
||||
const { t } = useTranslation();
|
||||
const isDisabled = disabled;
|
||||
const isReadOnly = readOnly && !disabled;
|
||||
const [open, setOpen] = useState(false);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
const [open, setOpen] = useState(false); // Popover
|
||||
const [displayValue, setDisplayValue] = useState<string>("");
|
||||
const parsedDate = useMemo(() => {
|
||||
if (!field.value) return null;
|
||||
const d = parse(field.value, parseDateFormat, new Date());
|
||||
return isValid(d) ? d : null;
|
||||
}, [field.value, parseDateFormat]);
|
||||
|
||||
// Sync cuando RHF actualiza el valor externamente
|
||||
useEffect(() => {
|
||||
if (field.value) {
|
||||
// field.value ya viene en formato parseDateFormat
|
||||
const parsed = parse(field.value, parseDateFormat, new Date());
|
||||
if (isValid(parsed)) {
|
||||
setDisplayValue(format(parsed, displayDateFormat));
|
||||
}
|
||||
} else {
|
||||
setDisplayValue("");
|
||||
}
|
||||
}, [field.value, parseDateFormat, displayDateFormat]);
|
||||
setDisplayValue(parsedDate ? format(parsedDate, displayDateFormat) : "");
|
||||
}, [parsedDate, displayDateFormat]);
|
||||
|
||||
const [inputError, setInputError] = useState<string | null>(null);
|
||||
const handleClear = useCallback(() => {
|
||||
field.onChange("");
|
||||
setDisplayValue("");
|
||||
setLocalError(null);
|
||||
}, [field]);
|
||||
|
||||
const handleDisplayValueChange = (value: string) => {
|
||||
setDisplayValue(value);
|
||||
setInputError(null);
|
||||
};
|
||||
|
||||
const handleClearDate = () => {
|
||||
handleDisplayValueChange("");
|
||||
}
|
||||
|
||||
const validateAndSetDate = () => {
|
||||
const validateAndSet = useCallback(() => {
|
||||
const trimmed = displayValue.trim();
|
||||
if (!trimmed) {
|
||||
field.onChange(""); // guardar vacío en el form
|
||||
setInputError(null);
|
||||
handleClear();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parse(trimmed, displayDateFormat, new Date());
|
||||
if (isValid(parsed)) {
|
||||
// Guardar en form como string con parseDateFormat
|
||||
const newDateStr = format(parsed, parseDateFormat);
|
||||
field.onChange(newDateStr);
|
||||
// Asegurar displayValue consistente
|
||||
handleDisplayValueChange(newDateStr);
|
||||
const d = parse(trimmed, displayDateFormat, new Date());
|
||||
if (isValid(d)) {
|
||||
field.onChange(format(d, parseDateFormat));
|
||||
setDisplayValue(format(d, displayDateFormat));
|
||||
setLocalError(null);
|
||||
} else {
|
||||
setInputError(t("components.date_picker_input_field.invalid_date"));
|
||||
setLocalError(t("components.date_picker_input_field.invalid_date"));
|
||||
}
|
||||
};
|
||||
}, [displayValue, displayDateFormat, parseDateFormat, field, t, handleClear]);
|
||||
|
||||
const hasError = Boolean(localError || fieldState.error);
|
||||
const describedById = description ? `${field.name}-desc` : undefined;
|
||||
const errorId = hasError ? `${field.name}-err` : undefined;
|
||||
const popoverId = `${field.name}-popover`;
|
||||
|
||||
return (
|
||||
<Field data-invalid={invalid} orientation={orientation} className={className}>
|
||||
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={htmlFor}>{label}</FieldLabel>}
|
||||
<Field data-invalid={invalid} orientation={orientation} className={cn("gap-1", className)}>
|
||||
<div className="flex justify-between gap-2 overflow-hidden">
|
||||
<div className="flex items-center gap-2 flex-nowrap">
|
||||
<FieldLabel htmlFor={field.name} className={cn("m-0 text-xs text-muted-foreground text-nowrap text-ellipsis", disabled && "text-muted-foreground")}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{required && <span className="text-xs text-destructive">{t("common.required")}</span>}
|
||||
</div>
|
||||
{fieldState.isDirty && <span className="text-[10px] text-primary text-ellipsis">{t("common.modified")}</span>}
|
||||
</div>
|
||||
|
||||
|
||||
<Popover modal={true} open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
type='text'
|
||||
value={displayValue}
|
||||
onChange={(e) => handleDisplayValueChange(e.target.value)}
|
||||
onBlur={() => { if (!open) 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 placeholder:font-normal placeholder:italic",
|
||||
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}
|
||||
/>
|
||||
<div className='absolute inset-y-0 right-2 flex items-center gap-2 pr-1'>
|
||||
{!isReadOnly && !required && displayValue && (
|
||||
<button
|
||||
|
||||
<DateInputField
|
||||
id={field.name}
|
||||
value={displayValue}
|
||||
onChange={(v) => {
|
||||
setDisplayValue(v);
|
||||
setLocalError(null);
|
||||
}}
|
||||
onBlurConfirm={validateAndSet}
|
||||
onClear={handleClear}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
hasError={hasError}
|
||||
describedBy={[describedById, errorId].filter(Boolean).join(" ") || undefined}
|
||||
onOpenRequest={() => setOpen(true)}
|
||||
triggerButton={
|
||||
!readOnly && !disabled && (
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={'link'}
|
||||
type='button'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
field.onChange(""); // limpiar valor real en el form
|
||||
setDisplayValue(""); // limpiar input visible
|
||||
setInputError(null); // limpiar error
|
||||
}} aria-label={t("common.clear_date")}
|
||||
className='text-muted-foreground hover:text-foreground focus:outline-none'
|
||||
size={"icon-sm"}
|
||||
aria-label={t("components.date_picker_input_field.open_calendar")}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
aria-controls={popoverId}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="text-muted-foreground transition cursor-pointer -mr-3"
|
||||
>
|
||||
<XIcon className='size-4 hover:text-destructive' />
|
||||
</button>
|
||||
)}
|
||||
{isReadOnly ? (
|
||||
<LockIcon className='size-4 text-muted-foreground' />
|
||||
) : (
|
||||
<CalendarIcon className='size-4 text-muted-foreground hover:text-primary hover:cursor-pointer' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<CalendarIcon className="size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
</PopoverTrigger>
|
||||
|
||||
{!isDisabled && !isReadOnly && (
|
||||
<PopoverContent className='w-auto p-0'>
|
||||
<Card className='border-none shadow-none py-6 gap-3'>
|
||||
<CardHeader className="border-b px-3 [.border-b]:pb-3">
|
||||
<CardTitle>{label}</CardTitle>
|
||||
<CardDescription>{description || "\u00A0"}</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const today = format(new Date(), parseDateFormat);
|
||||
field.onChange(today);
|
||||
handleDisplayValueChange(today);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("components.date_picker_input_field.today")}
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className='px-0'>
|
||||
<Calendar
|
||||
defaultMonth={field.value ? new Date(field.value) : undefined}
|
||||
{...calendarProps}
|
||||
mode='single'
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => {
|
||||
const newDateStr = date ? format(date, parseDateFormat) : "";
|
||||
field.onChange(newDateStr);
|
||||
handleDisplayValueChange(newDateStr);
|
||||
setOpen(false);
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className='mx-auto'>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("components.date_picker_input_field.close")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
{!disabled && !readOnly && (
|
||||
<DatePopoverCalendar
|
||||
contentId={popoverId}
|
||||
label={label}
|
||||
description={description}
|
||||
parsedDate={parsedDate}
|
||||
onSelect={(date) => {
|
||||
if (!date) return;
|
||||
field.onChange(format(date, parseDateFormat));
|
||||
setDisplayValue(format(date, displayDateFormat));
|
||||
setOpen(false);
|
||||
}}
|
||||
onToday={() => {
|
||||
const today = new Date();
|
||||
field.onChange(format(today, parseDateFormat));
|
||||
setDisplayValue(format(today, displayDateFormat));
|
||||
setOpen(false);
|
||||
}}
|
||||
onClose={() => setOpen(false)}
|
||||
{...calendarProps}
|
||||
/>
|
||||
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
{isReadOnly && (
|
||||
<p className='text-xs text-muted-foreground italic mt-1 flex items-center gap-1'>
|
||||
<LockIcon className='w-3 h-3' /> {t("common.read_only") || "Solo lectura"}
|
||||
</p>
|
||||
{false && (
|
||||
<div className='mt-1 flex items-start justify-between'>
|
||||
<FieldDescription
|
||||
id={describedById}
|
||||
className={cn("text-xs truncate", !description && "invisible")}
|
||||
>
|
||||
{description || "\u00A0"}
|
||||
</FieldDescription>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{false && <FieldDescription className='text-xs'>{description || "\u00A0"}</FieldDescription>}
|
||||
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
|
||||
@ -14,7 +14,7 @@ type DatePickerInputFieldProps<TFormValues extends FieldValues> = SUICalendarPro
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
|
||||
displayDateFormat?: string; // e.g. "dd/MM/yyyy"
|
||||
displayDateFormat?: string; // e.g. "dd-MM-yyyy"
|
||||
parseDateFormat?: string; // e.g. "yyyy-MM-dd"
|
||||
orientation?: "vertical" | "horizontal" | "responsive",
|
||||
};
|
||||
@ -22,7 +22,7 @@ type DatePickerInputFieldProps<TFormValues extends FieldValues> = SUICalendarPro
|
||||
export function DatePickerInputField<TFormValues extends FieldValues>({
|
||||
control,
|
||||
name,
|
||||
displayDateFormat = "dd-MM-y1qyyy",
|
||||
displayDateFormat = "dd-MM-yyyy",
|
||||
parseDateFormat = "yyyy-MM-dd",
|
||||
...props
|
||||
}: DatePickerInputFieldProps<TFormValues>) {
|
||||
@ -35,8 +35,6 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
||||
field={field}
|
||||
fieldState={fieldState}
|
||||
formState={formState}
|
||||
htmlFor={name}
|
||||
|
||||
|
||||
displayDateFormat={displayDateFormat}
|
||||
parseDateFormat={parseDateFormat}
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
// DatePopoverCalendar.tsx
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
PopoverContent,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { useTranslation } from "../../../locales/i18n.ts";
|
||||
|
||||
export function DatePopoverCalendar({
|
||||
contentId,
|
||||
label,
|
||||
description,
|
||||
parsedDate,
|
||||
onSelect,
|
||||
onToday,
|
||||
onClose,
|
||||
...calendarProps
|
||||
}: {
|
||||
contentId: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
parsedDate: Date | null;
|
||||
onSelect: (date: Date | undefined) => void;
|
||||
onToday: () => void;
|
||||
onClose: () => void;
|
||||
} & Omit<React.ComponentProps<typeof Calendar>, "mode" | "selected" | "onSelect">) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<PopoverContent id={contentId} className="w-auto p-0">
|
||||
<Card className="border-none shadow-none">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>{label}</CardTitle>
|
||||
<CardDescription>{description || "\u00A0"}</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onToday}
|
||||
>
|
||||
{t("components.date_picker_input_field.today")}
|
||||
</Button>
|
||||
</CardAction>
|
||||
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className='px-0'>
|
||||
<Calendar
|
||||
selected={parsedDate ?? undefined}
|
||||
defaultMonth={parsedDate ?? undefined}
|
||||
onSelect={onSelect}
|
||||
mode='single'
|
||||
numberOfMonths={2}
|
||||
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className='mx-auto'>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onClose}>
|
||||
{t("components.date_picker_input_field.close")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
</Card>
|
||||
</PopoverContent >
|
||||
);
|
||||
}
|
||||
@ -1069,8 +1069,8 @@ importers:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.0
|
||||
react-day-picker:
|
||||
specifier: 8.10.1
|
||||
version: 8.10.1(date-fns@4.1.0)(react@19.2.0)
|
||||
specifier: 9.11.1
|
||||
version: 9.11.1(react@19.2.0)
|
||||
react-dom:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.0(react@19.2.0)
|
||||
@ -1302,6 +1302,9 @@ packages:
|
||||
'@dabh/diagnostics@2.0.8':
|
||||
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
|
||||
|
||||
'@date-fns/tz@1.4.1':
|
||||
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1':
|
||||
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
|
||||
peerDependencies:
|
||||
@ -3607,6 +3610,9 @@ packages:
|
||||
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
date-fns-jalali@4.1.0-0:
|
||||
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
@ -5304,11 +5310,11 @@ packages:
|
||||
react: '>= 17.0.0'
|
||||
styled-components: '>= 5.0.0'
|
||||
|
||||
react-day-picker@8.10.1:
|
||||
resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==}
|
||||
react-day-picker@9.11.1:
|
||||
resolution: {integrity: sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
date-fns: ^2.28.0 || ^3.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react: '>=16.8.0'
|
||||
|
||||
react-dom@19.2.0:
|
||||
resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
|
||||
@ -6567,6 +6573,8 @@ snapshots:
|
||||
enabled: 2.0.0
|
||||
kuler: 2.0.0
|
||||
|
||||
'@date-fns/tz@1.4.1': {}
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1(react@19.2.0)':
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
@ -8897,6 +8905,8 @@ snapshots:
|
||||
|
||||
data-uri-to-buffer@6.0.2: {}
|
||||
|
||||
date-fns-jalali@4.1.0-0: {}
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
debug@2.6.9:
|
||||
@ -10687,9 +10697,11 @@ snapshots:
|
||||
react: 19.2.0
|
||||
styled-components: 6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
|
||||
react-day-picker@8.10.1(date-fns@4.1.0)(react@19.2.0):
|
||||
react-day-picker@9.11.1(react@19.2.0):
|
||||
dependencies:
|
||||
'@date-fns/tz': 1.4.1
|
||||
date-fns: 4.1.0
|
||||
date-fns-jalali: 4.1.0-0
|
||||
react: 19.2.0
|
||||
|
||||
react-dom@19.2.0(react@19.2.0):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user