Facturas de cliente

This commit is contained in:
David Arranz 2025-07-08 20:50:08 +02:00
parent 3b16556f84
commit a5af168e6b
23 changed files with 335 additions and 504 deletions

View File

@ -1,5 +1,12 @@
{
"common": {},
"common": {
"append_empty_row": "Append row",
"append_empty_row_tooltip": "Append a empty row",
"duplicate_row": "Duplicate",
"insert_row_above": "Insert row above",
"insert_row_below": "Insert row below",
"remove_row": "Remove"
},
"pages": {
"title": "Customer invoices",
"description": "Manage your customer invoices",
@ -45,6 +52,16 @@
"placeholder": "",
"description": ""
},
"issue_date": {
"label": "Date",
"placeholder": "Select a date",
"description": "Invoice issue date"
},
"operation_date": {
"label": "Operation date",
"placeholder": "Select a date",
"description": "Invoice operation date"
},
"items": {
"quantity": {
"label": "Quantity",

View File

@ -1,5 +1,12 @@
{
"common": {},
"common": {
"append_empty_row": "Añadir fila",
"append_empty_row_tooltip": "Añadir una fila vacía",
"duplicate_row": "Duplicar fila",
"insert_row_above": "Insertar fila encima",
"insert_row_below": "Insertar fila debajo",
"remove_row": "Eliminar"
},
"pages": {
"title": "Facturas",
"description": "Gestiona tus facturas",
@ -45,6 +52,16 @@
"placeholder": "",
"description": ""
},
"issue_date": {
"label": "Fecha",
"placeholder": "Seleccionar una fecha",
"description": "Fecha de emisión de la factura"
},
"operation_date": {
"label": "Intervención",
"placeholder": "Seleccionar una fecha",
"description": "Fecha de intervención de los trabajos"
},
"items": {
"quantity": {
"label": "Cantidad",

View File

@ -1,23 +0,0 @@
import { Button } from "@repo/shadcn-ui/components";
import { t } from "i18next";
import { PackagePlusIcon } from "lucide-react";
import { JSX, forwardRef } from "react";
export interface AppendBlockRowButtonProps extends React.ComponentProps<typeof Button> {
label?: string;
}
export const AppendBlockRowButton = forwardRef<HTMLButtonElement, AppendBlockRowButtonProps>(
(
{ label = t("common.append_block"), className, ...props }: AppendBlockRowButtonProps,
ref
): JSX.Element => (
<Button type='button' variant='outline' ref={ref} {...props}>
{" "}
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>}
</Button>
)
);
AppendBlockRowButton.displayName = "AppendBlockRowButton";

View File

@ -1,26 +0,0 @@
import { Button } from "@repo/shadcn-ui/components";
import { t } from "i18next";
import { PackagePlusIcon } from "lucide-react";
import { JSX, forwardRef } from "react";
export interface AppendCatalogArticleRowButtonProps extends React.ComponentProps<typeof Button> {
label?: string;
}
export const AppendCatalogArticleRowButton = forwardRef<
HTMLButtonElement,
AppendCatalogArticleRowButtonProps
>(
(
{ label = t("common.append_article"), className, ...props }: AppendCatalogArticleRowButtonProps,
ref
): JSX.Element => (
<Button type='button' variant='outline' ref={ref} {...props}>
{" "}
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>}
</Button>
)
);
AppendCatalogArticleRowButton.displayName = "AppendCatalogArticleRowButton";

View File

@ -1,7 +1,8 @@
import { Button } from "@repo/shadcn-ui/components";
import { t } from "i18next";
import { PlusCircleIcon } from "lucide-react";
import { JSX, forwardRef } from "react";
import { useTranslation } from "react-i18next";
import { MODULE_NAME } from "../../manifest";
export interface AppendEmptyRowButtonProps extends React.ComponentProps<typeof Button> {
label?: string;
@ -9,15 +10,17 @@ export interface AppendEmptyRowButtonProps extends React.ComponentProps<typeof B
}
export const AppendEmptyRowButton = forwardRef<HTMLButtonElement, AppendEmptyRowButtonProps>(
(
{ label = t("common.append_empty_row"), className, ...props }: AppendEmptyRowButtonProps,
ref
): JSX.Element => (
<Button type='button' variant='outline' ref={ref} {...props}>
<PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>}
</Button>
)
({ label, className, ...props }: AppendEmptyRowButtonProps, ref): JSX.Element => {
const { t } = useTranslation(MODULE_NAME);
const _label = label || t("common.append_empty_row");
return (
<Button type='button' variant='outline' ref={ref} {...props}>
<PlusCircleIcon className={_label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{_label && <>{_label}</>}
</Button>
);
}
);
AppendEmptyRowButton.displayName = "AppendEmptyRowButton";

View File

@ -1,3 +1 @@
export * from "./append-block-row-button";
export * from "./append-catalog-article-row-button";
export * from "./append-empty-row-button";

View File

@ -76,7 +76,7 @@ export const CustomerInvoiceItemsCardEditor = ({
{
id: "description" as const,
accessorKey: "description",
header: t("customer_invoices.form_fields.description.label"),
header: t("form_fields.items.description.label"),
cell: ({ row: { index, original } }) => (
<FormField
control={control}
@ -85,7 +85,7 @@ export const CustomerInvoiceItemsCardEditor = ({
<FormItem className='md:col-span-2'>
<FormControl>
<Textarea
placeholder={t("customer_invoices.form_fields.description.placeholder")}
placeholder={t("form_fields.items.description.placeholder")}
{...field}
/>
</FormControl>
@ -100,9 +100,7 @@ export const CustomerInvoiceItemsCardEditor = ({
{
id: "quantity" as const,
accessorKey: "quantity",
header: () => (
<div className='text-right'>{t("customer_invoices.form_fields.quantity.label")}</div>
),
header: () => <div className='text-right'>{t("form_fields.items.quantity.label")}</div>,
cell: ({ row: { index } }) => (
<FormField
control={control}
@ -129,11 +127,7 @@ export const CustomerInvoiceItemsCardEditor = ({
{
id: "unit_price" as const,
accessorKey: "unit_price",
header: () => (
<div className='text-right'>
{t("customer_invoices.form_fields.items.unit_price.label")}
</div>
),
header: () => <div className='text-right'>{t("form_fields.items.unit_price.label")}</div>,
cell: ({ row: { index } }) => (
<FormField
control={control}
@ -161,9 +155,7 @@ export const CustomerInvoiceItemsCardEditor = ({
id: "subtotal_price" as const,
accessorKey: "subtotal_price",
header: () => (
<div className='text-right'>
{t("customer_invoices.form_fields.items.subtotal_price.label")}
</div>
<div className='text-right'>{t("form_fields.items.subtotal_price.label")}</div>
),
cell: ({ row: { index } }) => {
/*return (
@ -184,11 +176,7 @@ export const CustomerInvoiceItemsCardEditor = ({
{
id: "discount" as const,
accessorKey: "discount",
header: () => (
<div className='text-right'>
{t("customer_invoices.form_fields.items.discount.label")}
</div>
),
header: () => <div className='text-right'>{t("form_fields.items.discount.label")}</div>,
cell: ({ row: { index } }) => (
<FormField
control={control}
@ -216,11 +204,7 @@ export const CustomerInvoiceItemsCardEditor = ({
{
id: "total_price" as const,
accessorKey: "total_price",
header: () => (
<div className='text-right'>
{t("customer_invoices.form_fields.items.total_price.label")}
</div>
),
header: () => <div className='text-right'>{t("form_fields.items.total_price.label")}</div>,
cell: ({ row: { index } }) => (
<>
{formatCurrency(
@ -324,8 +308,6 @@ export const CustomerInvoiceItemsCardEditor = ({
const defaultLayout = [265, 440, 655];
const navCollapsedSize = 4;
console.log(columns);
return (
<div className='relative'>
<CustomerInvoiceItemsSortableDataTable

View File

@ -8,11 +8,7 @@ import {
import { Table } from "@tanstack/react-table";
import { t } from "i18next";
import { CopyPlusIcon, ScanIcon, Trash2Icon } from "lucide-react";
import {
AppendBlockRowButton,
AppendCatalogArticleRowButton,
AppendEmptyRowButton,
} from "../buttons";
import { AppendEmptyRowButton } from "../buttons";
export const CustomerInvoiceItemsSortableDataTableToolbar = ({ table }: { table: Table<any> }) => {
const selectedRowsCount = table.getSelectedRowModel().rows.length;
@ -84,32 +80,6 @@ export const CustomerInvoiceItemsSortableDataTableToolbar = ({ table }: { table:
</TooltipTrigger>
<TooltipContent>{t("common.append_empty_row_tooltip")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<AppendCatalogArticleRowButton
variant='link'
onClick={() => {
if (table.options.meta && table.options.meta.pickCatalogArticle) {
table.options.meta?.pickCatalogArticle();
}
}}
/>
</TooltipTrigger>
<TooltipContent>{t("common.append_article_tooltip")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<AppendBlockRowButton
variant='link'
onClick={() => {
if (table.options.meta && table.options.meta.pickBlock) {
table.options.meta?.pickBlock();
}
}}
/>
</TooltipTrigger>
<TooltipContent>{t("common.append_block_tooltip")}</TooltipContent>
</Tooltip>
</div>
<div className='flex items-center gap-2 ml-auto' />
</nav>

View File

@ -48,11 +48,7 @@ import {
import { useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { FieldValues, UseFieldArrayReturn } from "react-hook-form";
import {
AppendBlockRowButton,
AppendCatalogArticleRowButton,
AppendEmptyRowButton,
} from "../buttons";
import { AppendEmptyRowButton } from "../buttons";
import { CustomerInvoiceItemsSortableDataTableToolbar } from "./customer-invoice-items-sortable-datatable-toolbar";
import { CustomerInvoiceItemsSortableTableRow } from "./customer-invoice-items-sortable-table-row";
@ -509,20 +505,6 @@ export function CustomerInvoiceItemsSortableDataTable<
<CardFooter>
<ButtonGroup>
<AppendEmptyRowButton onClick={() => table.options.meta?.appendItem()} />
<AppendCatalogArticleRowButton
onClick={() => {
if (table.options.meta && table.options.meta.pickCatalogArticle) {
table.options.meta?.pickCatalogArticle();
}
}}
/>
<AppendBlockRowButton
onClick={() => {
if (table.options.meta && table.options.meta.pickBlock) {
table.options.meta?.pickBlock();
}
}}
/>
</ButtonGroup>
</CardFooter>
</Card>

View File

@ -0,0 +1,2 @@
@source "./components";
@source "./pages";

View File

@ -4,7 +4,7 @@ import * as z from "zod";
import { ClientSelector } from "@erp/customers/components";
import { formatDate } from "@erp/core/client";
import { DatePickerField } from "@repo/rdx-ui/components";
import {
Button,
Calendar,
@ -264,254 +264,68 @@ export const CustomerInvoiceEditForm = ({
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit, handleError)} className='grid gap-6'>
{/* Cliente */}
<Card>
<CardHeader>
<CardTitle>Cliente</CardTitle>
</CardHeader>
<CardContent className='grid grid-cols-1 gap-4 space-y-6'>
<ClientSelector />
<FormField
control={form.control}
name='customer_id'
render={({ field }) => (
<FormItem>
<FormLabel>ID Cliente</FormLabel>
<FormControl>
<Input placeholder='ID del cliente' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
{/* Información básica */}
<Card>
<CardHeader>
<CardTitle>Información Básica</CardTitle>
<CardDescription>Detalles generales de la factura</CardDescription>
</CardHeader>
<CardContent className='grid grid-cols-1 gap-4 space-y-6'>
<div className='grid grid-cols-1'>
<ClientSelector />
<FormField
control={form.control}
name='customer_id'
render={({ field }) => (
<FormItem>
<FormLabel>ID Cliente</FormLabel>
<FormControl>
<Input placeholder='ID del cliente' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='grid grid-cols-4'>
<FormField
control={form.control}
name='invoice_number'
render={({ field }) => (
<FormItem>
<FormLabel>{t("form_fields.invoice_number.label")}</FormLabel>
<FormControl>
<Input placeholder='1' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='issue_date'
render={({ field }) => (
<FormItem>
<FormLabel>Fecha de Emisión</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant='outline'
className='w-full justify-start text-left font-normal'
>
<CalendarIcon className='mr-2 h-4 w-4' />
{field.value ? formatDate(field.value) : "Seleccionar fecha"}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={field.value ? new Date(field.value) : undefined}
onSelect={(date) => field.onChange(date?.toISOString())}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='invoice_series'
render={({ field }) => (
<FormItem>
<FormLabel>Serie</FormLabel>
<FormControl>
<Input placeholder='A' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='invoice_status'
render={({ field }) => (
<FormItem>
<FormLabel>Estado</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder='Seleccionar estado' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='draft'>Borrador</SelectItem>
<SelectItem value='sent'>Enviada</SelectItem>
<SelectItem value='paid'>Pagada</SelectItem>
<SelectItem value='cancelled'>Cancelada</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='operation_date'
render={({ field }) => (
<FormItem>
<FormLabel>Fecha de Operación</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant='outline'
className='w-full justify-start text-left font-normal'
>
<CalendarIcon className='mr-2 h-4 w-4' />
{field.value ? formatDate(field.value) : "Seleccionar fecha"}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={field.value ? new Date(field.value) : undefined}
onSelect={(date) => field.onChange(date?.toISOString())}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='operation_date'
render={({ field }) => (
<FormItem>
<FormLabel>Inicio periodo de facturación</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant='outline'
className='w-full justify-start text-left font-normal'
>
<CalendarIcon className='mr-2 h-4 w-4' />
{field.value ? formatDate(field.value) : "Seleccionar fecha"}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={field.value ? new Date(field.value) : undefined}
onSelect={(date) => field.onChange(date?.toISOString())}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='operation_date'
render={({ field }) => (
<FormItem>
<FormLabel>Fin periodo de facturación</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant='outline'
className='w-full justify-start text-left font-normal'
>
<CalendarIcon className='mr-2 h-4 w-4' />
{field.value ? formatDate(field.value) : "Seleccionar fecha"}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={field.value ? new Date(field.value) : undefined}
onSelect={(date) => field.onChange(date?.toISOString())}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
</div>
<CardContent className='grid gap-6 md:grid-cols-6'>
<FormField
control={form.control}
name='language_code'
name='invoice_number'
render={({ field }) => (
<FormItem>
<FormLabel>Idioma</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder='Seleccionar idioma' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='ES'>Español</SelectItem>
<SelectItem value='EN'>English</SelectItem>
<SelectItem value='FR'>Français</SelectItem>
<SelectItem value='DE'>Deutsch</SelectItem>
</SelectContent>
</Select>
<FormLabel>{t("form_fields.invoice_number.label")}</FormLabel>
<FormControl>
<Input placeholder={t("form_fields.invoice_number.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DatePickerField
control={form.control}
name='issue_date'
required
label={t("form_fields.issue_date.label")}
placeholder={t("form_fields.issue_date.placeholder")}
description={t("form_fields.issue_date.description")}
/>
<FormField
control={form.control}
name='currency'
name='invoice_series'
render={({ field }) => (
<FormItem>
<FormLabel>Moneda</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder='Seleccionar moneda' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='EUR'>EUR</SelectItem>
<SelectItem value='USD'>USD</SelectItem>
<SelectItem value='GBP'>GBP</SelectItem>
</SelectContent>
</Select>
<FormLabel>Serie</FormLabel>
<FormControl>
<Input placeholder='A' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}

View File

@ -0,0 +1,2 @@
@source "./components";
@source "./pages";

View File

@ -14,6 +14,13 @@
"./locales/*": "./src/locales/*",
"./hooks/*": ["./src/hooks/*.tsx", "./src/hooks/*.ts"]
},
"peerDependencies": {
"date-fns": "^4.1.0",
"i18next": "^25.1.1",
"react-hook-form": "^7.58.1",
"typescript": "^5.8.3",
"zod": "^3.25.67"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@repo/typescript-config": "workspace:*",
@ -40,7 +47,6 @@
"@repo/shadcn-ui": "workspace:*",
"@tanstack/react-table": "^8.21.3",
"esbuild-raw-plugin": "^0.2.0",
"i18next": "^25.0.2",
"lucide-react": "^0.503.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",

View File

@ -0,0 +1,89 @@
// DatePickerField.tsx
import {
Button,
Calendar,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Popover,
PopoverContent,
PopoverTrigger,
} from "@repo/shadcn-ui/components";
import { CalendarIcon } 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";
type DatePickerFieldProps<TFormValues extends FieldValues> = {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label: string;
placeholder?: string;
description?: string;
disabled?: boolean;
required?: boolean;
className?: string;
formatDateFn?: (iso: string) => string;
};
export function DatePickerField<TFormValues extends FieldValues>({
control,
name,
label,
placeholder,
description,
disabled = false,
required = false,
className,
formatDateFn = (iso) => format(new Date(iso), "dd/MM/yyyy"),
}: DatePickerFieldProps<TFormValues>) {
const { t } = useTranslation();
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>{" "}
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant='outline'
disabled={disabled}
className={cn(
"w-full justify-start text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
<CalendarIcon className='mr-2 h-4 w-4' />
{field.value ? formatDateFn(field.value) : placeholder}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={field.value ? new Date(field.value) : undefined}
onSelect={(date) => field.onChange(date?.toISOString())}
initialFocus
/>
</PopoverContent>
</Popover>
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
{description || "\u00A0"}
</p>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@ -0,0 +1 @@
export * from "./DatePickerField.tsx";

View File

@ -2,6 +2,7 @@ export * from "./buttons/index.tsx";
export * from "./custom-dialog.tsx";
export * from "./datatable/index.tsx";
export * from "./error-overlay.tsx";
export * from "./form/index.tsx";
export * from "./layout/index.tsx";
export * from "./loading-overlay/index.tsx";
export * from "./scroll-to-top.tsx";

View File

@ -1,3 +1,4 @@
"use client";
export const PACKAGE_NAME = "rdx-ui";
export * from "./components/index.tsx";

View File

@ -1,4 +1,7 @@
{
"common": {
"required": "required"
},
"components": {
"loading_indicator": {
"title": "Loading..."

View File

@ -1,4 +1,7 @@
{
"common": {
"required": "obligatorio"
},
"components": {
"LoadingIndicator": {
"title": "Cargando..."

View File

@ -0,0 +1,14 @@
import i18next from "i18next";
import { useTranslation as useTrans } 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);
}
return useTrans(PACKAGE_NAME);
};

View File

@ -0,0 +1 @@
@source "../components";

View File

@ -5,8 +5,9 @@
/**
*
* https://www.windpalette.com/app
https://themecn.dev/api/registry/eyJsIjp7ImJnIjoiMCwwLDEwMCIsImZnIjoiMjE5LDEwLDQiLCJwIjoiMjE5LDgzLDU4IiwicyI6IjIxOSwyMyw5NiIsImEiOiIyMDUsMTAwLDY3IiwiZCI6IjM1Ny4xOCwxMDAsNDUifSwiciI6MC41fQ==
https://themecn.dev/dashboard?theme=eyJsIjp7ImJnIjoiMCwwLDEwMCIsImZnIjoiMjI2LDEwLDQiLCJwIjoiMjI2LDEwMCw1NSIsInMiOiIyMjYsMjUsOTUiLCJhIjoiMjI2LDEwMCw4NSIsImQiOiIzNTcuMTgsMTAwLDQ1In0sImYiOlsiTGF0byIsIlBvcHBpbnMiXSwiciI6MC41fQ==
**/
@ -48,149 +49,115 @@
--magenta-950: #660031;
}
:root {
--background: oklch(0.9911 0 0);
--foreground: oklch(0.2046 0 0);
--card: oklch(0.9911 0 0);
--card-foreground: oklch(0.2046 0 0);
--popover: oklch(0.9911 0 0);
--popover-foreground: oklch(0.4386 0 0);
--primary: var(--sapphire-600);
--primary-foreground: var(--sapphire-50);
--secondary: var(--graphite-600);
--secondary-foreground: var(--graphite-50);
--muted: oklch(0.9461 0 0);
--muted-foreground: oklch(0.2435 0 0);
--accent: var(--magenta-60);
--accent-foreground: var(--magenta-50);
--destructive: oklch(0.5795 0.2369 28.4251);
--destructive-foreground: oklch(0.9934 0.0032 17.2118);
--border: var(--graphite-300);
--input: var(--graphite-300);
--ring: var(--sapphire-500);
--chart-1: oklch(0.6202 0.1561 261.2713);
--chart-2: oklch(0.6611 0.1491 261.8026);
--chart-3: oklch(0.7537 0.1098 262.1599);
--chart-4: oklch(0.4174 0.1322 261.2301);
--chart-5: oklch(0.3372 0.0777 261.8039);
--sidebar: var(--graphite-800);
--sidebar-foreground: var(--graphite-50);
--sidebar-primary: var(--sapphire-500);
--sidebar-primary-foreground: var(--graphite-900);
--sidebar-accent: var(--graphite-200);
--sidebar-accent-foreground: var(--graphite-700);
--sidebar-border: var(--graphite-200);
--sidebar-ring: var(--sapphire-500);
--font-sans: Geist, sans-serif;
--font-serif: Merriweather, serif;
--font-mono: Geist Mono, monospace;
--radius: 0.5rem;
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
.dark {
--background: oklch(0.1822 0 0);
--foreground: oklch(0.9288 0.0126 255.5078);
--card: oklch(0.2046 0 0);
--card-foreground: oklch(0.9288 0.0126 255.5078);
--popover: oklch(0.2603 0 0);
--popover-foreground: oklch(0.7348 0 0);
--primary: oklch(0.4365 0.1044 156.7556);
--primary-foreground: oklch(0.9213 0.0135 167.1556);
--secondary: oklch(0.2603 0 0);
--secondary-foreground: oklch(0.9851 0 0);
--muted: oklch(0.2393 0 0);
--muted-foreground: oklch(0.7122 0 0);
--accent: oklch(0.3132 0 0);
--accent-foreground: oklch(0.9851 0 0);
--destructive: oklch(0.3123 0.0852 29.7877);
--destructive-foreground: oklch(0.9368 0.0045 34.3092);
--border: oklch(0.2809 0 0);
--input: oklch(0.2603 0 0);
--ring: oklch(0.8003 0.1821 151.711);
--chart-1: oklch(0.8003 0.1821 151.711);
--chart-2: oklch(0.7137 0.1434 254.624);
--chart-3: oklch(0.709 0.1592 293.5412);
--chart-4: oklch(0.8369 0.1644 84.4286);
--chart-5: oklch(0.7845 0.1325 181.912);
--sidebar: oklch(0.1822 0 0);
--sidebar-foreground: oklch(0.6301 0 0);
--sidebar-primary: oklch(0.4365 0.1044 156.7556);
--sidebar-primary-foreground: oklch(0.9213 0.0135 167.1556);
--sidebar-accent: oklch(0.3132 0 0);
--sidebar-accent-foreground: oklch(0.9851 0 0);
--sidebar-border: oklch(0.2809 0 0);
--sidebar-ring: oklch(0.8003 0.1821 151.711);
@theme inline {
--font-sans: Geist, sans-serif;
--font-serif: Merriweather, serif;
--font-mono: "Geist Mono", monospace;
--radius: 0.5rem;
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
:root {
--radius: 0.5rem;
--background: oklch(1.0 0.0 0);
--foreground: oklch(0.143 0.003 271.9282674829111);
--card: oklch(1.0 0.0 0);
--card-foreground: oklch(0.143 0.003 271.9282674829111);
--popover: oklch(1.0 0.0 0);
--popover-foreground: oklch(0.143 0.003 271.9282674829111);
--primary: oklch(0.527 0.263 264.6358829854314);
--primary-foreground: oklch(0.96 0.003 272.6281326778613);
--secondary: oklch(0.957 0.007 272.5840410480741);
--secondary-foreground: oklch(0.2 0.024 271.03952637990255);
--muted: oklch(0.957 0.007 272.5840410480741);
--muted-foreground: oklch(0.501 0.087 270.1873691459851);
--accent: oklch(0.893 0.011 270.0165724649083);
--accent-foreground: oklch(0.194 0.039 267.55460547761646);
--destructive: oklch(0.58 0.237 28.43022926835137);
--border: oklch(0.87 0.021 272.39716219522893);
--input: oklch(0.87 0.021 272.39716219522893);
--ring: oklch(0.527 0.263 264.6358829854314);
--chart-1: oklch(0.527 0.263 264.6358829854314);
--chart-2: oklch(0.587 0.17 268.089554204009);
--chart-3: oklch(0.698 0.107 270.5337309213311);
--chart-4: oklch(0.344 0.167 264.8096356890612);
--chart-5: oklch(0.265 0.121 265.17321408623843);
--sidebar: oklch(0.957 0.007 272.5840410480741);
--sidebar-foreground: oklch(0.2 0.024 271.03952637990255);
--sidebar-primary: oklch(0.527 0.263 264.6358829854314);
--sidebar-primary-foreground: oklch(0.96 0.003 272.6281326778613);
--sidebar-accent: oklch(0.893 0.011 270.0165724649083);
--sidebar-accent-foreground: oklch(0.194 0.039 267.55460547761646);
--sidebar-border: oklch(0.87 0.021 272.39716219522893);
--sidebar-ring: oklch(0.527 0.263 264.6358829854314);
}
.dark {
--background: oklch(0.18 0.02 271.2071690195347);
--foreground: oklch(0.96 0.003 272.6281326778613);
--card: oklch(0.219 0.028 270.9064636444989);
--card-foreground: oklch(0.96 0.003 272.6281326778613);
--popover: oklch(0.219 0.028 270.9064636444989);
--popover-foreground: oklch(0.96 0.003 272.6281326778613);
--primary: oklch(0.572 0.216 266.49817452939124);
--primary-foreground: oklch(0.96 0.003 272.6281326778613);
--secondary: oklch(0.297 0.037 271.01211769802813);
--secondary-foreground: oklch(0.956 0.008 272.5689802513852);
--muted: oklch(0.297 0.037 271.01211769802813);
--muted-foreground: oklch(0.703 0.031 272.154700569603);
--accent: oklch(0.645 0.061 269.1114142997618);
--accent-foreground: oklch(0.953 0.013 270.00473513692674);
--destructive: oklch(0.58 0.237 28.43022926835137);
--border: oklch(0.38 0.062 270.35456036349854);
--input: oklch(0.38 0.062 270.35456036349854);
--ring: oklch(0.572 0.216 266.49817452939124);
--chart-1: oklch(0.572 0.216 266.49817452939124);
--chart-2: oklch(0.628 0.168 268.4986758327506);
--chart-3: oklch(0.728 0.124 270.22825208762697);
--chart-4: oklch(0.388 0.15 266.35164847353985);
--chart-5: oklch(0.32 0.089 268.3276964633901);
--sidebar: oklch(0.293 0.044 270.5701620578212);
--sidebar-foreground: oklch(0.96 0.003 272.6281326778613);
--sidebar-primary: oklch(0.572 0.216 266.49817452939124);
--sidebar-primary-foreground: oklch(0.96 0.003 272.6281326778613);
--sidebar-accent: oklch(0.645 0.061 269.1114142997618);
--sidebar-accent-foreground: oklch(0.953 0.013 270.00473513692674);
--sidebar-border: oklch(0.38 0.062 270.35456036349854);
--sidebar-ring: oklch(0.572 0.216 266.49817452939124);
}
@layer base {

View File

@ -173,7 +173,7 @@ importers:
version: 29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3))
ts-jest:
specifier: ^29.2.5
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
tsconfig-paths:
specifier: ^4.2.0
version: 4.2.0
@ -712,11 +712,14 @@ importers:
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
date-fns:
specifier: ^4.1.0
version: 4.1.0
esbuild-raw-plugin:
specifier: ^0.2.0
version: 0.2.0
i18next:
specifier: ^25.0.2
specifier: ^25.1.1
version: 25.2.1(typescript@5.8.3)
lucide-react:
specifier: ^0.503.0
@ -727,6 +730,9 @@ importers:
react-dom:
specifier: ^19.1.0
version: 19.1.0(react@19.1.0)
react-hook-form:
specifier: ^7.58.1
version: 7.58.1(react@19.1.0)
react-i18next:
specifier: ^15.5.1
version: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
@ -11828,7 +11834,7 @@ snapshots:
ts-interface-checker@0.1.13: {}
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
dependencies:
bs-logger: 0.2.6
ejs: 3.1.10
@ -11846,6 +11852,7 @@ snapshots:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.27.4)
esbuild: 0.25.5
jest-util: 29.7.0
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):