Facturas de cliente
This commit is contained in:
parent
dc49094f00
commit
d539e5b5f1
@ -136,53 +136,53 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Retención 35%",
|
"name": "Retenc. 35%",
|
||||||
"code": "retencion_35",
|
"code": "retencion_35",
|
||||||
"value": "3500",
|
"value": "3500",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
"group": "Retención",
|
"group": "Retención",
|
||||||
"description": "Retención profesional o fiscal tipo máximo.",
|
"description": "Retenc. profesional o fiscal tipo máximo.",
|
||||||
"aeat_code": null
|
"aeat_code": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Retención 19%",
|
"name": "Retenc. 19%",
|
||||||
"code": "retencion_19",
|
"code": "retencion_19",
|
||||||
"value": "1900",
|
"value": "1900",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
"group": "Retención",
|
"group": "Retención",
|
||||||
"description": "Retención IRPF general.",
|
"description": "Retenc. IRPF general.",
|
||||||
"aeat_code": "R1"
|
"aeat_code": "R1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Retención 15%",
|
"name": "Retenc. 15%",
|
||||||
"code": "retencion_15",
|
"code": "retencion_15",
|
||||||
"value": "1500",
|
"value": "1500",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
"group": "Retención",
|
"group": "Retención",
|
||||||
"description": "Retención para autónomos y profesionales.",
|
"description": "Retenc. para autónomos y profesionales.",
|
||||||
"aeat_code": "R2"
|
"aeat_code": "R2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Retención 7%",
|
"name": "Retenc. 7%",
|
||||||
"code": "retencion_7",
|
"code": "retencion_7",
|
||||||
"value": "700",
|
"value": "700",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
"group": "Retención",
|
"group": "Retención",
|
||||||
"description": "Retención para nuevos autónomos.",
|
"description": "Retenc. para nuevos autónomos.",
|
||||||
"aeat_code": null
|
"aeat_code": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Retención 2%",
|
"name": "Retenc. 2%",
|
||||||
"code": "retencion_2",
|
"code": "retencion_2",
|
||||||
"value": "200",
|
"value": "200",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
"group": "Retención",
|
"group": "Retención",
|
||||||
"description": "Retención sobre arrendamientos de inmuebles urbanos.",
|
"description": "Retenc. sobre arrendamientos de inmuebles urbanos.",
|
||||||
"aeat_code": "R3"
|
"aeat_code": "R3"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Rec. de equivalencia 5,2%",
|
"name": "Rec. 5,2%",
|
||||||
"code": "rec_5_2",
|
"code": "rec_5_2",
|
||||||
"value": "520",
|
"value": "520",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
@ -191,7 +191,7 @@
|
|||||||
"aeat_code": "51"
|
"aeat_code": "51"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Rec. de equivalencia 1,75%",
|
"name": "Rec. 1,75%",
|
||||||
"code": "rec_1_75",
|
"code": "rec_1_75",
|
||||||
"value": "175",
|
"value": "175",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
@ -200,7 +200,7 @@
|
|||||||
"aeat_code": "52"
|
"aeat_code": "52"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Rec. de equivalencia 1,4%",
|
"name": "Rec. 1,4%",
|
||||||
"code": "rec_1_4",
|
"code": "rec_1_4",
|
||||||
"value": "140",
|
"value": "140",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
@ -209,7 +209,7 @@
|
|||||||
"aeat_code": null
|
"aeat_code": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Rec. de equivalencia 1%",
|
"name": "Rec. 1%",
|
||||||
"code": "rec_1",
|
"code": "rec_1",
|
||||||
"value": "100",
|
"value": "100",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
@ -218,7 +218,7 @@
|
|||||||
"aeat_code": null
|
"aeat_code": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Rec. de equivalencia 0,62%",
|
"name": "Rec. 0,62%",
|
||||||
"code": "rec_0_62",
|
"code": "rec_0_62",
|
||||||
"value": "62",
|
"value": "62",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
@ -227,7 +227,7 @@
|
|||||||
"aeat_code": null
|
"aeat_code": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Rec. de equivalencia 0,5%",
|
"name": "Rec. 0,5%",
|
||||||
"code": "rec_0_5",
|
"code": "rec_0_5",
|
||||||
"value": "50",
|
"value": "50",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
@ -236,7 +236,7 @@
|
|||||||
"aeat_code": null
|
"aeat_code": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Rec. de equivalencia 0,26%",
|
"name": "Rec. 0,26%",
|
||||||
"code": "rec_0_26",
|
"code": "rec_0_26",
|
||||||
"value": "26",
|
"value": "26",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
@ -245,7 +245,7 @@
|
|||||||
"aeat_code": null
|
"aeat_code": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Rec. de equivalencia 0%",
|
"name": "Rec. 0%",
|
||||||
"code": "rec_0",
|
"code": "rec_0",
|
||||||
"value": "0",
|
"value": "0",
|
||||||
"scale": "2",
|
"scale": "2",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"rows_selected": "{{count}} fila(s) seleccionadas.",
|
"rows_selected": "{{count}} fila(s) seleccionadas.",
|
||||||
"rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas."
|
"rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas."
|
||||||
},
|
},
|
||||||
|
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"status": {
|
"status": {
|
||||||
"draft": "Draft",
|
"draft": "Draft",
|
||||||
@ -198,6 +199,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
"datatable": {
|
||||||
|
"actions": "Actions"
|
||||||
|
},
|
||||||
"customer_invoice_taxes_multi_select": {
|
"customer_invoice_taxes_multi_select": {
|
||||||
"label": "Taxes",
|
"label": "Taxes",
|
||||||
"placeholder": "Select taxes",
|
"placeholder": "Select taxes",
|
||||||
|
|||||||
@ -191,6 +191,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
"datatable": {
|
||||||
|
"actions": "Acciones"
|
||||||
|
},
|
||||||
"customer_invoice_taxes_multi_select": {
|
"customer_invoice_taxes_multi_select": {
|
||||||
"label": "Impuestos",
|
"label": "Impuestos",
|
||||||
"placeholder": "Selecciona impuestos",
|
"placeholder": "Selecciona impuestos",
|
||||||
|
|||||||
@ -4,41 +4,9 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
|
|
||||||
const taxesList = [
|
|
||||||
{ label: "IVA 21%", value: "iva_21", group: "IVA" },
|
|
||||||
{ label: "IVA 10%", value: "iva_10", group: "IVA" },
|
|
||||||
{ label: "IVA 7,5%", value: "iva_7_5", group: "IVA" },
|
|
||||||
{ label: "IVA 5%", value: "iva_5", group: "IVA" },
|
|
||||||
{ label: "IVA 4%", value: "iva_4", group: "IVA" },
|
|
||||||
{ label: "IVA 2%", value: "iva_2", group: "IVA" },
|
|
||||||
{ label: "IVA 0%", value: "iva_0", group: "IVA" },
|
|
||||||
{ label: "Exenta", value: "iva_exenta", group: "IVA" },
|
|
||||||
{ label: "No sujeto", value: "iva_no_sujeto", group: "IVA" },
|
|
||||||
{ label: "Iva Intracomunitario Bienes", value: "iva_intracomunitario_bienes", group: "IVA" },
|
|
||||||
{ label: "Iva Intracomunitario Servicio", value: "iva_intracomunitario_servicio", group: "IVA" },
|
|
||||||
{ label: "Exportación", value: "iva_exportacion", group: "IVA" },
|
|
||||||
{ label: "Inv. Suj. Pasivo", value: "iva_inversion_sujeto_pasivo", group: "IVA" },
|
|
||||||
|
|
||||||
{ label: "Retención 35%", value: "retencion_35", group: "Retención" },
|
|
||||||
{ label: "Retención 19%", value: "retencion_19", group: "Retención" },
|
|
||||||
{ label: "Retención 15%", value: "retencion_15", group: "Retención" },
|
|
||||||
{ label: "Retención 7%", value: "retencion_7", group: "Retención" },
|
|
||||||
{ label: "Retención 2%", value: "retencion_2", group: "Retención" },
|
|
||||||
|
|
||||||
{ label: "REC 5,2%", value: "rec_5_2", group: "Recargo de equivalencia" },
|
|
||||||
{ label: "REC 1,75%", value: "rec_1_75", group: "Recargo de equivalencia" },
|
|
||||||
{ label: "REC 1,4%", value: "rec_1_4", group: "Recargo de equivalencia" },
|
|
||||||
{ label: "REC 1%", value: "rec_1", group: "Recargo de equivalencia" },
|
|
||||||
{ label: "REC 0,62%", value: "rec_0_62", group: "Recargo de equivalencia" },
|
|
||||||
{ label: "REC 0,5%", value: "rec_0_5", group: "Recargo de equivalencia" },
|
|
||||||
{ label: "REC 0,26%", value: "rec_0_26", group: "Recargo de equivalencia" },
|
|
||||||
{ label: "REC 0%", value: "rec_0", group: "Recargo de equivalencia" },
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface CustomerInvoiceTaxesMultiSelect {
|
interface CustomerInvoiceTaxesMultiSelect {
|
||||||
value: string[];
|
value?: string[];
|
||||||
onChange: (selectedValues: string[]) => void;
|
onChange: (selectedValues: string[]) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
[key: string]: any; // Allow other props to be passed
|
[key: string]: any; // Allow other props to be passed
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { formatCurrency } from '@erp/core';
|
import { formatCurrency } from '@erp/core';
|
||||||
import { useMoney } from '@erp/core/hooks';
|
import { useMoney } from '@erp/core/hooks';
|
||||||
|
import { Input } from '@repo/shadcn-ui/components';
|
||||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { findFocusableInCell, focusAndSelect } from './input-utils';
|
import { findFocusableInCell, focusAndSelect } from './input-utils';
|
||||||
@ -166,7 +167,7 @@ export function AmountInput({
|
|||||||
|
|
||||||
if (readOnly && readOnlyMode === "textlike-input") {
|
if (readOnly && readOnlyMode === "textlike-input") {
|
||||||
return (
|
return (
|
||||||
<input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
readOnly
|
readOnly
|
||||||
@ -186,13 +187,13 @@ export function AmountInput({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
pattern="[0-9]*[.,]?[0-9]*"
|
pattern="[0-9]*[.,]?[0-9]*"
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full bg-transparent p-0 text-right tabular-nums h-8 px-1",
|
"w-full bg-transparent p-0 text-right tabular-nums h-8 px-1 shadow-none",
|
||||||
"border-none",
|
"border-none",
|
||||||
"focus:bg-background",
|
"focus:bg-background",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[2px]",
|
"focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[2px]",
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { Button, Input, Label, Textarea } from "@repo/shadcn-ui/components";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { InvoiceFormData } from '../../../schemas';
|
||||||
|
|
||||||
|
export function ItemRowEditor({ index, close }: { index: number; close: () => void }) {
|
||||||
|
// Editor simple reutilizando el mismo RHF
|
||||||
|
const { register } = useFormContext<InvoiceFormData>();
|
||||||
|
return (
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<h3 className="text-base font-semibold">Edit line #{index + 1}</h3>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={`desc-${index}`}>Description</Label>
|
||||||
|
<Textarea id={`desc-${index}`} rows={4} {...register(`items.${index}.description`)} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={`qty-${index}`}>Qty</Label>
|
||||||
|
<Input id={`qty-${index}`} type="number" step="1" {...register(`items.${index}.quantity`, { valueAsNumber: true })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={`unit-${index}`}>Unit</Label>
|
||||||
|
<Input id={`unit-${index}`} type="number" step="0.01" {...register(`items.${index}.unit_amount`, { valueAsNumber: true })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={`disc-${index}`}>Discount %</Label>
|
||||||
|
<Input id={`disc-${index}`} type="number" step="0.01" {...register(`items.${index}.discount_percentage`, { valueAsNumber: true })} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="secondary" onClick={close}>Close</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Row, Table } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger
|
||||||
|
} from '@repo/shadcn-ui/components';
|
||||||
|
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, PencilIcon, Trash2Icon } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DataTableRowActionsProps<TData> {
|
||||||
|
row: Row<TData>,
|
||||||
|
table: Table<TData>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ItemDataTableRowActions<TData>({
|
||||||
|
row, table
|
||||||
|
}: DataTableRowActionsProps<TData>) {
|
||||||
|
const ops = (table.options.meta as any)?.rowOps as {
|
||||||
|
duplicate?: (i: number) => void;
|
||||||
|
remove?: (i: number) => void;
|
||||||
|
move?: (f: number, t: number) => void;
|
||||||
|
canMoveUp?: (i: number) => boolean;
|
||||||
|
canMoveDown?: (i: number, last: number) => boolean;
|
||||||
|
};
|
||||||
|
const openEditor = (table.options.meta as any)?.openEditor as (i: number) => void;
|
||||||
|
const lastRow = table.getRowModel().rows.length - 1;
|
||||||
|
const rowIndex = row.index;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center gap-1">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" aria-label="Edit row" onClick={() => openEditor?.(rowIndex)}>
|
||||||
|
<PencilIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Edit</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" aria-label="Duplicate row" onClick={() => ops?.duplicate?.(rowIndex)}>
|
||||||
|
<CopyIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Copy</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost" size="icon" aria-label="Move up"
|
||||||
|
disabled={ops?.canMoveUp ? !ops.canMoveUp(rowIndex) : rowIndex === 0}
|
||||||
|
onClick={() => ops?.move?.(rowIndex, rowIndex - 1)}
|
||||||
|
>
|
||||||
|
<ArrowUpIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Up</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost" size="icon" aria-label="Move down"
|
||||||
|
disabled={ops?.canMoveDown ? !ops.canMoveDown(rowIndex, lastRow) : rowIndex === lastRow}
|
||||||
|
onClick={() => ops?.move?.(rowIndex, rowIndex + 1)}
|
||||||
|
>
|
||||||
|
<ArrowDownIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Down</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" aria-label="Delete row" onClick={() => ops?.remove?.(rowIndex)}>
|
||||||
|
<Trash2Icon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Delete</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { useRowSelection } from '@repo/rdx-ui/hooks';
|
import { CheckedState, useRowSelection } from '@repo/rdx-ui/hooks';
|
||||||
import { Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@repo/shadcn-ui/components";
|
import { Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@repo/shadcn-ui/components";
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import { useItemsTableNavigation } from '../../../hooks';
|
import { useInvoiceContext } from '../../../context';
|
||||||
|
import { useInvoiceAutoRecalc, useItemsTableNavigation } from '../../../hooks';
|
||||||
import { useTranslation } from '../../../i18n';
|
import { useTranslation } from '../../../i18n';
|
||||||
import { InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
import { InvoiceFormData, InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||||
import { ItemRow } from './item-row';
|
import { ItemRow } from './item-row';
|
||||||
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
||||||
import { LastCellTabHook } from './last-cell-tab-hook';
|
|
||||||
|
|
||||||
interface ItemsEditorProps {
|
interface ItemsEditorProps {
|
||||||
onChange?: (items: InvoiceItemFormData[]) => void;
|
onChange?: (items: InvoiceItemFormData[]) => void;
|
||||||
@ -16,9 +16,11 @@ interface ItemsEditorProps {
|
|||||||
|
|
||||||
const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
|
const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
|
||||||
|
|
||||||
export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) => {
|
export const ItemsEditor = ({ readOnly = false }: ItemsEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = useFormContext();
|
const context = useInvoiceContext();
|
||||||
|
const form = useFormContext<InvoiceFormData>();
|
||||||
|
const { control } = form;
|
||||||
|
|
||||||
// Navegación y operaciones sobre las filas
|
// Navegación y operaciones sobre las filas
|
||||||
const tableNav = useItemsTableNavigation(form, {
|
const tableNav = useItemsTableNavigation(form, {
|
||||||
@ -27,6 +29,8 @@ export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) =>
|
|||||||
firstEditableField: "description",
|
firstEditableField: "description",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { fieldArray: { fields } } = tableNav;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedRows,
|
selectedRows,
|
||||||
selectedIndexes,
|
selectedIndexes,
|
||||||
@ -34,111 +38,83 @@ export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) =>
|
|||||||
toggleRow,
|
toggleRow,
|
||||||
setSelectAll,
|
setSelectAll,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
} = useRowSelection(tableNav.fa.fields.length);
|
} = useRowSelection(fields.length);
|
||||||
|
|
||||||
const { control } = form;
|
useInvoiceAutoRecalc(form, context);
|
||||||
const items = useWatch({ control: control, name: "items" });
|
|
||||||
|
|
||||||
// propagar cambios a componente padre
|
const handleAddSelection = useCallback(() => {
|
||||||
/*useEffect(() => {
|
|
||||||
onChange?.(items ?? []);
|
|
||||||
}, [items, onChange]);*/
|
|
||||||
|
|
||||||
const handleAdd = useCallback(() => {
|
|
||||||
if (readOnly) return;
|
if (readOnly) return;
|
||||||
tableNav.addEmpty(true);
|
tableNav.addEmpty(true);
|
||||||
}, [readOnly, tableNav]);
|
}, [readOnly, tableNav]);
|
||||||
|
|
||||||
const handleDuplicate = useCallback(() => {
|
const handleDuplicateSelection = useCallback(() => {
|
||||||
if (readOnly || selectedIndexes.length === 0) return;
|
if (readOnly || selectedIndexes.length === 0) return;
|
||||||
// duplicar en orden ascendente no rompe índices
|
// duplicar en orden ascendente no rompe índices
|
||||||
selectedIndexes.forEach((i) => tableNav.duplicate(i));
|
selectedIndexes.forEach((i) => tableNav.duplicate(i));
|
||||||
}, [readOnly, selectedIndexes, tableNav]);
|
}, [readOnly, selectedIndexes, tableNav]);
|
||||||
|
|
||||||
const handleMoveUp = useCallback(() => {
|
const handleMoveUpSelection = useCallback(() => {
|
||||||
if (readOnly || selectedIndexes.length === 0) return;
|
if (readOnly || selectedIndexes.length === 0) return;
|
||||||
// mover de menor a mayor para mantener índices válidos
|
// mover de menor a mayor para mantener índices válidos
|
||||||
selectedIndexes.forEach((i) => tableNav.moveUp(i));
|
selectedIndexes.forEach((i) => tableNav.moveUp(i));
|
||||||
}, [readOnly, selectedIndexes, tableNav]);
|
}, [readOnly, selectedIndexes, tableNav]);
|
||||||
|
|
||||||
const handleMoveDown = useCallback(() => {
|
const handleMoveDownSelection = useCallback(() => {
|
||||||
if (readOnly || selectedIndexes.length === 0) return;
|
if (readOnly || selectedIndexes.length === 0) return;
|
||||||
// mover de mayor a menor evita desplazar objetivos
|
// mover de mayor a menor evita desplazar objetivos
|
||||||
[...selectedIndexes].reverse().forEach((i) => tableNav.moveDown(i));
|
[...selectedIndexes].reverse().forEach((i) => tableNav.moveDown(i));
|
||||||
}, [readOnly, selectedIndexes, tableNav]);
|
}, [readOnly, selectedIndexes, tableNav]);
|
||||||
|
|
||||||
const handleRemove = useCallback(() => {
|
const handleRemoveSelection = useCallback(() => {
|
||||||
if (readOnly || selectedIndexes.length === 0) return;
|
if (readOnly || selectedIndexes.length === 0) return;
|
||||||
// borrar de mayor a menor para no invalidar índices siguientes
|
// borrar de mayor a menor para no invalidar índices siguientes
|
||||||
[...selectedIndexes].reverse().forEach((i) => tableNav.remove(i));
|
[...selectedIndexes].reverse().forEach((i) => tableNav.remove(i));
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}, [readOnly, selectedIndexes, tableNav, clearSelection]);
|
}, [readOnly, selectedIndexes, tableNav, clearSelection]);
|
||||||
|
|
||||||
const hasSelection = selectedIndexes.length > 0;
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
{/* Toolbar selección múltiple */}
|
{/* Toolbar selección múltiple */}
|
||||||
<ItemsEditorToolbar
|
<ItemsEditorToolbar
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
selectedIndexes={selectedIndexes}
|
selectedIndexes={selectedIndexes}
|
||||||
onAdd={() => tableNav.addEmpty(true)}
|
onAdd={handleAddSelection}
|
||||||
onDuplicate={() => selectedIndexes.forEach((i) => tableNav.duplicate(i))}
|
onDuplicate={handleDuplicateSelection}
|
||||||
onMoveUp={() => selectedIndexes.forEach((i) => tableNav.moveUp(i))}
|
onMoveUp={handleMoveUpSelection}
|
||||||
onMoveDown={() => [...selectedIndexes].reverse().forEach((i) => tableNav.moveDown(i))}
|
onMoveDown={handleMoveDownSelection}
|
||||||
onRemove={() => {
|
onRemove={handleRemoveSelection} />
|
||||||
[...selectedIndexes].reverse().forEach((i) => tableNav.remove(i));
|
|
||||||
clearSelection();
|
|
||||||
}} />
|
|
||||||
|
|
||||||
<div className="bg-background">
|
<div className="bg-background">
|
||||||
<Table className="w-full border-collapse text-sm">
|
<Table className="w-full border-collapse text-sm">
|
||||||
<colgroup>
|
|
||||||
<col className='w-[1%]' /> {/* sel */}
|
|
||||||
<col className='w-[1%]' /> {/* # */}
|
|
||||||
<col className='w-[42%]' /> {/* description */}
|
|
||||||
<col className="w-[4%]" /> {/* qty */}
|
|
||||||
<col className="w-[10%]" /> {/* unit */}
|
|
||||||
<col className="w-[4%]" /> {/* discount */}
|
|
||||||
<col className="w-[16%]" /> {/* taxes */}
|
|
||||||
<col className="w-[8%]" /> {/* taxes2 */}
|
|
||||||
<col className="w-[12%]" /> {/* total */}
|
|
||||||
<col className='w-[10%]' /> {/* actions */}
|
|
||||||
</colgroup>
|
|
||||||
<TableHeader className="text-sm bg-muted backdrop-blur supports-[backdrop-filter]:bg-muted/60 ">
|
<TableHeader className="text-sm bg-muted backdrop-blur supports-[backdrop-filter]:bg-muted/60 ">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>
|
<TableHead className='w-[1%] h-5'>
|
||||||
<div className='h-5'>
|
<Checkbox
|
||||||
<Checkbox
|
aria-label={t("common.select_all")}
|
||||||
aria-label={t("common.select_all")}
|
checked={selectAllState}
|
||||||
checked={selectAllState}
|
disabled={readOnly}
|
||||||
disabled={readOnly}
|
onCheckedChange={(checked: CheckedState) => setSelectAll(checked)}
|
||||||
onCheckedChange={(checked) => setSelectAll(checked)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>#</TableHead>
|
<TableHead className='w-[1%]' aria-hidden="true">#</TableHead>
|
||||||
<TableHead>{t("form_fields.item.description.label")}</TableHead>
|
<TableHead className='w-[40%]'>{t("form_fields.item.description.label")}</TableHead>
|
||||||
<TableHead className="text-right">{t("form_fields.item.quantity.label")}</TableHead>
|
<TableHead className="w-[4%] text-right">{t("form_fields.item.quantity.label")}</TableHead>
|
||||||
<TableHead className="text-right">{t("form_fields.item.unit_amount.label")}</TableHead>
|
<TableHead className="w-[10%] text-right">{t("form_fields.item.unit_amount.label")}</TableHead>
|
||||||
<TableHead className="text-right">{t("form_fields.item.discount_percentage.label")}</TableHead>
|
<TableHead className="w-[4%] text-right">{t("form_fields.item.discount_percentage.label")}</TableHead>
|
||||||
<TableHead className="text-right">{t("form_fields.item.tax_codes.label")}</TableHead>
|
<TableHead className="w-[16%] text-right">{t("form_fields.item.tax_codes.label")}</TableHead>
|
||||||
<TableHead className="text-right">{t("form_fields.item.total_amount.label")}</TableHead>
|
<TableHead className="w-[8%] text-right">{t("form_fields.item.total_amount.label")}</TableHead>
|
||||||
<TableHead aria-hidden="true" />
|
<TableHead className='w-[1%]' aria-hidden="true" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className='text-sm'>
|
<TableBody className='text-sm'>
|
||||||
|
|
||||||
{tableNav.fa.fields.map((f, rowIndex) => (
|
{fields.map((f, rowIndex: number) => (
|
||||||
<ItemRow
|
<ItemRow
|
||||||
key={f.id}
|
key={f.id}
|
||||||
control={control}
|
control={control}
|
||||||
item={form.watch(`items.${rowIndex}`)}
|
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
isSelected={selectedRows.has(rowIndex)}
|
isSelected={selectedRows.has(rowIndex)}
|
||||||
isFirst={rowIndex === 0}
|
isFirst={rowIndex === 0}
|
||||||
isLast={rowIndex === tableNav.fa.fields.length - 1}
|
isLast={rowIndex === fields.length - 1}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onToggleSelect={() => toggleRow(rowIndex)}
|
onToggleSelect={() => toggleRow(rowIndex)}
|
||||||
onDuplicate={() => tableNav.duplicate(rowIndex)}
|
onDuplicate={() => tableNav.duplicate(rowIndex)}
|
||||||
@ -151,22 +127,18 @@ export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) =>
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter>
|
<TableFooter>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={9}>
|
<TableCell colSpan={9} className='p-0 m-0'>
|
||||||
<ItemsEditorToolbar
|
<ItemsEditorToolbar
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
selectedIndexes={selectedIndexes}
|
selectedIndexes={selectedIndexes}
|
||||||
onAdd={() => tableNav.addEmpty(true)}
|
onAdd={() => tableNav.addEmpty(true)}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
</TableFooter>
|
</TableFooter>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navegación por TAB: último campo de la fila */}
|
|
||||||
<LastCellTabHook itemsLength={tableNav.fa.fields.length} onTabFromLast={tableNav.onTabFromLastCell} />
|
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,157 +1,48 @@
|
|||||||
import { CheckedState, useRowSelection } from '@repo/rdx-ui/hooks';
|
import { DataTable } from '@repo/rdx-ui/components';
|
||||||
import { Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@repo/shadcn-ui/components";
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useFormContext } from "react-hook-form";
|
|
||||||
import { useInvoiceContext } from '../../../context';
|
import { useInvoiceContext } from '../../../context';
|
||||||
import { useInvoiceAutoRecalc, useItemsTableNavigation } from '../../../hooks';
|
import { useInvoiceAutoRecalc } from '../../../hooks';
|
||||||
import { useTranslation } from '../../../i18n';
|
import { useTranslation } from '../../../i18n';
|
||||||
import { InvoiceFormData, InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
import { InvoiceFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||||
import { ItemRow } from './item-row';
|
import { ItemRowEditor } from './item-row-editor';
|
||||||
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
import { useItemsColumns } from './use-items-columns';
|
||||||
|
|
||||||
interface ItemsEditorProps {
|
|
||||||
onChange?: (items: InvoiceItemFormData[]) => void;
|
|
||||||
readOnly?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
|
const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
|
||||||
|
|
||||||
export const ItemsEditor = ({ readOnly = false }: ItemsEditorProps) => {
|
export const ItemsEditor = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const context = useInvoiceContext();
|
const context = useInvoiceContext();
|
||||||
const form = useFormContext<InvoiceFormData>();
|
const form = useFormContext<InvoiceFormData>();
|
||||||
const { control } = form;
|
const { control, getValues } = form;
|
||||||
|
|
||||||
// Navegación y operaciones sobre las filas
|
|
||||||
const tableNav = useItemsTableNavigation(form, {
|
|
||||||
name: "items",
|
|
||||||
createEmpty: createEmptyItem,
|
|
||||||
firstEditableField: "description",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { fieldArray: { fields } } = tableNav;
|
|
||||||
|
|
||||||
const {
|
|
||||||
selectedRows,
|
|
||||||
selectedIndexes,
|
|
||||||
selectAllState,
|
|
||||||
toggleRow,
|
|
||||||
setSelectAll,
|
|
||||||
clearSelection,
|
|
||||||
} = useRowSelection(fields.length);
|
|
||||||
|
|
||||||
useInvoiceAutoRecalc(form, context);
|
useInvoiceAutoRecalc(form, context);
|
||||||
|
|
||||||
const handleAddSelection = useCallback(() => {
|
const { fields, append, remove, move, insert } = useFieldArray({
|
||||||
if (readOnly) return;
|
control,
|
||||||
tableNav.addEmpty(true);
|
name: "items",
|
||||||
}, [readOnly, tableNav]);
|
});
|
||||||
|
|
||||||
const handleDuplicateSelection = useCallback(() => {
|
const columns = useItemsColumns();
|
||||||
if (readOnly || selectedIndexes.length === 0) return;
|
|
||||||
// duplicar en orden ascendente no rompe índices
|
|
||||||
selectedIndexes.forEach((i) => tableNav.duplicate(i));
|
|
||||||
}, [readOnly, selectedIndexes, tableNav]);
|
|
||||||
|
|
||||||
const handleMoveUpSelection = useCallback(() => {
|
|
||||||
if (readOnly || selectedIndexes.length === 0) return;
|
|
||||||
// mover de menor a mayor para mantener índices válidos
|
|
||||||
selectedIndexes.forEach((i) => tableNav.moveUp(i));
|
|
||||||
}, [readOnly, selectedIndexes, tableNav]);
|
|
||||||
|
|
||||||
const handleMoveDownSelection = useCallback(() => {
|
|
||||||
if (readOnly || selectedIndexes.length === 0) return;
|
|
||||||
// mover de mayor a menor evita desplazar objetivos
|
|
||||||
[...selectedIndexes].reverse().forEach((i) => tableNav.moveDown(i));
|
|
||||||
}, [readOnly, selectedIndexes, tableNav]);
|
|
||||||
|
|
||||||
const handleRemoveSelection = useCallback(() => {
|
|
||||||
if (readOnly || selectedIndexes.length === 0) return;
|
|
||||||
// borrar de mayor a menor para no invalidar índices siguientes
|
|
||||||
[...selectedIndexes].reverse().forEach((i) => tableNav.remove(i));
|
|
||||||
clearSelection();
|
|
||||||
}, [readOnly, selectedIndexes, tableNav, clearSelection]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
{/* Toolbar selección múltiple */}
|
<DataTable columns={columns} data={fields}
|
||||||
<ItemsEditorToolbar
|
pageSize={999}
|
||||||
readOnly={readOnly}
|
getRowId={(r) => (r as any).id}
|
||||||
selectedIndexes={selectedIndexes}
|
meta={{
|
||||||
onAdd={handleAddSelection}
|
rowOps: {
|
||||||
onDuplicate={handleDuplicateSelection}
|
//duplicate: (indexRow: number) => insert(indexRow + 1, { ...getValues(`items.${indexRow}`) /*, id: crypto.randomUUID()*/ }),
|
||||||
onMoveUp={handleMoveUpSelection}
|
remove: (indexRow: number) => remove(indexRow),
|
||||||
onMoveDown={handleMoveDownSelection}
|
move: (fromIndex: number, toIndex: number) => {
|
||||||
onRemove={handleRemoveSelection} />
|
if (toIndex < 0 || toIndex >= fields.length) return;
|
||||||
<div className="bg-background">
|
move(fromIndex, toIndex);
|
||||||
<Table className="w-full border-collapse text-sm">
|
},
|
||||||
<colgroup>
|
canMoveUp: (indexRow: number) => indexRow > 0,
|
||||||
<col className='w-[1%]' />
|
canMoveDown: (indexRow: number, lastIndexRow: number) => indexRow < lastIndexRow,
|
||||||
<col className='w-[1%]' />
|
},
|
||||||
<col className='w-[42%]' />
|
}}
|
||||||
<col className="w-[4%]" />
|
renderRowEditor={(index, close) => <ItemRowEditor index={index} close={close} />} />
|
||||||
<col className="w-[10%]" />
|
|
||||||
<col className="w-[4%]" />
|
|
||||||
<col className="w-[16%]" />
|
|
||||||
<col className="w-[8%]" />
|
|
||||||
<col className="w-[12%]" />
|
|
||||||
</colgroup>
|
|
||||||
<TableHeader className="text-sm bg-muted backdrop-blur supports-[backdrop-filter]:bg-muted/60 ">
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>
|
|
||||||
<div className='h-5'>
|
|
||||||
<Checkbox
|
|
||||||
aria-label={t("common.select_all")}
|
|
||||||
checked={selectAllState}
|
|
||||||
disabled={readOnly}
|
|
||||||
onCheckedChange={(checked: CheckedState) => setSelectAll(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>#</TableHead>
|
|
||||||
<TableHead>{t("form_fields.item.description.label")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("form_fields.item.quantity.label")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("form_fields.item.unit_amount.label")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("form_fields.item.discount_percentage.label")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("form_fields.item.tax_codes.label")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("form_fields.item.total_amount.label")}</TableHead>
|
|
||||||
<TableHead aria-hidden="true" />
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody className='text-sm'>
|
|
||||||
|
|
||||||
{fields.map((f, rowIndex: number) => (
|
|
||||||
<ItemRow
|
|
||||||
key={f.id}
|
|
||||||
control={control}
|
|
||||||
rowIndex={rowIndex}
|
|
||||||
isSelected={selectedRows.has(rowIndex)}
|
|
||||||
isFirst={rowIndex === 0}
|
|
||||||
isLast={rowIndex === fields.length - 1}
|
|
||||||
readOnly={readOnly}
|
|
||||||
onToggleSelect={() => toggleRow(rowIndex)}
|
|
||||||
onDuplicate={() => tableNav.duplicate(rowIndex)}
|
|
||||||
onMoveUp={() => tableNav.moveUp(rowIndex)}
|
|
||||||
onMoveDown={() => tableNav.moveDown(rowIndex)}
|
|
||||||
onRemove={() => tableNav.remove(rowIndex)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={9} className='p-0 m-0'>
|
|
||||||
<ItemsEditorToolbar
|
|
||||||
readOnly={readOnly}
|
|
||||||
selectedIndexes={selectedIndexes}
|
|
||||||
onAdd={() => tableNav.addEmpty(true)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Input } from '@repo/shadcn-ui/components';
|
||||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { findFocusableInCell, focusAndSelect } from './input-utils';
|
import { findFocusableInCell, focusAndSelect } from './input-utils';
|
||||||
@ -198,7 +199,7 @@ export function PercentageInput({
|
|||||||
|
|
||||||
if (readOnly && readOnlyMode === "textlike-input") {
|
if (readOnly && readOnlyMode === "textlike-input") {
|
||||||
return (
|
return (
|
||||||
<input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
readOnly
|
readOnly
|
||||||
@ -219,13 +220,13 @@ export function PercentageInput({
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
pattern="[0-9]*[.,]?[0-9]*%?"
|
pattern="[0-9]*[.,]?[0-9]*%?"
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full bg-transparent p-0 text-right tabular-nums h-8 px-1",
|
"w-full bg-transparent p-0 text-right tabular-nums h-8 px-1 shadow-none",
|
||||||
"border-none",
|
"border-none",
|
||||||
"focus:bg-background",
|
"focus:bg-background",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[2px]",
|
"focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[2px]",
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
// Comentarios en español. TS estricto.
|
// Comentarios en español. TS estricto.
|
||||||
|
|
||||||
import { useQuantity } from '@erp/core/hooks';
|
import { useQuantity } from '@erp/core/hooks';
|
||||||
|
import { Input } from '@repo/shadcn-ui/components';
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { findFocusableInCell, focusAndSelect } from './input-utils';
|
import { findFocusableInCell, focusAndSelect } from './input-utils';
|
||||||
@ -196,7 +197,7 @@ export function QuantityInput({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
readOnly
|
readOnly
|
||||||
@ -217,13 +218,13 @@ export function QuantityInput({
|
|||||||
|
|
||||||
// ── Editable / readOnly normal ──────────────────────────────────────────
|
// ── Editable / readOnly normal ──────────────────────────────────────────
|
||||||
return (
|
return (
|
||||||
<input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
pattern="[0-9]*[.,]?[0-9]*"
|
pattern="[0-9]*[.,]?[0-9]*"
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full bg-transparent p-0 text-right tabular-nums h-8 px-1",
|
"w-full bg-transparent p-0 text-right tabular-nums h-8 px-1 shadow-none",
|
||||||
"border-none",
|
"border-none",
|
||||||
"focus:bg-background",
|
"focus:bg-background",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[2px]",
|
"focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[2px]",
|
||||||
|
|||||||
@ -0,0 +1,218 @@
|
|||||||
|
import { DataTableColumnHeader } from '@repo/rdx-ui/components';
|
||||||
|
import { Checkbox, Textarea } from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { useInvoiceContext } from '../../../context';
|
||||||
|
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
||||||
|
import { AmountInputField } from './amount-input-field';
|
||||||
|
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
||||||
|
import { ItemDataTableRowActions } from './items-data-table-row-actions';
|
||||||
|
import { PercentageInputField } from './percentage-input-field';
|
||||||
|
import { QuantityInputField } from './quantity-input-field';
|
||||||
|
|
||||||
|
|
||||||
|
export interface InvoiceItemFormData {
|
||||||
|
id: string; // ← mapea RHF field.id aquí
|
||||||
|
description: string;
|
||||||
|
quantity: number | "";
|
||||||
|
unit_amount: number | "";
|
||||||
|
discount_percentage: number | "";
|
||||||
|
tax_codes: string[];
|
||||||
|
total_amount: number | ""; // readonly calculado
|
||||||
|
}
|
||||||
|
export interface InvoiceFormData { items: InvoiceItemFormData[] }
|
||||||
|
|
||||||
|
export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
||||||
|
const { t, readOnly, currency_code, language_code } = useInvoiceContext();
|
||||||
|
const { control } = useFormContext<InvoiceFormData>();
|
||||||
|
|
||||||
|
// Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla
|
||||||
|
return React.useMemo<ColumnDef<InvoiceItemFormData>[]>(() => [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
||||||
|
onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
|
||||||
|
aria-label="Select all"
|
||||||
|
className="translate-y-[2px]"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(v) => row.toggleSelected(!!v)}
|
||||||
|
aria-label="Select row"
|
||||||
|
className="translate-y-[2px]"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
size: 48, minSize: 40, maxSize: 64,
|
||||||
|
meta: { className: "w-[4ch]" }, // ancho aprox. por dígitos
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={"#"} className='text-center' />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => row.index + 1,
|
||||||
|
enableSorting: false,
|
||||||
|
size: 32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "description",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("form_fields.item.description.label")} className='text-left' />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.description`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
id={`desc-${row.original.id}`} // ← estable
|
||||||
|
rows={1}
|
||||||
|
aria-label={t("form_fields.item.description.label")}
|
||||||
|
spellCheck
|
||||||
|
readOnly={readOnly}
|
||||||
|
// auto-grow simple
|
||||||
|
onInput={(e) => {
|
||||||
|
const el = e.currentTarget;
|
||||||
|
el.style.height = "auto";
|
||||||
|
el.style.height = `${el.scrollHeight}px`;
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"min-w-[12rem] max-w-[46rem] w-full resize-none bg-transparent border-dashed transition",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-background focus-visible:border-solid",
|
||||||
|
"focus:resize-y"
|
||||||
|
)}
|
||||||
|
data-cell-focus
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 480, minSize: 240, maxSize: 768,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "quantity",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("form_fields.item.quantity.label")} className='text-right' />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<QuantityInputField
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.quantity`}
|
||||||
|
readOnly={readOnly}
|
||||||
|
inputId={`qty-${row.original.id}`}
|
||||||
|
emptyMode="blank"
|
||||||
|
data-row-index={row.index}
|
||||||
|
data-col-index={4}
|
||||||
|
data-cell-focus
|
||||||
|
className="font-base"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 52, minSize: 48, maxSize: 64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "unit_amount",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("form_fields.item.unit_amount.label")} className='text-right' />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<AmountInputField
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.unit_amount`}
|
||||||
|
readOnly={readOnly}
|
||||||
|
inputId={`unit-${row.original.id}`}
|
||||||
|
scale={4}
|
||||||
|
currencyCode={currency_code}
|
||||||
|
languageCode={language_code}
|
||||||
|
data-row-index={row.index}
|
||||||
|
data-col-index={5}
|
||||||
|
data-cell-focus
|
||||||
|
className="font-base"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 120, minSize: 100, maxSize: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "discount_percentage",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("form_fields.item.discount_percentage.label")} className='text-right' />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<PercentageInputField
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.discount_percentage`}
|
||||||
|
readOnly={readOnly}
|
||||||
|
inputId={`disc-${row.original.id}`}
|
||||||
|
scale={4}
|
||||||
|
data-row-index={row.index}
|
||||||
|
data-col-index={6}
|
||||||
|
data-cell-focus
|
||||||
|
className="font-base"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 40, minSize: 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "tax_codes",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("form_fields.item.tax_codes.label")} />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.tax_codes`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomerInvoiceTaxesMultiSelect
|
||||||
|
{...field}
|
||||||
|
inputId={`tax-${row.original.id}`}
|
||||||
|
data-row-index={row.index}
|
||||||
|
data-col-index={7}
|
||||||
|
data-cell-focus
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 240, minSize: 232, maxSize: 320,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "total_amount",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("form_fields.item.total_amount.label")} className='text-right' />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<HoverCardTotalsSummary rowIndex={row.index}>
|
||||||
|
<AmountInputField
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.total_amount`}
|
||||||
|
readOnly
|
||||||
|
inputId={`total-${row.original.id}`}
|
||||||
|
currencyCode={currency_code}
|
||||||
|
languageCode={language_code}
|
||||||
|
className="font-semibold"
|
||||||
|
/>
|
||||||
|
</HoverCardTotalsSummary>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 120, minSize: 100, maxSize: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("components.datatable.actions")} />
|
||||||
|
),
|
||||||
|
cell: ({ row, table }) => <ItemDataTableRowActions row={row} table={table} />,
|
||||||
|
},
|
||||||
|
], [t, readOnly, control, currency_code, language_code,]);
|
||||||
|
}
|
||||||
@ -19,7 +19,6 @@ 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 { useDetailColumns } from "../../hooks";
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { formatCurrency } from "../../pages/create/utils";
|
import { formatCurrency } from "../../pages/create/utils";
|
||||||
import { CustomerInvoiceTaxesMultiSelect } from "../customer-invoice-taxes-multi-select";
|
import { CustomerInvoiceTaxesMultiSelect } from "../customer-invoice-taxes-multi-select";
|
||||||
@ -215,8 +214,8 @@ export const CustomerInvoiceItemsCardEditor = ({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CustomerInvoiceTaxesMultiSelect
|
<CustomerInvoiceTaxesMultiSelect
|
||||||
{...field}
|
{...field}
|
||||||
//onChange={(e) => field.onChange(Number(e.target.value) * 100)}
|
//onChange={(e) => field.onChange(Number(e.target.value) * 100)}
|
||||||
//value={field.value / 100}
|
//value={field.value / 100}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { TaxCatalogProvider } from '@erp/core';
|
import { TaxCatalogProvider } from '@erp/core';
|
||||||
|
import { TFunction } from 'i18next';
|
||||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react";
|
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "../i18n";
|
||||||
|
import { MODULE_NAME } from '../manifest';
|
||||||
|
|
||||||
export type InvoiceContextValue = {
|
export type InvoiceContextValue = {
|
||||||
company_id: string;
|
company_id: string;
|
||||||
@ -12,6 +15,8 @@ export type InvoiceContextValue = {
|
|||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
taxCatalog: TaxCatalogProvider;
|
taxCatalog: TaxCatalogProvider;
|
||||||
|
|
||||||
|
t: TFunction<typeof MODULE_NAME>;
|
||||||
|
|
||||||
changeLanguage: (lang: string) => void;
|
changeLanguage: (lang: string) => void;
|
||||||
changeCurrency: (currency: string) => void;
|
changeCurrency: (currency: string) => void;
|
||||||
changeIsProforma: (value: boolean) => void;
|
changeIsProforma: (value: boolean) => void;
|
||||||
@ -36,6 +41,8 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
|
|||||||
currency_code: initialCurrency = "EUR", readOnly: initialReadOnly = false,
|
currency_code: initialCurrency = "EUR", readOnly: initialReadOnly = false,
|
||||||
is_proforma: initialProforma = true, children }: PropsWithChildren<InvoiceProviderParams>) => {
|
is_proforma: initialProforma = true, children }: PropsWithChildren<InvoiceProviderParams>) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Estado interno local para campos dinámicos
|
// Estado interno local para campos dinámicos
|
||||||
const [language_code, setLanguage] = useState(initialLang);
|
const [language_code, setLanguage] = useState(initialLang);
|
||||||
const [currency_code, setCurrency] = useState(initialCurrency);
|
const [currency_code, setCurrency] = useState(initialCurrency);
|
||||||
@ -53,6 +60,8 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
|
|||||||
const value = useMemo<InvoiceContextValue>(() => {
|
const value = useMemo<InvoiceContextValue>(() => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
t,
|
||||||
|
|
||||||
invoice_id,
|
invoice_id,
|
||||||
company_id,
|
company_id,
|
||||||
status,
|
status,
|
||||||
@ -68,7 +77,7 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
|
|||||||
changeIsProforma: setIsProformaMemo,
|
changeIsProforma: setIsProformaMemo,
|
||||||
setReadOnly: setReadOnlyMemo,
|
setReadOnly: setReadOnlyMemo,
|
||||||
}
|
}
|
||||||
}, [readOnly, company_id, invoice_id, status, language_code, currency_code, is_proforma, taxCatalog, setLanguageMemo, setCurrencyMemo, setIsProformaMemo, setReadOnlyMemo]);
|
}, [t, readOnly, company_id, invoice_id, status, language_code, currency_code, is_proforma, taxCatalog, setLanguageMemo, setCurrencyMemo, setIsProformaMemo, setReadOnlyMemo]);
|
||||||
|
|
||||||
return <InvoiceContext.Provider value={value}>{children}</InvoiceContext.Provider>;
|
return <InvoiceContext.Provider value={value}>{children}</InvoiceContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
export * from "./calcs";
|
export * from "./calcs";
|
||||||
export * from "./use-create-customer-invoice-mutation";
|
export * from "./use-create-customer-invoice-mutation";
|
||||||
export * from "./use-customer-invoices-query";
|
export * from "./use-customer-invoices-query";
|
||||||
export * from "./use-detail-columns";
|
|
||||||
export * from "./use-invoice-query";
|
export * from "./use-invoice-query";
|
||||||
export * from "./use-items-table-navigation";
|
export * from "./use-items-table-navigation";
|
||||||
export * from "./use-update-customer-invoice-mutation";
|
export * from "./use-update-customer-invoice-mutation";
|
||||||
|
|||||||
@ -1,8 +1,3 @@
|
|||||||
import {
|
|
||||||
DataTablaRowActionFunction,
|
|
||||||
DataTableRowActions,
|
|
||||||
DataTableRowDragHandleCell,
|
|
||||||
} from "@repo/rdx-ui/components";
|
|
||||||
import { Checkbox } from "@repo/shadcn-ui/components";
|
import { Checkbox } from "@repo/shadcn-ui/components";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { i18n } from "i18next";
|
import { KeyPrefix, Namespace, i18n } from "i18next";
|
||||||
import { useTranslation as useI18NextTranslation } from "react-i18next";
|
import { UseTranslationResponse, useTranslation as useI18NextTranslation } from "react-i18next";
|
||||||
import enResources from "../common/locales/en.json";
|
import enResources from "../common/locales/en.json";
|
||||||
import esResources from "../common/locales/es.json";
|
import esResources from "../common/locales/es.json";
|
||||||
import { MODULE_NAME } from "./manifest";
|
import { MODULE_NAME } from "./manifest";
|
||||||
@ -17,9 +17,13 @@ const addMissingBundles = (i18n: i18n) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTranslation = () => {
|
export const useTranslation = <
|
||||||
|
Ns extends Namespace = typeof MODULE_NAME,
|
||||||
|
K extends KeyPrefix<Ns> = undefined,
|
||||||
|
>(
|
||||||
|
keyPrefix?: K
|
||||||
|
): UseTranslationResponse<Ns, K> => {
|
||||||
const { i18n } = useI18NextTranslation();
|
const { i18n } = useI18NextTranslation();
|
||||||
addMissingBundles(i18n);
|
addMissingBundles(i18n);
|
||||||
|
return useI18NextTranslation(MODULE_NAME, { keyPrefix });
|
||||||
return useI18NextTranslation(MODULE_NAME);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { Column } from "@tanstack/react-table";
|
||||||
|
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../locales/i18n.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button, DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from '@repo/shadcn-ui/components';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
|
||||||
|
interface DataTableColumnHeaderProps<TData, TValue>
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
column: Column<TData, TValue>
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableColumnHeader<TData, TValue>({
|
||||||
|
column,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!column.getCanSort()) {
|
||||||
|
return <div className={cn("text-xs text-muted-foreground text-nowrap", className)}>{title}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center gap-2", className)}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="data-[state=open]:bg-accent -ml-3 h-8 text-xs text-muted-foreground text-nowrap cursor-pointer"
|
||||||
|
>
|
||||||
|
<span>{title}</span>
|
||||||
|
{column.getIsSorted() === "desc" ? (
|
||||||
|
<ArrowDown />
|
||||||
|
) : column.getIsSorted() === "asc" ? (
|
||||||
|
<ArrowUp />
|
||||||
|
) : (
|
||||||
|
<ChevronsUpDown />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||||
|
<ArrowUp />
|
||||||
|
{t("components.datatabla.asc")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||||
|
<ArrowDown />
|
||||||
|
{t("components.datatabla.desc")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||||
|
<EyeOff />
|
||||||
|
{t("components.datatabla.hide")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
import { Column } from "@tanstack/react-table"
|
||||||
|
import { Check, PlusCircle } from "lucide-react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge, Button, Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator, Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger, Separator
|
||||||
|
} from '@repo/shadcn-ui/components'
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils'
|
||||||
|
|
||||||
|
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||||
|
column?: Column<TData, TValue>
|
||||||
|
title?: string
|
||||||
|
options: {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
icon?: React.ComponentType<{ className?: string }>
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableFacetedFilter<TData, TValue>({
|
||||||
|
column,
|
||||||
|
title,
|
||||||
|
options,
|
||||||
|
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||||
|
const facets = column?.getFacetedUniqueValues()
|
||||||
|
const selectedValues = new Set(column?.getFilterValue() as string[])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
||||||
|
<PlusCircle />
|
||||||
|
{title}
|
||||||
|
{selectedValues?.size > 0 && (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="rounded-sm px-1 font-normal lg:hidden"
|
||||||
|
>
|
||||||
|
{selectedValues.size}
|
||||||
|
</Badge>
|
||||||
|
<div className="hidden gap-1 lg:flex">
|
||||||
|
{selectedValues.size > 2 ? (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="rounded-sm px-1 font-normal"
|
||||||
|
>
|
||||||
|
{selectedValues.size} selected
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
options
|
||||||
|
.filter((option) => selectedValues.has(option.value))
|
||||||
|
.map((option) => (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
key={option.value}
|
||||||
|
className="rounded-sm px-1 font-normal"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder={title} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((option) => {
|
||||||
|
const isSelected = selectedValues.has(option.value)
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => {
|
||||||
|
if (isSelected) {
|
||||||
|
selectedValues.delete(option.value)
|
||||||
|
} else {
|
||||||
|
selectedValues.add(option.value)
|
||||||
|
}
|
||||||
|
const filterValues = Array.from(selectedValues)
|
||||||
|
column?.setFilterValue(
|
||||||
|
filterValues.length ? filterValues : undefined
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex size-4 items-center justify-center rounded-[4px] border",
|
||||||
|
isSelected
|
||||||
|
? "bg-primary border-primary text-primary-foreground"
|
||||||
|
: "border-input [&_svg]:invisible"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Check className="text-primary-foreground size-3.5" />
|
||||||
|
</div>
|
||||||
|
{option.icon && (
|
||||||
|
<option.icon className="text-muted-foreground size-4" />
|
||||||
|
)}
|
||||||
|
<span>{option.label}</span>
|
||||||
|
{facets?.get(option.value) && (
|
||||||
|
<span className="text-muted-foreground ml-auto flex size-4 items-center justify-center font-mono text-xs">
|
||||||
|
{facets.get(option.value)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
{selectedValues.size > 0 && (
|
||||||
|
<>
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => column?.setFilterValue(undefined)}
|
||||||
|
className="justify-center text-center"
|
||||||
|
>
|
||||||
|
Clear filters
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import { Table } from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button, Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@repo/shadcn-ui/components'
|
||||||
|
|
||||||
|
interface DataTablePaginationProps<TData> {
|
||||||
|
table: Table<TData>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTablePagination<TData>({
|
||||||
|
table,
|
||||||
|
}: DataTablePaginationProps<TData>) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-2">
|
||||||
|
<div className="text-muted-foreground flex-1 text-sm">
|
||||||
|
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||||
|
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-6 lg:space-x-8">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<p className="text-sm font-medium">Rows per page</p>
|
||||||
|
<Select
|
||||||
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
table.setPageSize(Number(value))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-[70px]">
|
||||||
|
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side="top">
|
||||||
|
{[10, 20, 25, 30, 40, 50].map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
|
{pageSize}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||||
|
Page {table.getState().pagination.pageIndex + 1} of{" "}
|
||||||
|
{table.getPageCount()}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="hidden size-8 lg:flex"
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to first page</span>
|
||||||
|
<ChevronsLeft />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to previous page</span>
|
||||||
|
<ChevronLeft />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to next page</span>
|
||||||
|
<ChevronRight />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="hidden size-8 lg:flex"
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to last page</span>
|
||||||
|
<ChevronsRight />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components'
|
||||||
|
import { Table } from "@tanstack/react-table"
|
||||||
|
import { DataTableViewOptions } from './data-table-view-options.tsx'
|
||||||
|
|
||||||
|
|
||||||
|
interface DataTableToolbarProps<TData> {
|
||||||
|
table: Table<TData>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableToolbar<TData>({
|
||||||
|
table,
|
||||||
|
}: DataTableToolbarProps<TData>) {
|
||||||
|
const isFiltered = table.getState().columnFilters.length > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-1 items-center gap-2">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DataTableViewOptions table={table} />
|
||||||
|
<Button size="sm">Add Task</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Table } from "@tanstack/react-table"
|
||||||
|
import { Settings2 } from "lucide-react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button, DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from '@repo/shadcn-ui/components'
|
||||||
|
|
||||||
|
export function DataTableViewOptions<TData>({
|
||||||
|
table,
|
||||||
|
}: {
|
||||||
|
table: Table<TData>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="ml-auto hidden h-8 lg:flex"
|
||||||
|
>
|
||||||
|
<Settings2 />
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-[150px]">
|
||||||
|
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter(
|
||||||
|
(column) =>
|
||||||
|
typeof column.accessorFn !== "undefined" && column.getCanHide()
|
||||||
|
)
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
className="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
183
packages/rdx-ui/src/components/datatable/data-table.tsx
Normal file
183
packages/rdx-ui/src/components/datatable/data-table.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
ColumnFiltersState,
|
||||||
|
ColumnSizingState,
|
||||||
|
SortingState,
|
||||||
|
VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFacetedRowModel,
|
||||||
|
getFacetedUniqueValues,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
Table, TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@repo/shadcn-ui/components'
|
||||||
|
import { DataTablePagination } from './data-table-pagination.tsx'
|
||||||
|
import { DataTableToolbar } from "./data-table-toolbar.tsx"
|
||||||
|
|
||||||
|
import { useTranslation } from "../../locales/i18n.ts"
|
||||||
|
|
||||||
|
|
||||||
|
type DataTableRowOps = {
|
||||||
|
duplicate?(index: number): void;
|
||||||
|
remove?(index: number): void;
|
||||||
|
move?(from: number, to: number): void;
|
||||||
|
canMoveUp?(index: number): boolean;
|
||||||
|
canMoveDown?(index: number, lastIndex: number): boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[]
|
||||||
|
data: TData[],
|
||||||
|
meta?: Record<string, any>,
|
||||||
|
|
||||||
|
getRowId?: (row: TData, index: number) => string;
|
||||||
|
pageSize?: number;
|
||||||
|
enableRowSelection?: boolean;
|
||||||
|
|
||||||
|
renderRowEditor?: (index: number, close: () => void) => React.ReactNode; // editor modal opcional. Se muestra dentro de un Dialog.
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
meta,
|
||||||
|
getRowId,
|
||||||
|
pageSize = 25,
|
||||||
|
enableRowSelection = true,
|
||||||
|
renderRowEditor,
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||||
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||||
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
|
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
|
||||||
|
const [editIndex, setEditIndex] = React.useState<number | null>(null);
|
||||||
|
|
||||||
|
const openEditor = React.useCallback((i: number) => setEditIndex(i), []);
|
||||||
|
const closeEditor = React.useCallback(() => setEditIndex(null), []);
|
||||||
|
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
columnResizeMode: "onChange",
|
||||||
|
onColumnSizingChange: setColSizes,
|
||||||
|
getRowId: getRowId ?? ((row: any, idx) => (row?.id ? String(row.id) : String(idx))),
|
||||||
|
state: {
|
||||||
|
columnSizing: colSizes,
|
||||||
|
sorting,
|
||||||
|
columnVisibility,
|
||||||
|
rowSelection,
|
||||||
|
columnFilters,
|
||||||
|
},
|
||||||
|
initialState: { pagination: { pageSize } },
|
||||||
|
meta: { ...meta, openEditor },
|
||||||
|
|
||||||
|
enableRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFacetedRowModel: getFacetedRowModel(),
|
||||||
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<DataTableToolbar table={table} />
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-md border">
|
||||||
|
<Table className="w-full text-sm">
|
||||||
|
<TableHeader className="sticky top-0 bg-muted hover:bg-muted z-10">
|
||||||
|
{table.getHeaderGroups().map((hg) => (
|
||||||
|
<TableRow key={hg.id}>
|
||||||
|
{hg.headers.map((h) => {
|
||||||
|
const w = h.getSize(); // px
|
||||||
|
const minW = h.column.columnDef.minSize;
|
||||||
|
const maxW = h.column.columnDef.maxSize;
|
||||||
|
return (
|
||||||
|
<TableHead
|
||||||
|
key={h.id}
|
||||||
|
colSpan={h.colSpan}
|
||||||
|
style={{
|
||||||
|
width: w ? `${w}px` : undefined,
|
||||||
|
minWidth: typeof minW === "number" ? `${minW}px` : undefined,
|
||||||
|
maxWidth: typeof maxW === "number" ? `${maxW}px` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{row.getVisibleCells().map((cell) => {
|
||||||
|
const w = cell.column.getSize();
|
||||||
|
const minW = cell.column.columnDef.minSize;
|
||||||
|
const maxW = cell.column.columnDef.maxSize;
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
key={cell.id}
|
||||||
|
className="align-top"
|
||||||
|
style={{
|
||||||
|
width: w ? `${w}px` : undefined,
|
||||||
|
minWidth: typeof minW === "number" ? `${minW}px` : undefined,
|
||||||
|
maxWidth: typeof maxW === "number" ? `${maxW}px` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
{t("components.datatabla.empty")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTablePagination table={table} />
|
||||||
|
|
||||||
|
{!!renderRowEditor && editIndex !== null && (
|
||||||
|
<Dialog open onOpenChange={(open) => (!open ? closeEditor() : null)}>
|
||||||
|
<DialogContent className="max-w-xl">
|
||||||
|
{renderRowEditor(editIndex, closeEditor)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,3 +1,3 @@
|
|||||||
export * from "./datatable-column-header.tsx";
|
export * from "./data-table-column-header.tsx";
|
||||||
export * from "./datatable-row-actions.tsx";
|
export * from "./data-table.tsx";
|
||||||
export * from "./datatable-row-drag-handle-cell.tsx";
|
|
||||||
|
|||||||
62
packages/rdx-ui/src/components/datatable/user-nav.tsx
Normal file
62
packages/rdx-ui/src/components/datatable/user-nav.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from "@/registry/new-york-v4/ui/avatar"
|
||||||
|
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||||
|
|
||||||
|
export function UserNav() {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||||
|
<Avatar className="h-9 w-9">
|
||||||
|
<AvatarImage src="/avatars/03.png" alt="@shadcn" />
|
||||||
|
<AvatarFallback>SC</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||||
|
<DropdownMenuLabel className="font-normal">
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-sm leading-none font-medium">shadcn</p>
|
||||||
|
<p className="text-muted-foreground text-xs leading-none">
|
||||||
|
m@example.com
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Profile
|
||||||
|
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Billing
|
||||||
|
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Settings
|
||||||
|
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Log out
|
||||||
|
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
3
packages/rdx-ui/src/components/datatable2/index.tsx
Normal file
3
packages/rdx-ui/src/components/datatable2/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./datatable-column-header.tsx";
|
||||||
|
export * from "./datatable-row-actions.tsx";
|
||||||
|
export * from "./datatable-row-drag-handle-cell.tsx";
|
||||||
@ -13,3 +13,4 @@ export * from "./multi-select.tsx";
|
|||||||
export * from "./multiple-selector.tsx";
|
export * from "./multiple-selector.tsx";
|
||||||
export * from "./scroll-to-top.tsx";
|
export * from "./scroll-to-top.tsx";
|
||||||
export * from "./tailwind-indicator.tsx";
|
export * from "./tailwind-indicator.tsx";
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useTranslation } from "@repo/rdx-ui/locales/i18n.ts";
|
import { useTranslation } from "../../locales/i18n.ts";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|||||||
@ -7,6 +7,13 @@
|
|||||||
"read_only": "Read only"
|
"read_only": "Read only"
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
"datatable": {
|
||||||
|
"asc": "Asc",
|
||||||
|
"desc": "Desc",
|
||||||
|
"hide": "Hide",
|
||||||
|
"empty": "No results found",
|
||||||
|
"actions": "Actions"
|
||||||
|
},
|
||||||
"loading_indicator": {
|
"loading_indicator": {
|
||||||
"title": "Loading...",
|
"title": "Loading...",
|
||||||
"subtitle": "This may take a few seconds. Please do not close this page."
|
"subtitle": "This may take a few seconds. Please do not close this page."
|
||||||
|
|||||||
@ -7,6 +7,13 @@
|
|||||||
"search": "Buscar"
|
"search": "Buscar"
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
"datatable": {
|
||||||
|
"asc": "Asc",
|
||||||
|
"desc": "Desc",
|
||||||
|
"hide": "Ocultar",
|
||||||
|
"empty": "No hay resultados",
|
||||||
|
"actions": "Acciones"
|
||||||
|
},
|
||||||
"loading_indicator": {
|
"loading_indicator": {
|
||||||
"title": "Cargando...",
|
"title": "Cargando...",
|
||||||
"subtitle": "Esto puede tardar unos segundos. Por favor, no cierre esta página."
|
"subtitle": "Esto puede tardar unos segundos. Por favor, no cierre esta página."
|
||||||
|
|||||||
@ -8,6 +8,9 @@
|
|||||||
|
|
||||||
"plugins": [{ "name": "typescript-plugin-css-modules" }]
|
"plugins": [{ "name": "typescript-plugin-css-modules" }]
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": [
|
||||||
|
"src",
|
||||||
|
"../../modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx"
|
||||||
|
],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user