Pruebas
This commit is contained in:
parent
bd209374bc
commit
b88632d395
@ -1,5 +1,5 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export const ProformaLayout = ({ children }: PropsWithChildren) => {
|
||||
return <div>{children}</div>;
|
||||
return <div className="overflow-y-scroll">{children}</div>;
|
||||
};
|
||||
|
||||
@ -1,2 +1,5 @@
|
||||
export * from "./proforma-form-field-shell";
|
||||
export * from "./proforma-header-fields-card";
|
||||
export * from "./proforma-update-editor";
|
||||
export * from "./proforma-header-form-grid";
|
||||
export * from "./proforma-section-card";
|
||||
export * from "./proforma-update-editor-form";
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
import { DatePickerInputField, TextField } from "@repo/rdx-ui/components";
|
||||
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "@repo/shadcn-ui/components";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
|
||||
export const ProformaBasicInfoFields = (props: ComponentProps<"fieldset">) => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<ProformaFormData>();
|
||||
|
||||
return (
|
||||
<FieldSet {...props}>
|
||||
<FieldLegend className="hidden text-foreground" variant="label">
|
||||
{t("form_groups.basic_info.title")}
|
||||
</FieldLegend>
|
||||
<FieldDescription className="hidden">
|
||||
{t("form_groups.basic_info.description")}
|
||||
</FieldDescription>
|
||||
|
||||
<FieldGroup className="flex flex-row flex-wrap gap-6 xl:flex-nowrap">
|
||||
<DatePickerInputField
|
||||
className="min-w-44 flex-1 sm:max-w-44"
|
||||
control={control}
|
||||
description={t("form_fields.invoice_date.description")}
|
||||
label={t("form_fields.invoice_date.label")}
|
||||
name="invoice_date"
|
||||
numberOfMonths={2}
|
||||
placeholder={t("form_fields.invoice_date.placeholder")}
|
||||
required
|
||||
/>
|
||||
|
||||
<DatePickerInputField
|
||||
className="min-w-44 flex-1 sm:max-w-44"
|
||||
control={control}
|
||||
description={t("form_fields.operation_date.description")}
|
||||
label={t("form_fields.operation_date.label")}
|
||||
name="operation_date"
|
||||
numberOfMonths={2}
|
||||
placeholder={t("form_fields.operation_date.placeholder")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="min-w-16 flex-1 sm:max-w-16"
|
||||
control={control}
|
||||
description={t("form_fields.series.description")}
|
||||
label={t("form_fields.series.label")}
|
||||
name="series"
|
||||
placeholder={t("form_fields.series.placeholder")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="min-w-32 flex-1 sm:max-w-44"
|
||||
control={control}
|
||||
description={t("form_fields.reference.description")}
|
||||
label={t("form_fields.reference.label")}
|
||||
maxLength={256}
|
||||
name="reference"
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="min-w-32 flex-1 xs:max-w-full"
|
||||
control={control}
|
||||
description={t("form_fields.description.description")}
|
||||
label={t("form_fields.description.label")}
|
||||
maxLength={256}
|
||||
name="description"
|
||||
placeholder={t("form_fields.description.placeholder")}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
// update/ui/blocks/proforma-form-field-shell.tsx
|
||||
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type ProformaFieldSpan = "xs" | "sm" | "md" | "lg" | "full";
|
||||
|
||||
const fieldSpanClasses: Record<ProformaFieldSpan, string> = {
|
||||
xs: "md:col-span-2",
|
||||
sm: "md:col-span-3",
|
||||
md: "md:col-span-4",
|
||||
lg: "md:col-span-6",
|
||||
full: "md:col-span-12",
|
||||
};
|
||||
|
||||
interface ProformaFormFieldShellProps {
|
||||
label: ReactNode;
|
||||
htmlFor: string;
|
||||
span?: ProformaFieldSpan;
|
||||
required?: boolean;
|
||||
description?: ReactNode;
|
||||
error?: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ProformaFormFieldShell = ({
|
||||
label,
|
||||
htmlFor,
|
||||
span = "md",
|
||||
required = false,
|
||||
description,
|
||||
error,
|
||||
children,
|
||||
className,
|
||||
}: ProformaFormFieldShellProps) => {
|
||||
return (
|
||||
<div className={cn(fieldSpanClasses[span], "space-y-1.5", className)}>
|
||||
<label className="inline-flex items-center gap-1 text-sm font-medium" htmlFor={htmlFor}>
|
||||
<span>{label}</span>
|
||||
{required ? <span className="text-destructive">*</span> : null}
|
||||
</label>
|
||||
|
||||
{children}
|
||||
|
||||
{description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
|
||||
|
||||
{error ? (
|
||||
<p className="text-xs text-destructive" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,17 +1,83 @@
|
||||
import { DatePickerField, TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
Input,
|
||||
Textarea,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaUpdateForm } from "../../entities";
|
||||
|
||||
export const ProformaHeaderFieldsCard = () => {
|
||||
export const ProformaHeaderFieldsCard = (props: ComponentProps<"fieldset">) => {
|
||||
const { className, ...rest } = props;
|
||||
const { t } = useTranslation();
|
||||
//const { register, formState } = useFormContext<ProformaUpdateForm>();
|
||||
|
||||
return (
|
||||
<FieldSet {...rest}>
|
||||
<FieldLegend className="text-foreground" variant="label">
|
||||
{t("form_groups.basic_info.title")}
|
||||
</FieldLegend>
|
||||
<FieldDescription className="">{t("form_groups.basic_info.description")}</FieldDescription>
|
||||
|
||||
<FieldGroup className={className}>
|
||||
<TextField
|
||||
className="md:col-span-2"
|
||||
description={t("form_fields.series.description")}
|
||||
label={t("form_fields.series.label")}
|
||||
name="series"
|
||||
placeholder={t("form_fields.series.placeholder")}
|
||||
/>
|
||||
|
||||
<DatePickerField
|
||||
className="md:col-span-2"
|
||||
description={t("form_fields.invoice_date.description")}
|
||||
label={t("form_fields.invoice_date.label")}
|
||||
name="invoice_date"
|
||||
placeholder={t("form_fields.invoice_date.placeholder")}
|
||||
required
|
||||
/>
|
||||
|
||||
<DatePickerField
|
||||
className="md:col-span-2"
|
||||
description={t("form_fields.operation_date.description")}
|
||||
label={t("form_fields.operation_date.label")}
|
||||
name="operation_date"
|
||||
placeholder={t("form_fields.operation_date.placeholder")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="md:col-span-7"
|
||||
description={t("form_fields.reference.description")}
|
||||
label={t("form_fields.reference.label")}
|
||||
maxLength={256}
|
||||
name="reference"
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="md:col-span-12"
|
||||
description={t("form_fields.description.description")}
|
||||
label={t("form_fields.description.label")}
|
||||
maxLength={256}
|
||||
name="description"
|
||||
placeholder={t("form_fields.description.placeholder")}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
};
|
||||
|
||||
const ProformaHeaderFieldsCard2 = () => {
|
||||
const { t } = useTranslation();
|
||||
const { register, formState } = useFormContext<ProformaUpdateForm>();
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
// update/ui/blocks/proforma-header-form-grid.tsx
|
||||
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface ProformaHeaderFormGridProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ProformaHeaderFormGrid = ({ children, className }: ProformaHeaderFormGridProps) => {
|
||||
return <div className={cn("grid grid-cols-1 gap-4 md:grid-cols-12", className)}>{children}</div>;
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
// update/ui/blocks/proforma-section-card.tsx
|
||||
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface ProformaSectionCardProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ProformaSectionCard = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
}: ProformaSectionCardProps) => {
|
||||
return (
|
||||
<section className={cn("rounded-xl border bg-background p-4 md:p-6", className)}>
|
||||
<div className="mb-4 space-y-1 md:mb-6">
|
||||
<h2 className="text-base font-semibold tracking-tight">{title}</h2>
|
||||
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,74 @@
|
||||
// modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx
|
||||
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { Proforma } from "../../../shared/entities";
|
||||
import { ProformaUpdateHeaderEditor } from "../editors";
|
||||
|
||||
import { ProformaHeaderFieldsCard } from "./proforma-header-fields-card";
|
||||
|
||||
type ProformaUpdateEditorProps = {
|
||||
formId: string;
|
||||
proforma?: Proforma;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
||||
onReset: () => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ProformaUpdateEditorForm = ({
|
||||
formId,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
onReset,
|
||||
className,
|
||||
}: ProformaUpdateEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<form className="mx-auto w-full max-w-7xl px-4 py-6 md:px-6 xl:px-8" onSubmit={onSubmit}>
|
||||
<div className="space-y-6">
|
||||
<ProformaUpdateHeaderEditor
|
||||
currencyOptions={[]}
|
||||
customerOptions={[]}
|
||||
disabled={isSubmitting}
|
||||
paymentMethodOptions={[]}
|
||||
priceListOptions={[]}
|
||||
salesPersonOptions={[]}
|
||||
statusOptions={[]}
|
||||
warehouseOptions={[]}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">
|
||||
<Button type="button" variant="outline">
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button disabled={isSubmitting} type="submit">
|
||||
{isSubmitting ? t("common.saving") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
return (
|
||||
<form id={formId} noValidate onSubmit={onSubmit}>
|
||||
<section className={cn("rounded-xl border bg-background p-4 md:p-6", className)}>
|
||||
<ProformaHeaderFieldsCard className="grid grid-cols-1 gap-6 md:grid-cols-12" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
|
||||
<Button disabled={isSubmitting} onClick={onReset} type="button" variant="outline">
|
||||
{t("common.reset", "Restablecer")}
|
||||
</Button>
|
||||
|
||||
<Button disabled={isSubmitting} type="submit">
|
||||
{isSubmitting ? t("common.saving", "Guardando...") : t("common.save", "Guardar")}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@ -1,59 +0,0 @@
|
||||
// modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx
|
||||
|
||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { Proforma } from "../../../shared/entities";
|
||||
|
||||
import { ProformaHeaderFieldsCard } from "./proforma-header-fields-card";
|
||||
|
||||
type ProformaUpdateEditorProps = {
|
||||
formId: string;
|
||||
proforma?: Proforma;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
export const ProformaUpdateEditor = ({
|
||||
formId,
|
||||
proforma,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
onReset,
|
||||
}: ProformaUpdateEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<AppContent className="space-y-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl font-semibold">
|
||||
{t("proformas.update.page_title", "Editar proforma")}
|
||||
</h1>
|
||||
|
||||
{proforma?.reference ? (
|
||||
<p className="text-sm text-muted-foreground">{proforma.reference}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<BackHistoryButton />
|
||||
</div>
|
||||
|
||||
<form className="space-y-6" id={formId} onSubmit={onSubmit}>
|
||||
<ProformaHeaderFieldsCard />
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button disabled={isSubmitting} onClick={onReset} type="button" variant="outline">
|
||||
{t("common.reset", "Restablecer")}
|
||||
</Button>
|
||||
|
||||
<Button disabled={isSubmitting} type="submit">
|
||||
{isSubmitting ? t("common.saving", "Guardando...") : t("common.save", "Guardar")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</AppContent>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-update-header-editor";
|
||||
@ -0,0 +1,391 @@
|
||||
import {
|
||||
Input,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Textarea,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaUpdateForm } from "../../entities";
|
||||
import { ProformaFormFieldShell, ProformaHeaderFormGrid, ProformaSectionCard } from "../blocks";
|
||||
|
||||
interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ProformaUpdateHeaderEditorProps {
|
||||
statusOptions: SelectOption[];
|
||||
customerOptions: SelectOption[];
|
||||
currencyOptions: SelectOption[];
|
||||
paymentMethodOptions: SelectOption[];
|
||||
salesPersonOptions: SelectOption[];
|
||||
warehouseOptions: SelectOption[];
|
||||
priceListOptions: SelectOption[];
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const ProformaUpdateHeaderEditor = ({
|
||||
statusOptions,
|
||||
customerOptions,
|
||||
currencyOptions,
|
||||
paymentMethodOptions,
|
||||
salesPersonOptions,
|
||||
warehouseOptions,
|
||||
priceListOptions,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
}: ProformaUpdateHeaderEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<ProformaUpdateForm>();
|
||||
|
||||
const isFieldLocked = disabled || readOnly;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ProformaSectionCard
|
||||
description={t("proformas.update.sections.document_description")}
|
||||
title={t("proformas.update.sections.document")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<ProformaFormFieldShell
|
||||
error={errors.series?.message}
|
||||
htmlFor="series"
|
||||
label={t("proformas.fields.series")}
|
||||
required
|
||||
span="xs"
|
||||
>
|
||||
<Input className="h-10" disabled={isFieldLocked} id="series" {...register("series")} />
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.number?.message}
|
||||
htmlFor="number"
|
||||
label={t("proformas.fields.number")}
|
||||
required
|
||||
span="sm"
|
||||
>
|
||||
<Input className="h-10" disabled={isFieldLocked} id="number" {...register("number")} />
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.document_date?.message}
|
||||
htmlFor="document_date"
|
||||
label={t("proformas.fields.document_date")}
|
||||
required
|
||||
span="sm"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="document_date"
|
||||
type="date"
|
||||
{...register("document_date")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.status?.message}
|
||||
htmlFor="status"
|
||||
label={t("proformas.fields.status")}
|
||||
required
|
||||
span="md"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="status">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{statusOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.external_reference?.message}
|
||||
htmlFor="external_reference"
|
||||
label={t("proformas.fields.external_reference")}
|
||||
span="md"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="external_reference"
|
||||
{...register("external_reference")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
|
||||
<ProformaSectionCard
|
||||
description={t("proformas.update.sections.customer_description")}
|
||||
title={t("proformas.update.sections.customer")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<ProformaFormFieldShell
|
||||
error={errors.customer_id?.message}
|
||||
htmlFor="customer_id"
|
||||
label={t("proformas.fields.customer")}
|
||||
required
|
||||
span="lg"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="customer_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="customer_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{customerOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.customer_tax_id?.message}
|
||||
htmlFor="customer_tax_id"
|
||||
label={t("proformas.fields.customer_tax_id")}
|
||||
span="sm"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="customer_tax_id"
|
||||
{...register("customer_tax_id")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.customer_reference?.message}
|
||||
htmlFor="customer_reference"
|
||||
label={t("proformas.fields.customer_reference")}
|
||||
span="md"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="customer_reference"
|
||||
{...register("customer_reference")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
|
||||
<ProformaSectionCard
|
||||
description={t("proformas.update.sections.commercial_description")}
|
||||
title={t("proformas.update.sections.commercial")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<ProformaFormFieldShell
|
||||
error={errors.currency_code?.message}
|
||||
htmlFor="currency_code"
|
||||
label={t("proformas.fields.currency_code")}
|
||||
required
|
||||
span="sm"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="currency_code"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="currency_code">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{currencyOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.payment_method_id?.message}
|
||||
htmlFor="payment_method_id"
|
||||
label={t("proformas.fields.payment_method")}
|
||||
span="md"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="payment_method_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="payment_method_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{paymentMethodOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.due_date?.message}
|
||||
htmlFor="due_date"
|
||||
label={t("proformas.fields.due_date")}
|
||||
span="sm"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="due_date"
|
||||
type="date"
|
||||
{...register("due_date")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.sales_person_id?.message}
|
||||
htmlFor="sales_person_id"
|
||||
label={t("proformas.fields.sales_person")}
|
||||
span="md"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="sales_person_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="sales_person_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{salesPersonOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.warehouse_id?.message}
|
||||
htmlFor="warehouse_id"
|
||||
label={t("proformas.fields.warehouse")}
|
||||
span="md"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="warehouse_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="warehouse_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{warehouseOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.price_list_id?.message}
|
||||
htmlFor="price_list_id"
|
||||
label={t("proformas.fields.price_list")}
|
||||
span="md"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="price_list_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="price_list_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{priceListOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
|
||||
<ProformaSectionCard
|
||||
description={t("proformas.update.sections.content_description")}
|
||||
title={t("proformas.update.sections.content")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<ProformaFormFieldShell
|
||||
error={errors.subject?.message}
|
||||
htmlFor="subject"
|
||||
label={t("proformas.fields.subject")}
|
||||
span="full"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="subject"
|
||||
{...register("subject")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.notes?.message}
|
||||
htmlFor="notes"
|
||||
label={t("proformas.fields.notes")}
|
||||
span="full"
|
||||
>
|
||||
<Textarea
|
||||
className="min-h-28 resize-y"
|
||||
disabled={isFieldLocked}
|
||||
id="notes"
|
||||
{...register("notes")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,12 +1,14 @@
|
||||
import { SpainTaxCatalogProvider } from "@erp/core";
|
||||
import { ErrorAlert } from "@erp/core/components";
|
||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { Spinner } from "@repo/shadcn-ui/components";
|
||||
import { useMemo } from "react";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller";
|
||||
import { ProformaUpdateEditor } from "../blocks";
|
||||
import { ProformaUpdateEditorForm } from "../blocks";
|
||||
import { ProformaUpdateSkeleton } from "../components";
|
||||
|
||||
export const ProformaUpdatePage = () => {
|
||||
@ -38,57 +40,69 @@ export const ProformaUpdatePage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...updateCtrl.form}>
|
||||
<ProformaUpdateEditor
|
||||
formId={updateCtrl.formId}
|
||||
isSubmitting={updateCtrl.isUpdating}
|
||||
onReset={updateCtrl.resetForm}
|
||||
onSubmit={updateCtrl.onSubmit}
|
||||
proforma={updateCtrl.proforma}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
if (isLoading) {
|
||||
return <ProformaEditorSkeleton />;
|
||||
}
|
||||
|
||||
if (isLoadError) {
|
||||
if (!updateCtrl.proformaData)
|
||||
return (
|
||||
<>
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
message={
|
||||
(loadError as Error)?.message ??
|
||||
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||
}
|
||||
title={t("pages.update.loadErrorTitle", "No se pudo cargar la proforma")}
|
||||
<NotFoundCard
|
||||
message={t("pages.update.notFoundMsg", "Revisa el identificador o vuelve al listado.")}
|
||||
title={t("pages.update.notFoundTitle", "Proforma no encontrada")}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<BackHistoryButton />
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Monta el contexto aquí, así todo lo que esté dentro puede usar hooks
|
||||
return (
|
||||
<ProformaProvider
|
||||
company_id={proformaData.company_id}
|
||||
currency_code={proformaData.currency_code}
|
||||
language_code={proformaData.language_code}
|
||||
proforma_id={proforma_id!}
|
||||
status={proformaData.status}
|
||||
taxCatalog={taxCatalog}
|
||||
>
|
||||
<ProformaUpdateComp proforma={proformaData} />
|
||||
</ProformaProvider>
|
||||
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||
<AppHeader className="bg-red-500 sticky top-0">
|
||||
<PageHeader
|
||||
backIcon
|
||||
description={t("pages.update.description")}
|
||||
rightSlot={
|
||||
<UpdateCommitButtonGroup
|
||||
cancel={{
|
||||
formId: updateCtrl.formId,
|
||||
to: "/customers/list",
|
||||
disabled: updateCtrl.isUpdating,
|
||||
}}
|
||||
disabled={updateCtrl.isUpdating}
|
||||
isLoading={updateCtrl.isUpdating}
|
||||
onReset={updateCtrl.resetForm}
|
||||
submit={{
|
||||
formId: updateCtrl.formId,
|
||||
disabled: updateCtrl.isUpdating,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
title={t("pages.update.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
{updateCtrl.isUpdateError && (
|
||||
<ErrorAlert
|
||||
message={
|
||||
(updateCtrl.updateError as Error)?.message ??
|
||||
t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
title={t("pages.update.errorTitle", "No se pudo guardar los cambios")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{updateCtrl.isLoading && <Spinner />}
|
||||
|
||||
{!updateCtrl.isLoading && (
|
||||
<FormProvider {...updateCtrl.form}>
|
||||
<ProformaUpdateEditorForm
|
||||
className="mx-auto w-full max-w-7xl px-4 py-6 md:px-6 xl:px-8"
|
||||
formId={updateCtrl.formId}
|
||||
isSubmitting={updateCtrl.isUpdating}
|
||||
onReset={updateCtrl.resetForm}
|
||||
onSubmit={updateCtrl.onSubmit}
|
||||
/>
|
||||
</FormProvider>
|
||||
)}
|
||||
</AppContent>
|
||||
</UnsavedChangesProvider>
|
||||
);
|
||||
};
|
||||
*/
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
import { PageHeader } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider, UpdateCommitButtonGroup, useUrlParamId } from "@erp/core/hooks";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import { useCustomerUpdatePageController } from "../../../update/controllers/use-customer-update.controller";
|
||||
import {
|
||||
CustomerEditForm,
|
||||
CustomerEditorSkeleton,
|
||||
ErrorAlert,
|
||||
NotFoundCard,
|
||||
} from "../../components";
|
||||
|
||||
export const CustomerUpdatePage = () => {
|
||||
const customerId = useUrlParamId();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
form,
|
||||
formId,
|
||||
onSubmit,
|
||||
resetForm,
|
||||
|
||||
customerData,
|
||||
isLoading,
|
||||
isLoadError,
|
||||
loadError,
|
||||
|
||||
isUpdating,
|
||||
isUpdateError,
|
||||
updateError,
|
||||
|
||||
FormProvider,
|
||||
} = useCustomerUpdatePageController(customerId, {});
|
||||
|
||||
if (isLoading) {
|
||||
return <CustomerEditorSkeleton />;
|
||||
}
|
||||
|
||||
if (isLoadError) {
|
||||
return (
|
||||
<>
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
message={
|
||||
(loadError as Error)?.message ??
|
||||
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||
}
|
||||
title={t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<BackHistoryButton />
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!customerData)
|
||||
return (
|
||||
<>
|
||||
<AppContent>
|
||||
<NotFoundCard
|
||||
message={t("pages.update.notFoundMsg", "Revisa el identificador o vuelve al listado.")}
|
||||
title={t("pages.update.notFoundTitle", "Cliente no encontrado")}
|
||||
/>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
backIcon
|
||||
description={t("pages.update.description")}
|
||||
rightSlot={
|
||||
<UpdateCommitButtonGroup
|
||||
cancel={{
|
||||
formId,
|
||||
to: "/customers/list",
|
||||
disabled: isUpdating,
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
isLoading={isUpdating}
|
||||
onReset={resetForm}
|
||||
submit={{
|
||||
formId,
|
||||
disabled: isUpdating,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
title={t("pages.update.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
{isUpdateError && (
|
||||
<ErrorAlert
|
||||
message={
|
||||
(updateError as Error)?.message ??
|
||||
t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
title={t("pages.update.errorTitle", "No se pudo guardar los cambios")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CustomerEditForm
|
||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6" // para que el botón del header pueda hacer submit
|
||||
formId={formId}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</FormProvider>
|
||||
</AppContent>
|
||||
</UnsavedChangesProvider>
|
||||
);
|
||||
};
|
||||
@ -1,149 +1,101 @@
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FormControl,
|
||||
FormField,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { format, isValid, parseISO } from "date-fns";
|
||||
import { CalendarIcon, LockIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormFieldLabel } from "./form-field-label.tsx";
|
||||
import type { NativeInputProps } from "./types.ts";
|
||||
|
||||
type DatePickerFieldProps<TFormValues extends FieldValues> = {
|
||||
type DatePickerFieldProps<TFormValues extends FieldValues> = Omit<NativeInputProps, "name"> & {
|
||||
name: FieldPath<TFormValues>;
|
||||
|
||||
label?: string;
|
||||
description?: string;
|
||||
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
|
||||
placeholder?: string;
|
||||
|
||||
orientation?: "vertical" | "horizontal" | "responsive";
|
||||
|
||||
className?: string;
|
||||
inputClassName?: string;
|
||||
formatDateFn?: (value: string) => string;
|
||||
};
|
||||
|
||||
const parseFieldDate = (value?: string): Date | undefined => {
|
||||
if (!value) return undefined;
|
||||
|
||||
const parsed = parseISO(value);
|
||||
if (!isValid(parsed)) return undefined;
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const toDateOnlyString = (date: Date): string => {
|
||||
return format(date, "yyyy-MM-dd");
|
||||
};
|
||||
|
||||
export function DatePickerField<TFormValues extends FieldValues>({
|
||||
export const DatePickerField = <TFormValues extends FieldValues>({
|
||||
name,
|
||||
|
||||
label,
|
||||
placeholder,
|
||||
description,
|
||||
disabled = false,
|
||||
|
||||
required = false,
|
||||
readOnly = false,
|
||||
|
||||
orientation = "vertical",
|
||||
|
||||
className,
|
||||
inputClassName,
|
||||
formatDateFn = (value) => {
|
||||
const parsed = parseFieldDate(value);
|
||||
return parsed ? format(parsed, "dd/MM/yyyy") : value;
|
||||
},
|
||||
}: DatePickerFieldProps<TFormValues>) {
|
||||
const triggerId = React.useId();
|
||||
const { control, formState } = useFormContext<TFormValues>();
|
||||
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
|
||||
|
||||
...inputRest
|
||||
}: DatePickerFieldProps<TFormValues>) => {
|
||||
const { register, formState, getFieldState } = useFormContext<TFormValues>();
|
||||
|
||||
const inputId = React.useId();
|
||||
const disabled = formState.isSubmitting || inputRest.disabled;
|
||||
|
||||
const presetProps = {
|
||||
type: "date",
|
||||
autoComplete: "off",
|
||||
spellCheck: false,
|
||||
};
|
||||
|
||||
const rightIcon = <CalendarIcon />;
|
||||
|
||||
// Obtener error del campo (tipado seguro)
|
||||
const fieldError = getFieldState(name, formState).error;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field, fieldState }) => {
|
||||
const selectedDate = parseFieldDate(field.value);
|
||||
const displayValue = field.value ? formatDateFn(field.value) : null;
|
||||
<Field className={cn("gap-1", className)} data-invalid={!!fieldError} orientation={orientation}>
|
||||
{label ? (
|
||||
<FormFieldLabel htmlFor={inputId} required={required}>
|
||||
{label}
|
||||
</FormFieldLabel>
|
||||
) : null}
|
||||
|
||||
return (
|
||||
<Field
|
||||
className={cn("gap-1", className)}
|
||||
data-invalid={fieldState.invalid}
|
||||
orientation={orientation}
|
||||
>
|
||||
{label ? (
|
||||
<FormFieldLabel htmlFor={triggerId} required={required}>
|
||||
{label}
|
||||
</FormFieldLabel>
|
||||
) : null}
|
||||
<InputGroup
|
||||
className={cn(
|
||||
"bg-muted/50 font-medium",
|
||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||
"placeholder:text-muted-foreground/50",
|
||||
inputClassName
|
||||
)}
|
||||
>
|
||||
<InputGroupInput
|
||||
{...presetProps}
|
||||
{...inputRest}
|
||||
{...register(name)}
|
||||
aria-invalid={!!fieldError}
|
||||
className="placeholder:text-muted-foreground/50"
|
||||
disabled={disabled}
|
||||
id={inputId}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
aria-invalid={fieldState.invalid}
|
||||
aria-required={required || undefined}
|
||||
className={cn(
|
||||
"h-9 w-full justify-start bg-muted/50 text-left font-medium",
|
||||
"hover:border-ring/60",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
!displayValue && "text-muted-foreground",
|
||||
inputClassName
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
id={triggerId}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{readOnly ? (
|
||||
<LockIcon aria-hidden="true" className="mr-2 h-4 w-4 opacity-70" />
|
||||
) : (
|
||||
<CalendarIcon aria-hidden="true" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{rightIcon && <InputGroupAddon aria-hidden="true">{rightIcon}</InputGroupAddon>}
|
||||
</InputGroup>
|
||||
|
||||
<span className="truncate">{displayValue ?? placeholder ?? "Select date"}</span>
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
{description ? (
|
||||
<FieldDescription>{description}</FieldDescription>
|
||||
) : (
|
||||
<div aria-hidden="true" className="min-h-5" />
|
||||
)}
|
||||
|
||||
{isDisabled ? null : (
|
||||
<PopoverContent align="start" className="w-auto p-0">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="single"
|
||||
onSelect={(date) => {
|
||||
field.onChange(date ? toDateOnlyString(date) : "");
|
||||
field.onBlur();
|
||||
}}
|
||||
selected={selectedDate}
|
||||
/>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
{description ? (
|
||||
<FieldDescription>{description}</FieldDescription>
|
||||
) : (
|
||||
<div aria-hidden="true" className="min-h-5" />
|
||||
)}
|
||||
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FieldError errors={[fieldError]} />
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,87 +1,87 @@
|
||||
import { Button, Input } from '@repo/shadcn-ui/components';
|
||||
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,
|
||||
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;
|
||||
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>
|
||||
);
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={hasError || undefined}
|
||||
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"
|
||||
)}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
onBlur={onBlurConfirm}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onBlurConfirm();
|
||||
}
|
||||
if ((e.altKey || e.metaKey) && e.key === "ArrowDown") {
|
||||
onOpenRequest?.();
|
||||
}
|
||||
}}
|
||||
pattern="\d{2}/\d{2}/\d{4}"
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
type="text"
|
||||
value={value}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-2 flex items-center gap-2 pr-1">
|
||||
{!(readOnly || required) && value && (
|
||||
<Button
|
||||
aria-label="Clear date"
|
||||
className="text-muted-foreground hover:text-destructive transition cursor-pointer -mr-3"
|
||||
onClick={onClear}
|
||||
size={"icon-sm"}
|
||||
type="button"
|
||||
variant={"link"}
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
{readOnly ? (
|
||||
<LockIcon className="size-4" />
|
||||
) : (
|
||||
(triggerButton ?? <CalendarIcon aria-hidden="true" className="size-4" />)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,205 +1,231 @@
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
type Calendar,
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
Popover,
|
||||
PopoverTrigger
|
||||
PopoverTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { format, isValid, parse } from "date-fns";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ControllerFieldState, ControllerRenderProps, FieldValues, Path, UseFormStateReturn } from "react-hook-form";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import type {
|
||||
ControllerFieldState,
|
||||
ControllerRenderProps,
|
||||
FieldValues,
|
||||
Path,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../locales/i18n.ts";
|
||||
import { DateInputField } from './date-input-field.tsx';
|
||||
import { DatePopoverCalendar } from './date-popover-calendar.tsx';
|
||||
import { FormFieldLabel } from "../form-field-label.tsx";
|
||||
|
||||
import { DateInputField } from "./date-input-field.tsx";
|
||||
import { DatePopoverCalendar } from "./date-popover-calendar.tsx";
|
||||
|
||||
export type SUICalendarProps = Omit<React.ComponentProps<
|
||||
typeof Calendar>, "select" | "onSelect">
|
||||
export type SUICalendarProps = Omit<
|
||||
React.ComponentProps<typeof Calendar>,
|
||||
"mode" | "selected" | "onSelect"
|
||||
>;
|
||||
|
||||
type DatePickerInputCompProps<TFormValues extends FieldValues = FieldValues> = SUICalendarProps & {
|
||||
field: ControllerRenderProps<TFormValues, Path<TFormValues>>;
|
||||
fieldState: ControllerFieldState;
|
||||
formState: UseFormStateReturn<TFormValues>;
|
||||
|
||||
displayDateFormat: string; // e.g. "dd/MM/yyyy"
|
||||
parseDateFormat: string; // e.g. "yyyy/MM/dd"
|
||||
inputId: string;
|
||||
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
invalid?: boolean;
|
||||
|
||||
orientation?: "vertical" | "horizontal" | "responsive",
|
||||
orientation?: "vertical" | "horizontal" | "responsive";
|
||||
|
||||
className?: string;
|
||||
inputClassName?: string;
|
||||
|
||||
displayDateFormat: string;
|
||||
valueDateFormat: string;
|
||||
};
|
||||
|
||||
export function DatePickerInputComp<TFormValues extends FieldValues = FieldValues>({
|
||||
field,
|
||||
fieldState,
|
||||
formState,
|
||||
|
||||
parseDateFormat,
|
||||
displayDateFormat,
|
||||
|
||||
inputId,
|
||||
label,
|
||||
placeholder,
|
||||
description,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
required = false,
|
||||
readOnly = false,
|
||||
invalid = false,
|
||||
|
||||
orientation = "vertical",
|
||||
|
||||
className,
|
||||
inputClassName,
|
||||
displayDateFormat,
|
||||
valueDateFormat,
|
||||
...calendarProps
|
||||
}: DatePickerInputCompProps<TFormValues>) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
const parsedDate = useMemo(() => {
|
||||
if (!field.value) return null;
|
||||
const d = parse(field.value, parseDateFormat, new Date());
|
||||
return isValid(d) ? d : null;
|
||||
}, [field.value, parseDateFormat]);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [displayValue, setDisplayValue] = React.useState("");
|
||||
const [localError, setLocalError] = React.useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const descriptionId = description ? `${inputId}-description` : undefined;
|
||||
const errorId = `${inputId}-error`;
|
||||
const popoverId = `${inputId}-popover`;
|
||||
|
||||
const parsedDate = React.useMemo(() => {
|
||||
if (!field.value || typeof field.value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = parse(field.value, valueDateFormat, new Date());
|
||||
|
||||
return isValid(parsed) ? parsed : undefined;
|
||||
}, [field.value, valueDateFormat]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setDisplayValue(parsedDate ? format(parsedDate, displayDateFormat) : "");
|
||||
}, [parsedDate, displayDateFormat]);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
const handleClear = React.useCallback(() => {
|
||||
field.onChange("");
|
||||
field.onBlur();
|
||||
setDisplayValue("");
|
||||
setLocalError(null);
|
||||
}, [field]);
|
||||
|
||||
const validateAndSet = useCallback(() => {
|
||||
const trimmed = displayValue.trim();
|
||||
if (!trimmed) {
|
||||
const confirmInputValue = React.useCallback(() => {
|
||||
const trimmedValue = displayValue.trim();
|
||||
|
||||
if (!trimmedValue) {
|
||||
handleClear();
|
||||
return;
|
||||
}
|
||||
const d = parse(trimmed, displayDateFormat, new Date());
|
||||
if (isValid(d)) {
|
||||
field.onChange(format(d, parseDateFormat));
|
||||
setDisplayValue(format(d, displayDateFormat));
|
||||
setLocalError(null);
|
||||
} else {
|
||||
|
||||
const parsed = parse(trimmedValue, displayDateFormat, new Date());
|
||||
|
||||
if (!isValid(parsed)) {
|
||||
setLocalError(t("components.date_picker_input_field.invalid_date"));
|
||||
field.onBlur();
|
||||
return;
|
||||
}
|
||||
}, [displayValue, displayDateFormat, parseDateFormat, field, t, handleClear]);
|
||||
|
||||
field.onChange(format(parsed, valueDateFormat));
|
||||
field.onBlur();
|
||||
setDisplayValue(format(parsed, displayDateFormat));
|
||||
setLocalError(null);
|
||||
}, [displayValue, displayDateFormat, field, handleClear, t, valueDateFormat]);
|
||||
|
||||
const handleSelectDate = React.useCallback(
|
||||
(date: Date | undefined) => {
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(format(date, valueDateFormat));
|
||||
field.onBlur();
|
||||
setDisplayValue(format(date, displayDateFormat));
|
||||
setLocalError(null);
|
||||
setOpen(false);
|
||||
},
|
||||
[displayDateFormat, field, valueDateFormat]
|
||||
);
|
||||
|
||||
const handleSelectToday = React.useCallback(() => {
|
||||
const today = new Date();
|
||||
|
||||
field.onChange(format(today, valueDateFormat));
|
||||
field.onBlur();
|
||||
setDisplayValue(format(today, displayDateFormat));
|
||||
setLocalError(null);
|
||||
setOpen(false);
|
||||
}, [displayDateFormat, field, valueDateFormat]);
|
||||
|
||||
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`;
|
||||
const describedBy =
|
||||
[descriptionId, hasError ? errorId : undefined].filter(Boolean).join(" ") || undefined;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<Field className={cn("gap-1", className)} data-invalid={hasError} orientation={orientation}>
|
||||
{label ? (
|
||||
<FormFieldLabel htmlFor={inputId} required={required}>
|
||||
{label}
|
||||
</FormFieldLabel>
|
||||
) : null}
|
||||
|
||||
|
||||
<Popover modal={true} open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
<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 && (
|
||||
<div className={cn(inputClassName)}>
|
||||
<DateInputField
|
||||
describedBy={describedBy}
|
||||
disabled={disabled}
|
||||
hasError={hasError}
|
||||
id={inputId}
|
||||
onBlurConfirm={confirmInputValue}
|
||||
onChange={(value) => {
|
||||
setDisplayValue(value);
|
||||
setLocalError(null);
|
||||
}}
|
||||
onClear={handleClear}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
triggerButton={
|
||||
disabled || readOnly ? null : (
|
||||
<Popover onOpenChange={setOpen} open={open}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={'link'}
|
||||
type='button'
|
||||
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"
|
||||
aria-expanded={open}
|
||||
aria-haspopup="dialog"
|
||||
aria-label={t("components.date_picker_input_field.open_calendar")}
|
||||
className="text-muted-foreground"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="link"
|
||||
>
|
||||
<CalendarIcon className="size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
</PopoverTrigger>
|
||||
<DatePopoverCalendar
|
||||
{...calendarProps}
|
||||
contentId={popoverId}
|
||||
description={description}
|
||||
numberOfMonths={2}
|
||||
onClose={() => setOpen(false)}
|
||||
onSelect={handleSelectDate}
|
||||
onToday={handleSelectToday}
|
||||
selectedDate={parsedDate}
|
||||
title={label}
|
||||
/>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
value={displayValue}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!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>
|
||||
|
||||
{false && (
|
||||
<div className='mt-1 flex items-start justify-between'>
|
||||
<FieldDescription
|
||||
id={describedById}
|
||||
className={cn("text-xs truncate", !description && "invisible")}
|
||||
>
|
||||
{description || "\u00A0"}
|
||||
</FieldDescription>
|
||||
</div>
|
||||
{description ? (
|
||||
<FieldDescription id={descriptionId}>{description}</FieldDescription>
|
||||
) : (
|
||||
<div aria-hidden="true" className="min-h-5" />
|
||||
)}
|
||||
|
||||
{localError ? (
|
||||
<p className="text-destructive text-sm" id={errorId}>
|
||||
{localError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
<FieldError
|
||||
errors={localError ? [] : [fieldState.error]}
|
||||
id={localError ? undefined : errorId}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,44 +1,58 @@
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import * as React from "react";
|
||||
import { Controller, type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
|
||||
|
||||
import { Control, Controller, FieldPath, FieldValues } from "react-hook-form";
|
||||
import { DatePickerInputComp, SUICalendarProps } from "./date-picker-input-comp.tsx";
|
||||
import { DatePickerInputComp, type SUICalendarProps } from "./date-picker-input-comp.tsx";
|
||||
|
||||
type DatePickerInputFieldProps<TFormValues extends FieldValues> = SUICalendarProps & {
|
||||
control: Control<TFormValues>;
|
||||
name: FieldPath<TFormValues>;
|
||||
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
|
||||
displayDateFormat?: string; // e.g. "dd-MM-yyyy"
|
||||
parseDateFormat?: string; // e.g. "yyyy-MM-dd"
|
||||
orientation?: "vertical" | "horizontal" | "responsive",
|
||||
displayDateFormat?: string;
|
||||
valueDateFormat?: string;
|
||||
|
||||
orientation?: "vertical" | "horizontal" | "responsive";
|
||||
|
||||
className?: string;
|
||||
inputClassName?: string;
|
||||
};
|
||||
|
||||
export function DatePickerInputField<TFormValues extends FieldValues>({
|
||||
control,
|
||||
name,
|
||||
displayDateFormat = "dd-MM-yyyy",
|
||||
parseDateFormat = "yyyy-MM-dd",
|
||||
displayDateFormat = "dd/MM/yyyy",
|
||||
valueDateFormat = "yyyy-MM-dd",
|
||||
inputClassName,
|
||||
...props
|
||||
}: DatePickerInputFieldProps<TFormValues>) {
|
||||
const { control } = useFormContext<TFormValues>();
|
||||
const inputId = React.useId();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field, fieldState, formState }) => (
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePickerInputComp
|
||||
{...props}
|
||||
displayDateFormat={displayDateFormat}
|
||||
field={field}
|
||||
fieldState={fieldState}
|
||||
formState={formState}
|
||||
|
||||
displayDateFormat={displayDateFormat}
|
||||
parseDateFormat={parseDateFormat}
|
||||
{...props}
|
||||
inputClassName={cn(
|
||||
"bg-muted/50 font-medium",
|
||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||
"placeholder:text-muted-foreground/50",
|
||||
inputClassName
|
||||
)}
|
||||
inputId={inputId}
|
||||
valueDateFormat={valueDateFormat}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -1,76 +1,67 @@
|
||||
// DatePopoverCalendar.tsx
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
PopoverContent,
|
||||
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,
|
||||
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;
|
||||
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>
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<PopoverContent className="w-auto p-0" id={contentId}>
|
||||
<Card className="border-none shadow-none">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>{label}</CardTitle>
|
||||
<CardDescription>{description || "\u00A0"}</CardDescription>
|
||||
<CardAction>
|
||||
<Button onClick={onToday} size="sm" variant="outline">
|
||||
{t("components.date_picker_input_field.today")}
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
|
||||
</CardHeader>
|
||||
<CardContent className="px-0">
|
||||
<Calendar
|
||||
defaultMonth={parsedDate ?? undefined}
|
||||
mode="single"
|
||||
numberOfMonths={2}
|
||||
onSelect={onSelect}
|
||||
selected={parsedDate ?? undefined}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
<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 >
|
||||
);
|
||||
<CardFooter className="mx-auto">
|
||||
<Button onClick={onClose} size="sm" variant="outline">
|
||||
{t("components.date_picker_input_field.close")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
);
|
||||
}
|
||||
|
||||
@ -157,9 +157,11 @@ export const MultiSelectField = <T extends FieldValues>({
|
||||
aria-invalid={fieldState.invalid}
|
||||
aria-required={required || undefined}
|
||||
className={cn(
|
||||
"min-h-10 h-auto w-full justify-between bg-muted/50 p-1 text-left font-medium",
|
||||
"hover:border-ring/60",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"min-h-10 h-auto w-full justify-between text-left ",
|
||||
"bg-muted/50 p-1 font-medium",
|
||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||
"placeholder:text-muted-foreground/50",
|
||||
inputClassName
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
|
||||
@ -89,8 +89,9 @@ export function SelectField<TFormValues extends FieldValues>({
|
||||
aria-required={required}
|
||||
className={cn(
|
||||
"bg-muted/50 font-medium",
|
||||
"hover:border-ring/60",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||
"placeholder:text-muted-foreground/50",
|
||||
inputClassName
|
||||
)}
|
||||
id={triggerId}
|
||||
|
||||
@ -66,8 +66,8 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
||||
aria-invalid={!!fieldError}
|
||||
className={cn(
|
||||
"bg-muted/50 font-medium",
|
||||
"hover:border-ring/60",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||
"placeholder:text-muted-foreground/50",
|
||||
inputClassName
|
||||
)}
|
||||
|
||||
@ -72,8 +72,9 @@ export const TextField = <TFormValues extends FieldValues>({
|
||||
<InputGroup
|
||||
className={cn(
|
||||
"bg-muted/50 font-medium",
|
||||
"hover:border-ring/60",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||
"placeholder:text-muted-foreground/50",
|
||||
inputClassName
|
||||
)}
|
||||
>
|
||||
|
||||
@ -16,7 +16,7 @@ export const AppLayout = () => {
|
||||
>
|
||||
<AppSidebar className="bg-sidebar" variant="inset" />
|
||||
{/* Aquí está el MAIN */}
|
||||
<SidebarInset className="app-main bg-muted">
|
||||
<SidebarInset className="app-main bg-muted overflow-hidden">
|
||||
<Outlet />
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
|
||||
@ -62,8 +62,10 @@
|
||||
"date_picker_input_field": {
|
||||
"invalid_date": "Fecha inválida",
|
||||
"clear_date": "Limpiar fecha",
|
||||
"open_calendar": "Abrir calendario",
|
||||
"today": "Hoy",
|
||||
"close": "Cerrar"
|
||||
"close": "Cerrar",
|
||||
"select_date": "Seleccionar fecha"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -133,7 +133,7 @@
|
||||
--accent-foreground: oklch(0.97 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--input: oklch(0.822 0 0);
|
||||
--ring: oklch(0.575 0.1533 256.4357); /* oklch(0.708 0 0); */
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
@ -149,7 +149,7 @@
|
||||
--sidebar-border: oklch(0.6427 0.1407 253.94);
|
||||
--sidebar-ring: oklch(1 0 0);
|
||||
|
||||
--radius: 0.325rem;
|
||||
--radius: 0.625rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 3px;
|
||||
@ -200,7 +200,7 @@
|
||||
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-border: oklch(1 0 0);
|
||||
--sidebar-ring: oklch(0.4915 0.2776 263.8724);
|
||||
--radius: 0.325rem;
|
||||
--radius: 0.625rem;
|
||||
--shadow-2xs: 1px 1px 6px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 1px 1px 6px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 1px 1px 6px 0px hsl(0 0% 0% / 0.1), 1px 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user