Pruebas
This commit is contained in:
parent
bd209374bc
commit
b88632d395
@ -1,5 +1,5 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
export const ProformaLayout = ({ children }: PropsWithChildren) => {
|
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-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 {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
|
FieldDescription,
|
||||||
|
FieldGroup,
|
||||||
|
FieldLegend,
|
||||||
|
FieldSet,
|
||||||
Input,
|
Input,
|
||||||
Textarea,
|
Textarea,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { ProformaUpdateForm } from "../../entities";
|
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 { t } = useTranslation();
|
||||||
const { register, formState } = useFormContext<ProformaUpdateForm>();
|
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 { SpainTaxCatalogProvider } from "@erp/core";
|
||||||
import { ErrorAlert } from "@erp/core/components";
|
import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
||||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/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 { useMemo } from "react";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller";
|
import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller";
|
||||||
import { ProformaUpdateEditor } from "../blocks";
|
import { ProformaUpdateEditorForm } from "../blocks";
|
||||||
import { ProformaUpdateSkeleton } from "../components";
|
import { ProformaUpdateSkeleton } from "../components";
|
||||||
|
|
||||||
export const ProformaUpdatePage = () => {
|
export const ProformaUpdatePage = () => {
|
||||||
@ -38,57 +40,69 @@ export const ProformaUpdatePage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (!updateCtrl.proformaData)
|
||||||
<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) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<ErrorAlert
|
<NotFoundCard
|
||||||
message={
|
message={t("pages.update.notFoundMsg", "Revisa el identificador o vuelve al listado.")}
|
||||||
(loadError as Error)?.message ??
|
title={t("pages.update.notFoundTitle", "Proforma no encontrada")}
|
||||||
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
|
||||||
}
|
|
||||||
title={t("pages.update.loadErrorTitle", "No se pudo cargar la proforma")}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<BackHistoryButton />
|
|
||||||
</div>
|
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Monta el contexto aquí, así todo lo que esté dentro puede usar hooks
|
|
||||||
return (
|
return (
|
||||||
<ProformaProvider
|
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||||
company_id={proformaData.company_id}
|
<AppHeader className="bg-red-500 sticky top-0">
|
||||||
currency_code={proformaData.currency_code}
|
<PageHeader
|
||||||
language_code={proformaData.language_code}
|
backIcon
|
||||||
proforma_id={proforma_id!}
|
description={t("pages.update.description")}
|
||||||
status={proformaData.status}
|
rightSlot={
|
||||||
taxCatalog={taxCatalog}
|
<UpdateCommitButtonGroup
|
||||||
>
|
cancel={{
|
||||||
<ProformaUpdateComp proforma={proformaData} />
|
formId: updateCtrl.formId,
|
||||||
</ProformaProvider>
|
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 {
|
import {
|
||||||
Button,
|
|
||||||
Calendar,
|
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldError,
|
FieldError,
|
||||||
FormControl,
|
InputGroup,
|
||||||
FormField,
|
InputGroupAddon,
|
||||||
Popover,
|
InputGroupInput,
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { format, isValid, parseISO } from "date-fns";
|
import { CalendarIcon } from "lucide-react";
|
||||||
import { CalendarIcon, LockIcon } from "lucide-react";
|
import * as React from "react";
|
||||||
import React from "react";
|
|
||||||
import { type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
|
import { type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { FormFieldLabel } from "./form-field-label.tsx";
|
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>;
|
name: FieldPath<TFormValues>;
|
||||||
|
|
||||||
label?: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
disabled?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
readOnly?: boolean;
|
|
||||||
|
|
||||||
placeholder?: string;
|
|
||||||
|
|
||||||
orientation?: "vertical" | "horizontal" | "responsive";
|
orientation?: "vertical" | "horizontal" | "responsive";
|
||||||
|
|
||||||
className?: string;
|
|
||||||
inputClassName?: string;
|
inputClassName?: string;
|
||||||
formatDateFn?: (value: string) => string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseFieldDate = (value?: string): Date | undefined => {
|
export const DatePickerField = <TFormValues extends FieldValues>({
|
||||||
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>({
|
|
||||||
name,
|
name,
|
||||||
|
|
||||||
label,
|
label,
|
||||||
placeholder,
|
|
||||||
description,
|
description,
|
||||||
disabled = false,
|
|
||||||
required = false,
|
required = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
|
|
||||||
orientation = "vertical",
|
orientation = "vertical",
|
||||||
|
|
||||||
className,
|
className,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
formatDateFn = (value) => {
|
|
||||||
const parsed = parseFieldDate(value);
|
...inputRest
|
||||||
return parsed ? format(parsed, "dd/MM/yyyy") : value;
|
}: DatePickerFieldProps<TFormValues>) => {
|
||||||
},
|
const { register, formState, getFieldState } = useFormContext<TFormValues>();
|
||||||
}: DatePickerFieldProps<TFormValues>) {
|
|
||||||
const triggerId = React.useId();
|
const inputId = React.useId();
|
||||||
const { control, formState } = useFormContext<TFormValues>();
|
const disabled = formState.isSubmitting || inputRest.disabled;
|
||||||
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
|
|
||||||
|
const presetProps = {
|
||||||
|
type: "date",
|
||||||
|
autoComplete: "off",
|
||||||
|
spellCheck: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rightIcon = <CalendarIcon />;
|
||||||
|
|
||||||
|
// Obtener error del campo (tipado seguro)
|
||||||
|
const fieldError = getFieldState(name, formState).error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<Field className={cn("gap-1", className)} data-invalid={!!fieldError} orientation={orientation}>
|
||||||
control={control}
|
{label ? (
|
||||||
name={name}
|
<FormFieldLabel htmlFor={inputId} required={required}>
|
||||||
render={({ field, fieldState }) => {
|
{label}
|
||||||
const selectedDate = parseFieldDate(field.value);
|
</FormFieldLabel>
|
||||||
const displayValue = field.value ? formatDateFn(field.value) : null;
|
) : null}
|
||||||
|
|
||||||
return (
|
<InputGroup
|
||||||
<Field
|
className={cn(
|
||||||
className={cn("gap-1", className)}
|
"bg-muted/50 font-medium",
|
||||||
data-invalid={fieldState.invalid}
|
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||||
orientation={orientation}
|
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||||
>
|
"placeholder:text-muted-foreground/50",
|
||||||
{label ? (
|
inputClassName
|
||||||
<FormFieldLabel htmlFor={triggerId} required={required}>
|
)}
|
||||||
{label}
|
>
|
||||||
</FormFieldLabel>
|
<InputGroupInput
|
||||||
) : null}
|
{...presetProps}
|
||||||
|
{...inputRest}
|
||||||
|
{...register(name)}
|
||||||
|
aria-invalid={!!fieldError}
|
||||||
|
className="placeholder:text-muted-foreground/50"
|
||||||
|
disabled={disabled}
|
||||||
|
id={inputId}
|
||||||
|
readOnly={readOnly}
|
||||||
|
required={required}
|
||||||
|
/>
|
||||||
|
|
||||||
<Popover>
|
{rightIcon && <InputGroupAddon aria-hidden="true">{rightIcon}</InputGroupAddon>}
|
||||||
<PopoverTrigger asChild>
|
</InputGroup>
|
||||||
<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" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className="truncate">{displayValue ?? placeholder ?? "Select date"}</span>
|
{description ? (
|
||||||
</Button>
|
<FieldDescription>{description}</FieldDescription>
|
||||||
</FormControl>
|
) : (
|
||||||
</PopoverTrigger>
|
<div aria-hidden="true" className="min-h-5" />
|
||||||
|
)}
|
||||||
|
|
||||||
{isDisabled ? null : (
|
<FieldError errors={[fieldError]} />
|
||||||
<PopoverContent align="start" className="w-auto p-0">
|
</Field>
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
// DateInputField.tsx
|
// DateInputField.tsx
|
||||||
import { CalendarIcon, LockIcon, XIcon } from "lucide-react";
|
import { CalendarIcon, LockIcon, XIcon } from "lucide-react";
|
||||||
|
|
||||||
export function DateInputField({
|
export function DateInputField({
|
||||||
id,
|
id,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onBlurConfirm,
|
onBlurConfirm,
|
||||||
onClear,
|
onClear,
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
readOnly,
|
readOnly,
|
||||||
required,
|
required,
|
||||||
hasError,
|
hasError,
|
||||||
describedBy,
|
describedBy,
|
||||||
onOpenRequest,
|
onOpenRequest,
|
||||||
triggerButton, // ← PopoverTrigger se inyecta aquí
|
triggerButton, // ← PopoverTrigger se inyecta aquí
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
onBlurConfirm: () => void; // valida+normaliza en blur/Enter
|
onBlurConfirm: () => void; // valida+normaliza en blur/Enter
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
describedBy?: string;
|
describedBy?: string;
|
||||||
onOpenRequest?: () => void;
|
onOpenRequest?: () => void;
|
||||||
triggerButton?: React.ReactNode;
|
triggerButton?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
aria-describedby={describedBy}
|
||||||
type="text"
|
aria-invalid={hasError || undefined}
|
||||||
pattern="\d{2}/\d{2}/\d{4}"
|
className={cn(
|
||||||
value={value}
|
"text-ellipsis pr-12",
|
||||||
onChange={(e) => onChange(e.target.value)}
|
disabled && "bg-muted text-muted-foreground cursor-not-allowed",
|
||||||
onBlur={onBlurConfirm}
|
readOnly && "bg-muted text-foreground cursor-default",
|
||||||
onKeyDown={(e) => {
|
!(disabled || readOnly) && "bg-white text-foreground",
|
||||||
if (e.key === "Enter") {
|
hasError && "border-destructive ring-destructive"
|
||||||
e.preventDefault();
|
)}
|
||||||
onBlurConfirm();
|
disabled={disabled}
|
||||||
}
|
id={id}
|
||||||
if ((e.altKey || e.metaKey) && e.key === "ArrowDown") {
|
onBlur={onBlurConfirm}
|
||||||
onOpenRequest?.();
|
onChange={(e) => onChange(e.target.value)}
|
||||||
}
|
onKeyDown={(e) => {
|
||||||
}}
|
if (e.key === "Enter") {
|
||||||
readOnly={readOnly}
|
e.preventDefault();
|
||||||
disabled={disabled}
|
onBlurConfirm();
|
||||||
aria-invalid={hasError || undefined}
|
}
|
||||||
aria-describedby={describedBy}
|
if ((e.altKey || e.metaKey) && e.key === "ArrowDown") {
|
||||||
className={cn(
|
onOpenRequest?.();
|
||||||
"text-ellipsis pr-12",
|
}
|
||||||
disabled && "bg-muted text-muted-foreground cursor-not-allowed",
|
}}
|
||||||
readOnly && "bg-muted text-foreground cursor-default",
|
pattern="\d{2}/\d{2}/\d{4}"
|
||||||
!disabled && !readOnly && "bg-white text-foreground",
|
placeholder={placeholder}
|
||||||
hasError && "border-destructive ring-destructive"
|
readOnly={readOnly}
|
||||||
)}
|
type="text"
|
||||||
placeholder={placeholder}
|
value={value}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-y-0 right-2 flex items-center gap-2 pr-1">
|
<div className="absolute inset-y-0 right-2 flex items-center gap-2 pr-1">
|
||||||
{!readOnly && !required && value && (
|
{!(readOnly || required) && value && (
|
||||||
<Button
|
<Button
|
||||||
variant={'link'}
|
aria-label="Clear date"
|
||||||
type='button'
|
className="text-muted-foreground hover:text-destructive transition cursor-pointer -mr-3"
|
||||||
size={"icon-sm"}
|
onClick={onClear}
|
||||||
onClick={onClear}
|
size={"icon-sm"}
|
||||||
aria-label="Clear date"
|
type="button"
|
||||||
className="text-muted-foreground hover:text-destructive transition cursor-pointer -mr-3"
|
variant={"link"}
|
||||||
>
|
>
|
||||||
<XIcon className="size-4" />
|
<XIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{readOnly ? (
|
{readOnly ? (
|
||||||
<LockIcon className="size-4" />
|
<LockIcon className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
triggerButton ?? <CalendarIcon className="size-4" aria-hidden="true" />
|
(triggerButton ?? <CalendarIcon aria-hidden="true" className="size-4" />)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,205 +1,231 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Calendar,
|
type Calendar,
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldError,
|
FieldError,
|
||||||
FieldLabel,
|
|
||||||
Popover,
|
Popover,
|
||||||
PopoverTrigger
|
PopoverTrigger,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { CalendarIcon } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { format, isValid, parse } from "date-fns";
|
import { format, isValid, parse } from "date-fns";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { CalendarIcon } from "lucide-react";
|
||||||
import { ControllerFieldState, ControllerRenderProps, FieldValues, Path, UseFormStateReturn } from "react-hook-form";
|
import * as React from "react";
|
||||||
|
import type {
|
||||||
|
ControllerFieldState,
|
||||||
|
ControllerRenderProps,
|
||||||
|
FieldValues,
|
||||||
|
Path,
|
||||||
|
} from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../locales/i18n.ts";
|
import { useTranslation } from "../../../locales/i18n.ts";
|
||||||
import { DateInputField } from './date-input-field.tsx';
|
import { FormFieldLabel } from "../form-field-label.tsx";
|
||||||
import { DatePopoverCalendar } from './date-popover-calendar.tsx';
|
|
||||||
|
|
||||||
|
import { DateInputField } from "./date-input-field.tsx";
|
||||||
|
import { DatePopoverCalendar } from "./date-popover-calendar.tsx";
|
||||||
|
|
||||||
export type SUICalendarProps = Omit<React.ComponentProps<
|
export type SUICalendarProps = Omit<
|
||||||
typeof Calendar>, "select" | "onSelect">
|
React.ComponentProps<typeof Calendar>,
|
||||||
|
"mode" | "selected" | "onSelect"
|
||||||
|
>;
|
||||||
|
|
||||||
type DatePickerInputCompProps<TFormValues extends FieldValues = FieldValues> = SUICalendarProps & {
|
type DatePickerInputCompProps<TFormValues extends FieldValues = FieldValues> = SUICalendarProps & {
|
||||||
field: ControllerRenderProps<TFormValues, Path<TFormValues>>;
|
field: ControllerRenderProps<TFormValues, Path<TFormValues>>;
|
||||||
fieldState: ControllerFieldState;
|
fieldState: ControllerFieldState;
|
||||||
formState: UseFormStateReturn<TFormValues>;
|
|
||||||
|
|
||||||
displayDateFormat: string; // e.g. "dd/MM/yyyy"
|
inputId: string;
|
||||||
parseDateFormat: string; // e.g. "yyyy/MM/dd"
|
|
||||||
|
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
invalid?: boolean;
|
|
||||||
|
|
||||||
orientation?: "vertical" | "horizontal" | "responsive",
|
orientation?: "vertical" | "horizontal" | "responsive";
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
|
inputClassName?: string;
|
||||||
|
|
||||||
|
displayDateFormat: string;
|
||||||
|
valueDateFormat: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DatePickerInputComp<TFormValues extends FieldValues = FieldValues>({
|
export function DatePickerInputComp<TFormValues extends FieldValues = FieldValues>({
|
||||||
field,
|
field,
|
||||||
fieldState,
|
fieldState,
|
||||||
formState,
|
inputId,
|
||||||
|
|
||||||
parseDateFormat,
|
|
||||||
displayDateFormat,
|
|
||||||
|
|
||||||
label,
|
label,
|
||||||
placeholder,
|
|
||||||
description,
|
description,
|
||||||
|
placeholder,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
required = false,
|
required = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
invalid = false,
|
|
||||||
|
|
||||||
orientation = "vertical",
|
orientation = "vertical",
|
||||||
|
|
||||||
className,
|
className,
|
||||||
|
inputClassName,
|
||||||
|
displayDateFormat,
|
||||||
|
valueDateFormat,
|
||||||
...calendarProps
|
...calendarProps
|
||||||
}: DatePickerInputCompProps<TFormValues>) {
|
}: DatePickerInputCompProps<TFormValues>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [displayValue, setDisplayValue] = useState("");
|
|
||||||
const [localError, setLocalError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const parsedDate = useMemo(() => {
|
const [open, setOpen] = React.useState(false);
|
||||||
if (!field.value) return null;
|
const [displayValue, setDisplayValue] = React.useState("");
|
||||||
const d = parse(field.value, parseDateFormat, new Date());
|
const [localError, setLocalError] = React.useState<string | null>(null);
|
||||||
return isValid(d) ? d : null;
|
|
||||||
}, [field.value, parseDateFormat]);
|
|
||||||
|
|
||||||
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) : "");
|
setDisplayValue(parsedDate ? format(parsedDate, displayDateFormat) : "");
|
||||||
}, [parsedDate, displayDateFormat]);
|
}, [parsedDate, displayDateFormat]);
|
||||||
|
|
||||||
const handleClear = useCallback(() => {
|
const handleClear = React.useCallback(() => {
|
||||||
field.onChange("");
|
field.onChange("");
|
||||||
|
field.onBlur();
|
||||||
setDisplayValue("");
|
setDisplayValue("");
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
}, [field]);
|
}, [field]);
|
||||||
|
|
||||||
const validateAndSet = useCallback(() => {
|
const confirmInputValue = React.useCallback(() => {
|
||||||
const trimmed = displayValue.trim();
|
const trimmedValue = displayValue.trim();
|
||||||
if (!trimmed) {
|
|
||||||
|
if (!trimmedValue) {
|
||||||
handleClear();
|
handleClear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = parse(trimmed, displayDateFormat, new Date());
|
|
||||||
if (isValid(d)) {
|
const parsed = parse(trimmedValue, displayDateFormat, new Date());
|
||||||
field.onChange(format(d, parseDateFormat));
|
|
||||||
setDisplayValue(format(d, displayDateFormat));
|
if (!isValid(parsed)) {
|
||||||
setLocalError(null);
|
|
||||||
} else {
|
|
||||||
setLocalError(t("components.date_picker_input_field.invalid_date"));
|
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 hasError = Boolean(localError || fieldState.error);
|
||||||
const describedById = description ? `${field.name}-desc` : undefined;
|
const describedBy =
|
||||||
const errorId = hasError ? `${field.name}-err` : undefined;
|
[descriptionId, hasError ? errorId : undefined].filter(Boolean).join(" ") || undefined;
|
||||||
const popoverId = `${field.name}-popover`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field data-invalid={invalid} orientation={orientation} className={cn("gap-1", className)}>
|
<Field className={cn("gap-1", className)} data-invalid={hasError} orientation={orientation}>
|
||||||
<div className="flex justify-between gap-2 overflow-hidden">
|
{label ? (
|
||||||
<div className="flex items-center gap-2 flex-nowrap">
|
<FormFieldLabel htmlFor={inputId} required={required}>
|
||||||
<FieldLabel htmlFor={field.name} className={cn("m-0 text-xs text-muted-foreground text-nowrap text-ellipsis", disabled && "text-muted-foreground")}>
|
{label}
|
||||||
{label}
|
</FormFieldLabel>
|
||||||
</FieldLabel>
|
) : null}
|
||||||
{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>
|
|
||||||
|
|
||||||
|
<div className={cn(inputClassName)}>
|
||||||
<Popover modal={true} open={open} onOpenChange={setOpen}>
|
<DateInputField
|
||||||
<PopoverTrigger asChild>
|
describedBy={describedBy}
|
||||||
|
disabled={disabled}
|
||||||
<DateInputField
|
hasError={hasError}
|
||||||
id={field.name}
|
id={inputId}
|
||||||
value={displayValue}
|
onBlurConfirm={confirmInputValue}
|
||||||
onChange={(v) => {
|
onChange={(value) => {
|
||||||
setDisplayValue(v);
|
setDisplayValue(value);
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
}}
|
}}
|
||||||
onBlurConfirm={validateAndSet}
|
onClear={handleClear}
|
||||||
onClear={handleClear}
|
placeholder={placeholder}
|
||||||
placeholder={placeholder}
|
readOnly={readOnly}
|
||||||
disabled={disabled}
|
required={required}
|
||||||
readOnly={readOnly}
|
triggerButton={
|
||||||
required={required}
|
disabled || readOnly ? null : (
|
||||||
hasError={hasError}
|
<Popover onOpenChange={setOpen} open={open}>
|
||||||
describedBy={[describedById, errorId].filter(Boolean).join(" ") || undefined}
|
|
||||||
onOpenRequest={() => setOpen(true)}
|
|
||||||
triggerButton={
|
|
||||||
!readOnly && !disabled && (
|
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<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}
|
aria-controls={popoverId}
|
||||||
onClick={() => setOpen((o) => !o)}
|
aria-expanded={open}
|
||||||
className="text-muted-foreground transition cursor-pointer -mr-3"
|
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" />
|
<CalendarIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</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 && (
|
{description ? (
|
||||||
<DatePopoverCalendar
|
<FieldDescription id={descriptionId}>{description}</FieldDescription>
|
||||||
contentId={popoverId}
|
) : (
|
||||||
label={label}
|
<div aria-hidden="true" className="min-h-5" />
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{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>
|
</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, type SUICalendarProps } from "./date-picker-input-comp.tsx";
|
||||||
import { DatePickerInputComp, SUICalendarProps } from "./date-picker-input-comp.tsx";
|
|
||||||
|
|
||||||
type DatePickerInputFieldProps<TFormValues extends FieldValues> = SUICalendarProps & {
|
type DatePickerInputFieldProps<TFormValues extends FieldValues> = SUICalendarProps & {
|
||||||
control: Control<TFormValues>;
|
|
||||||
name: FieldPath<TFormValues>;
|
name: FieldPath<TFormValues>;
|
||||||
|
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
||||||
displayDateFormat?: string; // e.g. "dd-MM-yyyy"
|
displayDateFormat?: string;
|
||||||
parseDateFormat?: string; // e.g. "yyyy-MM-dd"
|
valueDateFormat?: string;
|
||||||
orientation?: "vertical" | "horizontal" | "responsive",
|
|
||||||
|
orientation?: "vertical" | "horizontal" | "responsive";
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
inputClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DatePickerInputField<TFormValues extends FieldValues>({
|
export function DatePickerInputField<TFormValues extends FieldValues>({
|
||||||
control,
|
|
||||||
name,
|
name,
|
||||||
displayDateFormat = "dd-MM-yyyy",
|
displayDateFormat = "dd/MM/yyyy",
|
||||||
parseDateFormat = "yyyy-MM-dd",
|
valueDateFormat = "yyyy-MM-dd",
|
||||||
|
inputClassName,
|
||||||
...props
|
...props
|
||||||
}: DatePickerInputFieldProps<TFormValues>) {
|
}: DatePickerInputFieldProps<TFormValues>) {
|
||||||
|
const { control } = useFormContext<TFormValues>();
|
||||||
|
const inputId = React.useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field, fieldState, formState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<DatePickerInputComp
|
<DatePickerInputComp
|
||||||
|
{...props}
|
||||||
|
displayDateFormat={displayDateFormat}
|
||||||
field={field}
|
field={field}
|
||||||
fieldState={fieldState}
|
fieldState={fieldState}
|
||||||
formState={formState}
|
inputClassName={cn(
|
||||||
|
"bg-muted/50 font-medium",
|
||||||
displayDateFormat={displayDateFormat}
|
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||||
parseDateFormat={parseDateFormat}
|
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||||
{...props}
|
"placeholder:text-muted-foreground/50",
|
||||||
|
inputClassName
|
||||||
|
)}
|
||||||
|
inputId={inputId}
|
||||||
|
valueDateFormat={valueDateFormat}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,76 +1,67 @@
|
|||||||
// DatePopoverCalendar.tsx
|
// DatePopoverCalendar.tsx
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Calendar,
|
Calendar,
|
||||||
Card,
|
Card,
|
||||||
CardAction,
|
CardAction,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../../locales/i18n.ts";
|
import { useTranslation } from "../../../locales/i18n.ts";
|
||||||
|
|
||||||
export function DatePopoverCalendar({
|
export function DatePopoverCalendar({
|
||||||
contentId,
|
contentId,
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
parsedDate,
|
parsedDate,
|
||||||
onSelect,
|
onSelect,
|
||||||
onToday,
|
onToday,
|
||||||
onClose,
|
onClose,
|
||||||
...calendarProps
|
...calendarProps
|
||||||
}: {
|
}: {
|
||||||
contentId: string;
|
contentId: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
parsedDate: Date | null;
|
parsedDate: Date | null;
|
||||||
onSelect: (date: Date | undefined) => void;
|
onSelect: (date: Date | undefined) => void;
|
||||||
onToday: () => void;
|
onToday: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
} & Omit<React.ComponentProps<typeof Calendar>, "mode" | "selected" | "onSelect">) {
|
} & Omit<React.ComponentProps<typeof Calendar>, "mode" | "selected" | "onSelect">) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<PopoverContent id={contentId} className="w-auto p-0">
|
<PopoverContent className="w-auto p-0" id={contentId}>
|
||||||
<Card className="border-none shadow-none">
|
<Card className="border-none shadow-none">
|
||||||
<CardHeader className="border-b">
|
<CardHeader className="border-b">
|
||||||
<CardTitle>{label}</CardTitle>
|
<CardTitle>{label}</CardTitle>
|
||||||
<CardDescription>{description || "\u00A0"}</CardDescription>
|
<CardDescription>{description || "\u00A0"}</CardDescription>
|
||||||
<CardAction>
|
<CardAction>
|
||||||
<Button
|
<Button onClick={onToday} size="sm" variant="outline">
|
||||||
size="sm"
|
{t("components.date_picker_input_field.today")}
|
||||||
variant="outline"
|
</Button>
|
||||||
onClick={onToday}
|
</CardAction>
|
||||||
>
|
</CardHeader>
|
||||||
{t("components.date_picker_input_field.today")}
|
|
||||||
</Button>
|
|
||||||
</CardAction>
|
|
||||||
|
|
||||||
</CardHeader>
|
<CardContent className="px-0">
|
||||||
|
<Calendar
|
||||||
|
defaultMonth={parsedDate ?? undefined}
|
||||||
|
mode="single"
|
||||||
|
numberOfMonths={2}
|
||||||
|
onSelect={onSelect}
|
||||||
|
selected={parsedDate ?? undefined}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
<CardContent className='px-0'>
|
<CardFooter className="mx-auto">
|
||||||
<Calendar
|
<Button onClick={onClose} size="sm" variant="outline">
|
||||||
selected={parsedDate ?? undefined}
|
{t("components.date_picker_input_field.close")}
|
||||||
defaultMonth={parsedDate ?? undefined}
|
</Button>
|
||||||
onSelect={onSelect}
|
</CardFooter>
|
||||||
mode='single'
|
</Card>
|
||||||
numberOfMonths={2}
|
</PopoverContent>
|
||||||
|
);
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<CardFooter className='mx-auto'>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onClose}>
|
|
||||||
{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-invalid={fieldState.invalid}
|
||||||
aria-required={required || undefined}
|
aria-required={required || undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-h-10 h-auto w-full justify-between bg-muted/50 p-1 text-left font-medium",
|
"min-h-10 h-auto w-full justify-between text-left ",
|
||||||
"hover:border-ring/60",
|
"bg-muted/50 p-1 font-medium",
|
||||||
"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
|
inputClassName
|
||||||
)}
|
)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
|||||||
@ -89,8 +89,9 @@ export function SelectField<TFormValues extends FieldValues>({
|
|||||||
aria-required={required}
|
aria-required={required}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted/50 font-medium",
|
"bg-muted/50 font-medium",
|
||||||
"hover:border-ring/60",
|
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||||
|
"placeholder:text-muted-foreground/50",
|
||||||
inputClassName
|
inputClassName
|
||||||
)}
|
)}
|
||||||
id={triggerId}
|
id={triggerId}
|
||||||
|
|||||||
@ -66,8 +66,8 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
|||||||
aria-invalid={!!fieldError}
|
aria-invalid={!!fieldError}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted/50 font-medium",
|
"bg-muted/50 font-medium",
|
||||||
"hover:border-ring/60",
|
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||||
"placeholder:text-muted-foreground/50",
|
"placeholder:text-muted-foreground/50",
|
||||||
inputClassName
|
inputClassName
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -72,8 +72,9 @@ export const TextField = <TFormValues extends FieldValues>({
|
|||||||
<InputGroup
|
<InputGroup
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted/50 font-medium",
|
"bg-muted/50 font-medium",
|
||||||
"hover:border-ring/60",
|
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||||
|
"placeholder:text-muted-foreground/50",
|
||||||
inputClassName
|
inputClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export const AppLayout = () => {
|
|||||||
>
|
>
|
||||||
<AppSidebar className="bg-sidebar" variant="inset" />
|
<AppSidebar className="bg-sidebar" variant="inset" />
|
||||||
{/* Aquí está el MAIN */}
|
{/* Aquí está el MAIN */}
|
||||||
<SidebarInset className="app-main bg-muted">
|
<SidebarInset className="app-main bg-muted overflow-hidden">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
@ -62,8 +62,10 @@
|
|||||||
"date_picker_input_field": {
|
"date_picker_input_field": {
|
||||||
"invalid_date": "Fecha inválida",
|
"invalid_date": "Fecha inválida",
|
||||||
"clear_date": "Limpiar fecha",
|
"clear_date": "Limpiar fecha",
|
||||||
|
"open_calendar": "Abrir calendario",
|
||||||
"today": "Hoy",
|
"today": "Hoy",
|
||||||
"close": "Cerrar"
|
"close": "Cerrar",
|
||||||
|
"select_date": "Seleccionar fecha"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@
|
|||||||
--accent-foreground: oklch(0.97 0 0);
|
--accent-foreground: oklch(0.97 0 0);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.922 0 0);
|
--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); */
|
--ring: oklch(0.575 0.1533 256.4357); /* oklch(0.708 0 0); */
|
||||||
--chart-1: oklch(0.809 0.105 251.813);
|
--chart-1: oklch(0.809 0.105 251.813);
|
||||||
--chart-2: oklch(0.623 0.214 259.815);
|
--chart-2: oklch(0.623 0.214 259.815);
|
||||||
@ -149,7 +149,7 @@
|
|||||||
--sidebar-border: oklch(0.6427 0.1407 253.94);
|
--sidebar-border: oklch(0.6427 0.1407 253.94);
|
||||||
--sidebar-ring: oklch(1 0 0);
|
--sidebar-ring: oklch(1 0 0);
|
||||||
|
|
||||||
--radius: 0.325rem;
|
--radius: 0.625rem;
|
||||||
--shadow-x: 0;
|
--shadow-x: 0;
|
||||||
--shadow-y: 1px;
|
--shadow-y: 1px;
|
||||||
--shadow-blur: 3px;
|
--shadow-blur: 3px;
|
||||||
@ -200,7 +200,7 @@
|
|||||||
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
||||||
--sidebar-border: oklch(1 0 0);
|
--sidebar-border: oklch(1 0 0);
|
||||||
--sidebar-ring: oklch(0.4915 0.2776 263.8724);
|
--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-2xs: 1px 1px 6px 0px hsl(0 0% 0% / 0.05);
|
||||||
--shadow-xs: 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);
|
--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