Facturas de cliente
This commit is contained in:
parent
a5af168e6b
commit
1d1f412e6c
@ -57,6 +57,11 @@
|
||||
"placeholder": "Select a date",
|
||||
"description": "Invoice issue date"
|
||||
},
|
||||
"invoice_series": {
|
||||
"label": "Serie",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"operation_date": {
|
||||
"label": "Operation date",
|
||||
"placeholder": "Select a date",
|
||||
|
||||
@ -57,6 +57,11 @@
|
||||
"placeholder": "Seleccionar una fecha",
|
||||
"description": "Fecha de emisión de la factura"
|
||||
},
|
||||
"invoice_series": {
|
||||
"label": "Serie",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"operation_date": {
|
||||
"label": "Intervención",
|
||||
"placeholder": "Seleccionar una fecha",
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import { JSX, forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MODULE_NAME } from "../../manifest";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
export interface AppendEmptyRowButtonProps extends React.ComponentProps<typeof Button> {
|
||||
label?: string;
|
||||
@ -11,7 +10,7 @@ export interface AppendEmptyRowButtonProps extends React.ComponentProps<typeof B
|
||||
|
||||
export const AppendEmptyRowButton = forwardRef<HTMLButtonElement, AppendEmptyRowButtonProps>(
|
||||
({ label, className, ...props }: AppendEmptyRowButtonProps, ref): JSX.Element => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
const { t } = useTranslation();
|
||||
const _label = label || t("common.append_empty_row");
|
||||
|
||||
return (
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MODULE_NAME } from "../manifest";
|
||||
import { useTranslation } from "../i18n";
|
||||
|
||||
export type CustomerInvoiceStatus = "draft" | "emitted" | "sent" | "received" | "rejected";
|
||||
|
||||
@ -42,7 +41,7 @@ export const CustomerInvoiceStatusBadge = forwardRef<
|
||||
HTMLDivElement,
|
||||
CustomerInvoiceStatusBadgeProps
|
||||
>(({ status, className, ...props }, ref) => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
const { t } = useTranslation();
|
||||
const normalizedStatus = status.toLowerCase() as CustomerInvoiceStatus;
|
||||
const config = statusColorConfig[normalizedStatus];
|
||||
const commonClassName = "transition-colors duration-200 cursor-pointer shadow-none rounded-full";
|
||||
|
||||
@ -11,14 +11,12 @@ import { MoneyDTO } from "@erp/core";
|
||||
import { formatDate, formatMoney } from "@erp/core/client";
|
||||
// Core CSS
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCustomerInvoicesQuery } from "../hooks";
|
||||
import { MODULE_NAME } from "../manifest";
|
||||
import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge";
|
||||
|
||||
// Create new GridExample component
|
||||
export const CustomerInvoicesListGrid = () => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isPending, isError, error } = useCustomerInvoicesQuery({});
|
||||
|
||||
// Column Definitions: Defines & controls grid columns.
|
||||
|
||||
@ -1,19 +1,12 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
Input,
|
||||
Textarea,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { FormControl, FormField, FormItem, FormMessage, Input } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { TextAreaField } from "@repo/rdx-ui/components";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDetailColumns } from "../../hooks";
|
||||
import { MODULE_NAME } from "../../manifest";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { formatCurrency } from "../../pages/create/utils";
|
||||
import {
|
||||
CustomerInvoiceItemsSortableDataTable,
|
||||
@ -29,7 +22,7 @@ export const CustomerInvoiceItemsCardEditor = ({
|
||||
//language: Language;
|
||||
defaultValues: Readonly<{ [x: string]: any }> | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { control, watch, getValues } = useFormContext();
|
||||
|
||||
@ -78,20 +71,11 @@ export const CustomerInvoiceItemsCardEditor = ({
|
||||
accessorKey: "description",
|
||||
header: t("form_fields.items.description.label"),
|
||||
cell: ({ row: { index, original } }) => (
|
||||
<FormField
|
||||
<TextAreaField
|
||||
control={control}
|
||||
name={`items.${index}.description`}
|
||||
render={({ field }) => (
|
||||
<FormItem className='md:col-span-2'>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t("form_fields.items.description.placeholder")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
placeholder={t("form_fields.items.description.placeholder")}
|
||||
className='md:col-span-2'
|
||||
/>
|
||||
),
|
||||
minSize: 200,
|
||||
|
||||
28
modules/customer-invoices/src/web/i18n.ts
Normal file
28
modules/customer-invoices/src/web/i18n.ts
Normal 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);
|
||||
};
|
||||
@ -1,13 +1,12 @@
|
||||
import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCreateCustomerInvoiceMutation } from "../../hooks";
|
||||
import { MODULE_NAME } from "../../manifest";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerInvoiceEditForm } from "./customer-invoice-edit-form";
|
||||
|
||||
export const CustomerInvoiceCreate = () => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { mutate, isPending, isError, error } = useCreateCustomerInvoiceMutation();
|
||||
|
||||
@ -4,7 +4,7 @@ import * as z from "zod";
|
||||
|
||||
import { ClientSelector } from "@erp/customers/components";
|
||||
|
||||
import { DatePickerField } from "@repo/rdx-ui/components";
|
||||
import { DatePickerInputField, TextAreaField, TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
@ -35,9 +35,8 @@ import {
|
||||
import { format } from "date-fns";
|
||||
import { es } from "date-fns/locale";
|
||||
import { CalendarIcon, PlusIcon, Save, Trash2Icon, X } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CustomerInvoiceItemsCardEditor } from "../../components/items";
|
||||
import { MODULE_NAME } from "../../manifest";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerInvoiceData } from "./customer-invoice.schema";
|
||||
import { formatCurrency } from "./utils";
|
||||
|
||||
@ -219,7 +218,7 @@ export const CustomerInvoiceEditForm = ({
|
||||
onSubmit,
|
||||
isPending,
|
||||
}: InvoiceFormProps) => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<CustomerInvoiceData>({
|
||||
resolver: zodResolver(invoiceSchema),
|
||||
@ -271,18 +270,13 @@ export const CustomerInvoiceEditForm = ({
|
||||
</CardHeader>
|
||||
<CardContent className='grid grid-cols-1 gap-4 space-y-6'>
|
||||
<ClientSelector />
|
||||
<FormField
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='customer_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>ID Cliente</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='ID del cliente' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
required
|
||||
label={t("form_fields.customer_id.label")}
|
||||
placeholder={t("form_fields.customer_id.placeholder")}
|
||||
description={t("form_fields.customer_id.description")}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -293,22 +287,19 @@ export const CustomerInvoiceEditForm = ({
|
||||
<CardTitle>Información Básica</CardTitle>
|
||||
<CardDescription>Detalles generales de la factura</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='grid gap-6 md:grid-cols-6'>
|
||||
<FormField
|
||||
<CardContent className='grid gap-y-6 gap-x-8 md:grid-cols-4'>
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='invoice_number'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("form_fields.invoice_number.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("form_fields.invoice_number.placeholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
required
|
||||
disabled
|
||||
readOnly
|
||||
label={t("form_fields.invoice_number.label")}
|
||||
placeholder={t("form_fields.invoice_number.placeholder")}
|
||||
description={t("form_fields.invoice_number.description")}
|
||||
/>
|
||||
|
||||
<DatePickerField
|
||||
<DatePickerInputField
|
||||
control={form.control}
|
||||
name='issue_date'
|
||||
required
|
||||
@ -317,18 +308,13 @@ export const CustomerInvoiceEditForm = ({
|
||||
description={t("form_fields.issue_date.description")}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='invoice_series'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Serie</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='A' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
required
|
||||
label={t("form_fields.invoice_series.label")}
|
||||
placeholder={t("form_fields.invoice_series.placeholder")}
|
||||
description={t("form_fields.invoice_series.description")}
|
||||
/>
|
||||
</CardContent>
|
||||
</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
|
||||
control={form.control}
|
||||
name={`items.${index}.quantity.amount`}
|
||||
|
||||
@ -2,13 +2,12 @@ import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CustomerInvoicesListGrid } from "../components";
|
||||
import { MODULE_NAME } from "../manifest";
|
||||
import { useTranslation } from "../i18n";
|
||||
|
||||
export const CustomerInvoicesList = () => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [status, setStatus] = useState("all");
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "@repo/rdx-ui/locales/i18n.ts";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@ -9,7 +10,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { CellContext } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
import { MoreVerticalIcon } from "lucide-react";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
@ -34,6 +34,8 @@ export function DataTableRowActions<TData = any, TValue = unknown>({
|
||||
actions,
|
||||
rowContext,
|
||||
}: DataTableRowActionsProps<TData, TValue>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@ -12,21 +12,22 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} 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 { format } from "date-fns";
|
||||
import { Control, FieldPath, FieldValues } from "react-hook-form";
|
||||
import { useTranslation } from "../../locales/i18n.ts";
|
||||
|
||||
type DatePickerFieldProps<TFormValues extends FieldValues> = {
|
||||
control: Control<TFormValues>;
|
||||
name: FieldPath<TFormValues>;
|
||||
label: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
className?: string;
|
||||
formatDateFn?: (iso: string) => string;
|
||||
};
|
||||
@ -39,32 +40,43 @@ export function DatePickerField<TFormValues extends FieldValues>({
|
||||
description,
|
||||
disabled = false,
|
||||
required = false,
|
||||
readOnly = false,
|
||||
className,
|
||||
formatDateFn = (iso) => format(new Date(iso), "dd/MM/yyyy"),
|
||||
}: DatePickerFieldProps<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>{" "}
|
||||
{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>
|
||||
)}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant='outline'
|
||||
disabled={disabled}
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
"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}
|
||||
</Button>
|
||||
</FormControl>
|
||||
@ -73,11 +85,16 @@ export function DatePickerField<TFormValues extends FieldValues>({
|
||||
<Calendar
|
||||
mode='single'
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toISOString())}
|
||||
onSelect={(date) => {
|
||||
if (!readOnly) {
|
||||
field.onChange(date?.toISOString());
|
||||
}
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
|
||||
{description || "\u00A0"}
|
||||
</p>
|
||||
|
||||
155
packages/rdx-ui/src/components/form/DatePickerInputField.tsx
Normal file
155
packages/rdx-ui/src/components/form/DatePickerInputField.tsx
Normal 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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
64
packages/rdx-ui/src/components/form/NumberField.tsx
Normal file
64
packages/rdx-ui/src/components/form/NumberField.tsx
Normal 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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
66
packages/rdx-ui/src/components/form/TextAreaField.tsx
Normal file
66
packages/rdx-ui/src/components/form/TextAreaField.tsx
Normal 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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
66
packages/rdx-ui/src/components/form/TextField.tsx
Normal file
66
packages/rdx-ui/src/components/form/TextField.tsx
Normal 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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1 +1,4 @@
|
||||
export * from "./DatePickerField.tsx";
|
||||
export * from "./DatePickerInputField.tsx";
|
||||
export * from "./TextAreaField.tsx";
|
||||
export * from "./TextField.tsx";
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"actions": "Actions",
|
||||
"invalid_date": "Invalid date",
|
||||
"required": "required"
|
||||
},
|
||||
"components": {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"actions": "Actions",
|
||||
"invalid_date": "Fecha incorrecta o no válida",
|
||||
"required": "obligatorio"
|
||||
},
|
||||
"components": {
|
||||
|
||||
@ -1,14 +1,28 @@
|
||||
import i18next from "i18next";
|
||||
import { useTranslation as useTrans } from "react-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation as useI18NextTranslation } from "react-i18next";
|
||||
import { PACKAGE_NAME } from "../index.ts";
|
||||
import enResources from "./en.json" with { type: "json" };
|
||||
import esResources from "./es.json" with { type: "json" };
|
||||
|
||||
export const useTranslation = () => {
|
||||
if (!i18next.hasLoadedNamespace(PACKAGE_NAME)) {
|
||||
i18next.addResourceBundle("en", PACKAGE_NAME, enResources, true, true);
|
||||
i18next.addResourceBundle("es", PACKAGE_NAME, esResources, true, true);
|
||||
const addMissingBundles = (i18n: any) => {
|
||||
const needsEn = !i18n.hasResourceBundle("en", PACKAGE_NAME);
|
||||
const needsEs = !i18n.hasResourceBundle("es", PACKAGE_NAME);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@ -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">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
data-slot='input'
|
||||
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",
|
||||
"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",
|
||||
@ -15,7 +16,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user