Facturas de cliente

This commit is contained in:
David Arranz 2025-07-09 19:56:15 +02:00
parent a5af168e6b
commit 1d1f412e6c
21 changed files with 500 additions and 98 deletions

View File

@ -57,6 +57,11 @@
"placeholder": "Select a date", "placeholder": "Select a date",
"description": "Invoice issue date" "description": "Invoice issue date"
}, },
"invoice_series": {
"label": "Serie",
"placeholder": "",
"description": ""
},
"operation_date": { "operation_date": {
"label": "Operation date", "label": "Operation date",
"placeholder": "Select a date", "placeholder": "Select a date",

View File

@ -57,6 +57,11 @@
"placeholder": "Seleccionar una fecha", "placeholder": "Seleccionar una fecha",
"description": "Fecha de emisión de la factura" "description": "Fecha de emisión de la factura"
}, },
"invoice_series": {
"label": "Serie",
"placeholder": "",
"description": ""
},
"operation_date": { "operation_date": {
"label": "Intervención", "label": "Intervención",
"placeholder": "Seleccionar una fecha", "placeholder": "Seleccionar una fecha",

View File

@ -1,8 +1,7 @@
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { PlusCircleIcon } from "lucide-react"; import { PlusCircleIcon } from "lucide-react";
import { JSX, forwardRef } from "react"; import { JSX, forwardRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "../../i18n";
import { MODULE_NAME } from "../../manifest";
export interface AppendEmptyRowButtonProps extends React.ComponentProps<typeof Button> { export interface AppendEmptyRowButtonProps extends React.ComponentProps<typeof Button> {
label?: string; label?: string;
@ -11,7 +10,7 @@ export interface AppendEmptyRowButtonProps extends React.ComponentProps<typeof B
export const AppendEmptyRowButton = forwardRef<HTMLButtonElement, AppendEmptyRowButtonProps>( export const AppendEmptyRowButton = forwardRef<HTMLButtonElement, AppendEmptyRowButtonProps>(
({ label, className, ...props }: AppendEmptyRowButtonProps, ref): JSX.Element => { ({ label, className, ...props }: AppendEmptyRowButtonProps, ref): JSX.Element => {
const { t } = useTranslation(MODULE_NAME); const { t } = useTranslation();
const _label = label || t("common.append_empty_row"); const _label = label || t("common.append_empty_row");
return ( return (

View File

@ -1,8 +1,7 @@
import { Badge } from "@repo/shadcn-ui/components"; import { Badge } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "../i18n";
import { MODULE_NAME } from "../manifest";
export type CustomerInvoiceStatus = "draft" | "emitted" | "sent" | "received" | "rejected"; export type CustomerInvoiceStatus = "draft" | "emitted" | "sent" | "received" | "rejected";
@ -42,7 +41,7 @@ export const CustomerInvoiceStatusBadge = forwardRef<
HTMLDivElement, HTMLDivElement,
CustomerInvoiceStatusBadgeProps CustomerInvoiceStatusBadgeProps
>(({ status, className, ...props }, ref) => { >(({ status, className, ...props }, ref) => {
const { t } = useTranslation(MODULE_NAME); const { t } = useTranslation();
const normalizedStatus = status.toLowerCase() as CustomerInvoiceStatus; const normalizedStatus = status.toLowerCase() as CustomerInvoiceStatus;
const config = statusColorConfig[normalizedStatus]; const config = statusColorConfig[normalizedStatus];
const commonClassName = "transition-colors duration-200 cursor-pointer shadow-none rounded-full"; const commonClassName = "transition-colors duration-200 cursor-pointer shadow-none rounded-full";

View File

@ -11,14 +11,12 @@ import { MoneyDTO } from "@erp/core";
import { formatDate, formatMoney } from "@erp/core/client"; import { formatDate, formatMoney } from "@erp/core/client";
// Core CSS // Core CSS
import { AgGridReact } from "ag-grid-react"; import { AgGridReact } from "ag-grid-react";
import { useTranslation } from "react-i18next";
import { useCustomerInvoicesQuery } from "../hooks"; import { useCustomerInvoicesQuery } from "../hooks";
import { MODULE_NAME } from "../manifest";
import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge"; import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge";
// Create new GridExample component // Create new GridExample component
export const CustomerInvoicesListGrid = () => { export const CustomerInvoicesListGrid = () => {
const { t } = useTranslation(MODULE_NAME); const { t } = useTranslation();
const { data, isLoading, isPending, isError, error } = useCustomerInvoicesQuery({}); const { data, isLoading, isPending, isError, error } = useCustomerInvoicesQuery({});
// Column Definitions: Defines & controls grid columns. // Column Definitions: Defines & controls grid columns.

View File

@ -1,19 +1,12 @@
import { import { FormControl, FormField, FormItem, FormMessage, Input } from "@repo/shadcn-ui/components";
FormControl,
FormField,
FormItem,
FormMessage,
Input,
Textarea,
} from "@repo/shadcn-ui/components";
import { TextAreaField } from "@repo/rdx-ui/components";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react"; import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useFieldArray, useFormContext } from "react-hook-form"; import { useFieldArray, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useDetailColumns } from "../../hooks"; import { useDetailColumns } from "../../hooks";
import { MODULE_NAME } from "../../manifest"; import { useTranslation } from "../../i18n";
import { formatCurrency } from "../../pages/create/utils"; import { formatCurrency } from "../../pages/create/utils";
import { import {
CustomerInvoiceItemsSortableDataTable, CustomerInvoiceItemsSortableDataTable,
@ -29,7 +22,7 @@ export const CustomerInvoiceItemsCardEditor = ({
//language: Language; //language: Language;
defaultValues: Readonly<{ [x: string]: any }> | undefined; defaultValues: Readonly<{ [x: string]: any }> | undefined;
}) => { }) => {
const { t } = useTranslation(MODULE_NAME); const { t } = useTranslation();
const { control, watch, getValues } = useFormContext(); const { control, watch, getValues } = useFormContext();
@ -78,20 +71,11 @@ export const CustomerInvoiceItemsCardEditor = ({
accessorKey: "description", accessorKey: "description",
header: t("form_fields.items.description.label"), header: t("form_fields.items.description.label"),
cell: ({ row: { index, original } }) => ( cell: ({ row: { index, original } }) => (
<FormField <TextAreaField
control={control} control={control}
name={`items.${index}.description`} name={`items.${index}.description`}
render={({ field }) => ( placeholder={t("form_fields.items.description.placeholder")}
<FormItem className='md:col-span-2'> className='md:col-span-2'
<FormControl>
<Textarea
placeholder={t("form_fields.items.description.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/> />
), ),
minSize: 200, minSize: 200,

View File

@ -0,0 +1,28 @@
import { useEffect } from "react";
import { useTranslation as useI18NextTranslation } from "react-i18next";
import enResources from "../common/locales/en.json";
import esResources from "../common/locales/es.json";
import { MODULE_NAME } from "./manifest";
const addMissingBundles = (i18n: any) => {
const needsEn = !i18n.hasResourceBundle("en", MODULE_NAME);
const needsEs = !i18n.hasResourceBundle("es", MODULE_NAME);
if (needsEn) {
i18n.addResourceBundle("en", MODULE_NAME, enResources, true, true);
}
if (needsEs) {
i18n.addResourceBundle("es", MODULE_NAME, esResources, true, true);
}
};
export const useTranslation = () => {
const { i18n } = useI18NextTranslation();
useEffect(() => {
addMissingBundles(i18n);
}, [i18n]);
return useI18NextTranslation(MODULE_NAME);
};

View File

@ -1,13 +1,12 @@
import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components"; import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useCreateCustomerInvoiceMutation } from "../../hooks"; import { useCreateCustomerInvoiceMutation } from "../../hooks";
import { MODULE_NAME } from "../../manifest"; import { useTranslation } from "../../i18n";
import { CustomerInvoiceEditForm } from "./customer-invoice-edit-form"; import { CustomerInvoiceEditForm } from "./customer-invoice-edit-form";
export const CustomerInvoiceCreate = () => { export const CustomerInvoiceCreate = () => {
const { t } = useTranslation(MODULE_NAME); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { mutate, isPending, isError, error } = useCreateCustomerInvoiceMutation(); const { mutate, isPending, isError, error } = useCreateCustomerInvoiceMutation();

View File

@ -4,7 +4,7 @@ import * as z from "zod";
import { ClientSelector } from "@erp/customers/components"; import { ClientSelector } from "@erp/customers/components";
import { DatePickerField } from "@repo/rdx-ui/components"; import { DatePickerInputField, TextAreaField, TextField } from "@repo/rdx-ui/components";
import { import {
Button, Button,
Calendar, Calendar,
@ -35,9 +35,8 @@ import {
import { format } from "date-fns"; import { format } from "date-fns";
import { es } from "date-fns/locale"; import { es } from "date-fns/locale";
import { CalendarIcon, PlusIcon, Save, Trash2Icon, X } from "lucide-react"; import { CalendarIcon, PlusIcon, Save, Trash2Icon, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CustomerInvoiceItemsCardEditor } from "../../components/items"; import { CustomerInvoiceItemsCardEditor } from "../../components/items";
import { MODULE_NAME } from "../../manifest"; import { useTranslation } from "../../i18n";
import { CustomerInvoiceData } from "./customer-invoice.schema"; import { CustomerInvoiceData } from "./customer-invoice.schema";
import { formatCurrency } from "./utils"; import { formatCurrency } from "./utils";
@ -219,7 +218,7 @@ export const CustomerInvoiceEditForm = ({
onSubmit, onSubmit,
isPending, isPending,
}: InvoiceFormProps) => { }: InvoiceFormProps) => {
const { t } = useTranslation(MODULE_NAME); const { t } = useTranslation();
const form = useForm<CustomerInvoiceData>({ const form = useForm<CustomerInvoiceData>({
resolver: zodResolver(invoiceSchema), resolver: zodResolver(invoiceSchema),
@ -271,18 +270,13 @@ export const CustomerInvoiceEditForm = ({
</CardHeader> </CardHeader>
<CardContent className='grid grid-cols-1 gap-4 space-y-6'> <CardContent className='grid grid-cols-1 gap-4 space-y-6'>
<ClientSelector /> <ClientSelector />
<FormField <TextField
control={form.control} control={form.control}
name='customer_id' name='customer_id'
render={({ field }) => ( required
<FormItem> label={t("form_fields.customer_id.label")}
<FormLabel>ID Cliente</FormLabel> placeholder={t("form_fields.customer_id.placeholder")}
<FormControl> description={t("form_fields.customer_id.description")}
<Input placeholder='ID del cliente' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/> />
</CardContent> </CardContent>
</Card> </Card>
@ -293,22 +287,19 @@ export const CustomerInvoiceEditForm = ({
<CardTitle>Información Básica</CardTitle> <CardTitle>Información Básica</CardTitle>
<CardDescription>Detalles generales de la factura</CardDescription> <CardDescription>Detalles generales de la factura</CardDescription>
</CardHeader> </CardHeader>
<CardContent className='grid gap-6 md:grid-cols-6'> <CardContent className='grid gap-y-6 gap-x-8 md:grid-cols-4'>
<FormField <TextField
control={form.control} control={form.control}
name='invoice_number' name='invoice_number'
render={({ field }) => ( required
<FormItem> disabled
<FormLabel>{t("form_fields.invoice_number.label")}</FormLabel> readOnly
<FormControl> label={t("form_fields.invoice_number.label")}
<Input placeholder={t("form_fields.invoice_number.placeholder")} {...field} /> placeholder={t("form_fields.invoice_number.placeholder")}
</FormControl> description={t("form_fields.invoice_number.description")}
<FormMessage />
</FormItem>
)}
/> />
<DatePickerField <DatePickerInputField
control={form.control} control={form.control}
name='issue_date' name='issue_date'
required required
@ -317,18 +308,13 @@ export const CustomerInvoiceEditForm = ({
description={t("form_fields.issue_date.description")} description={t("form_fields.issue_date.description")}
/> />
<FormField <TextField
control={form.control} control={form.control}
name='invoice_series' name='invoice_series'
render={({ field }) => ( required
<FormItem> label={t("form_fields.invoice_series.label")}
<FormLabel>Serie</FormLabel> placeholder={t("form_fields.invoice_series.placeholder")}
<FormControl> description={t("form_fields.invoice_series.description")}
<Input placeholder='A' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/> />
</CardContent> </CardContent>
</Card> </Card>
@ -391,6 +377,14 @@ export const CustomerInvoiceEditForm = ({
)} )}
/> />
<TextAreaField
control={form.control}
name={`items.${index}.description`}
label={t("form_fields.items.description.label")}
placeholder={t("form_fields.items.description.placeholder")}
description={t("form_fields.items.description.description")}
/>
<FormField <FormField
control={form.control} control={form.control}
name={`items.${index}.quantity.amount`} name={`items.${index}.quantity.amount`}

View File

@ -2,13 +2,12 @@ import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { CustomerInvoicesListGrid } from "../components"; import { CustomerInvoicesListGrid } from "../components";
import { MODULE_NAME } from "../manifest"; import { useTranslation } from "../i18n";
export const CustomerInvoicesList = () => { export const CustomerInvoicesList = () => {
const { t } = useTranslation(MODULE_NAME); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [status, setStatus] = useState("all"); const [status, setStatus] = useState("all");

View File

@ -1,3 +1,4 @@
import { useTranslation } from "@repo/rdx-ui/locales/i18n.ts";
import { import {
Button, Button,
DropdownMenu, DropdownMenu,
@ -9,7 +10,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { CellContext } from "@tanstack/react-table"; import { CellContext } from "@tanstack/react-table";
import { t } from "i18next";
import { MoreVerticalIcon } from "lucide-react"; import { MoreVerticalIcon } from "lucide-react";
import { ReactElement } from "react"; import { ReactElement } from "react";
@ -34,6 +34,8 @@ export function DataTableRowActions<TData = any, TValue = unknown>({
actions, actions,
rowContext, rowContext,
}: DataTableRowActionsProps<TData, TValue>) { }: DataTableRowActionsProps<TData, TValue>) {
const { t } = useTranslation();
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>

View File

@ -12,21 +12,22 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { CalendarIcon } from "lucide-react"; import { CalendarIcon, LockIcon } from "lucide-react";
import { useTranslation } from "@repo/rdx-ui/locales/i18n.ts";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { format } from "date-fns"; import { format } from "date-fns";
import { Control, FieldPath, FieldValues } from "react-hook-form"; import { Control, FieldPath, FieldValues } from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts";
type DatePickerFieldProps<TFormValues extends FieldValues> = { type DatePickerFieldProps<TFormValues extends FieldValues> = {
control: Control<TFormValues>; control: Control<TFormValues>;
name: FieldPath<TFormValues>; name: FieldPath<TFormValues>;
label: string; label?: string;
placeholder?: string; placeholder?: string;
description?: string; description?: string;
disabled?: boolean; disabled?: boolean;
required?: boolean; required?: boolean;
readOnly?: boolean;
className?: string; className?: string;
formatDateFn?: (iso: string) => string; formatDateFn?: (iso: string) => string;
}; };
@ -39,32 +40,43 @@ export function DatePickerField<TFormValues extends FieldValues>({
description, description,
disabled = false, disabled = false,
required = false, required = false,
readOnly = false,
className, className,
formatDateFn = (iso) => format(new Date(iso), "dd/MM/yyyy"), formatDateFn = (iso) => format(new Date(iso), "dd/MM/yyyy"),
}: DatePickerFieldProps<TFormValues>) { }: DatePickerFieldProps<TFormValues>) {
const { t } = useTranslation(); const { t } = useTranslation();
const isDisabled = disabled || readOnly;
return ( return (
<FormField <FormField
control={control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-2", className)}> <FormItem className={cn("space-y-2", className)}>
<div className='flex justify-between items-center'> {label && (
<FormLabel className='m-0'>{label}</FormLabel> <div className='flex justify-between items-center'>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>} <FormLabel className='m-0'>{label}</FormLabel>
</div>{" "} {required && <span className='text-xs text-destructive'>{t("common.required")}</span>}
</div>
)}
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<FormControl> <FormControl>
<Button <Button
variant='outline' variant='outline'
disabled={disabled} disabled={isDisabled}
className={cn( className={cn(
"w-full justify-start text-left font-normal", "w-full justify-start text-left font-normal",
!field.value && "text-muted-foreground" !field.value && "text-muted-foreground",
disabled && "bg-muted text-muted-foreground cursor-not-allowed",
readOnly && !disabled && "bg-muted text-foreground cursor-default"
)} )}
> >
<CalendarIcon className='mr-2 h-4 w-4' /> {readOnly ? (
<LockIcon className='mr-2 h-4 w-4 opacity-70' />
) : (
<CalendarIcon className='mr-2 h-4 w-4' />
)}
{field.value ? formatDateFn(field.value) : placeholder} {field.value ? formatDateFn(field.value) : placeholder}
</Button> </Button>
</FormControl> </FormControl>
@ -73,11 +85,16 @@ export function DatePickerField<TFormValues extends FieldValues>({
<Calendar <Calendar
mode='single' mode='single'
selected={field.value ? new Date(field.value) : undefined} selected={field.value ? new Date(field.value) : undefined}
onSelect={(date) => field.onChange(date?.toISOString())} onSelect={(date) => {
if (!readOnly) {
field.onChange(date?.toISOString());
}
}}
initialFocus initialFocus
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}> <p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
{description || "\u00A0"} {description || "\u00A0"}
</p> </p>

View File

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

View File

@ -0,0 +1,64 @@
// DatePickerField.tsx
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { Control, FieldPath, FieldValues } from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts";
type NumberFieldProps<TFormValues extends FieldValues> = {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label: string;
placeholder?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
className?: string;
};
export function NumberField<TFormValues extends FieldValues>({
control,
name,
label,
placeholder,
description,
disabled = false,
required = false,
readOnly = false,
className,
}: NumberFieldProps<TFormValues>) {
const { t } = useTranslation();
const isDisabled = disabled || readOnly;
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className={cn("space-y-2", className)}>
<div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>}
</div>
<FormControl>
<Input disabled={isDisabled} placeholder={placeholder} {...field} />
</FormControl>
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
{description || "\u00A0"}
</p>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@ -0,0 +1,66 @@
// DatePickerField.tsx
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Textarea,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { Control, FieldPath, FieldValues } from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts";
type TextAreaFieldProps<TFormValues extends FieldValues> = {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label?: string;
placeholder?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
className?: string;
};
export function TextAreaField<TFormValues extends FieldValues>({
control,
name,
label,
placeholder,
description,
disabled = false,
required = false,
readOnly = false,
className,
}: TextAreaFieldProps<TFormValues>) {
const { t } = useTranslation();
const isDisabled = disabled || readOnly;
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className={cn("space-y-2", className)}>
{label && (
<div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>}
</div>
)}
<FormControl>
<Textarea disabled={isDisabled} placeholder={placeholder} {...field} />
</FormControl>
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
{description || "\u00A0"}
</p>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@ -0,0 +1,66 @@
// DatePickerField.tsx
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { Control, FieldPath, FieldValues } from "react-hook-form";
import { useTranslation } from "../../locales/i18n.ts";
type TextFieldProps<TFormValues extends FieldValues> = {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label?: string;
placeholder?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
className?: string;
};
export function TextField<TFormValues extends FieldValues>({
control,
name,
label,
placeholder,
description,
disabled = false,
required = false,
readOnly = false,
className,
}: TextFieldProps<TFormValues>) {
const { t } = useTranslation();
const isDisabled = disabled || readOnly;
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className={cn("space-y-2", className)}>
{label && (
<div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>}
</div>
)}
<FormControl>
<Input disabled={isDisabled} placeholder={placeholder} {...field} />
</FormControl>
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
{description || "\u00A0"}
</p>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@ -1 +1,4 @@
export * from "./DatePickerField.tsx"; export * from "./DatePickerField.tsx";
export * from "./DatePickerInputField.tsx";
export * from "./TextAreaField.tsx";
export * from "./TextField.tsx";

View File

@ -1,5 +1,7 @@
{ {
"common": { "common": {
"actions": "Actions",
"invalid_date": "Invalid date",
"required": "required" "required": "required"
}, },
"components": { "components": {

View File

@ -1,5 +1,7 @@
{ {
"common": { "common": {
"actions": "Actions",
"invalid_date": "Fecha incorrecta o no válida",
"required": "obligatorio" "required": "obligatorio"
}, },
"components": { "components": {

View File

@ -1,14 +1,28 @@
import i18next from "i18next"; import { useEffect } from "react";
import { useTranslation as useTrans } from "react-i18next"; import { useTranslation as useI18NextTranslation } from "react-i18next";
import { PACKAGE_NAME } from "../index.ts"; import { PACKAGE_NAME } from "../index.ts";
import enResources from "./en.json" with { type: "json" }; import enResources from "./en.json" with { type: "json" };
import esResources from "./es.json" with { type: "json" }; import esResources from "./es.json" with { type: "json" };
export const useTranslation = () => { const addMissingBundles = (i18n: any) => {
if (!i18next.hasLoadedNamespace(PACKAGE_NAME)) { const needsEn = !i18n.hasResourceBundle("en", PACKAGE_NAME);
i18next.addResourceBundle("en", PACKAGE_NAME, enResources, true, true); const needsEs = !i18n.hasResourceBundle("es", PACKAGE_NAME);
i18next.addResourceBundle("es", PACKAGE_NAME, esResources, true, true);
if (needsEn) {
i18n.addResourceBundle("en", PACKAGE_NAME, enResources, true, true);
} }
return useTrans(PACKAGE_NAME); if (needsEs) {
i18n.addResourceBundle("es", PACKAGE_NAME, esResources, true, true);
}
};
export const useTranslation = () => {
const { i18n } = useI18NextTranslation();
useEffect(() => {
addMissingBundles(i18n);
}, [i18n]);
return useI18NextTranslation(PACKAGE_NAME);
}; };

View File

@ -1,13 +1,14 @@
import * as React from "react" import * as React from "react";
import { cn } from "@repo/shadcn-ui/lib/utils" import { cn } from "@repo/shadcn-ui/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return ( return (
<input <input
type={type} type={type}
data-slot="input" data-slot='input'
className={cn( className={cn(
"bg-background text-foreground",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
@ -15,7 +16,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
)} )}
{...props} {...props}
/> />
) );
} }
export { Input } export { Input };