This commit is contained in:
David Arranz 2026-04-09 20:39:54 +02:00
parent bd209374bc
commit b88632d395
25 changed files with 1175 additions and 644 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from "./proforma-update-header-editor";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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