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",
|
||||
"value": "3500",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"description": "Retención profesional o fiscal tipo máximo.",
|
||||
"description": "Retenc. profesional o fiscal tipo máximo.",
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "Retención 19%",
|
||||
"name": "Retenc. 19%",
|
||||
"code": "retencion_19",
|
||||
"value": "1900",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"description": "Retención IRPF general.",
|
||||
"description": "Retenc. IRPF general.",
|
||||
"aeat_code": "R1"
|
||||
},
|
||||
{
|
||||
"name": "Retención 15%",
|
||||
"name": "Retenc. 15%",
|
||||
"code": "retencion_15",
|
||||
"value": "1500",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"description": "Retención para autónomos y profesionales.",
|
||||
"description": "Retenc. para autónomos y profesionales.",
|
||||
"aeat_code": "R2"
|
||||
},
|
||||
{
|
||||
"name": "Retención 7%",
|
||||
"name": "Retenc. 7%",
|
||||
"code": "retencion_7",
|
||||
"value": "700",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"description": "Retención para nuevos autónomos.",
|
||||
"description": "Retenc. para nuevos autónomos.",
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "Retención 2%",
|
||||
"name": "Retenc. 2%",
|
||||
"code": "retencion_2",
|
||||
"value": "200",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"description": "Retención sobre arrendamientos de inmuebles urbanos.",
|
||||
"description": "Retenc. sobre arrendamientos de inmuebles urbanos.",
|
||||
"aeat_code": "R3"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Rec. de equivalencia 5,2%",
|
||||
"name": "Rec. 5,2%",
|
||||
"code": "rec_5_2",
|
||||
"value": "520",
|
||||
"scale": "2",
|
||||
@ -191,7 +191,7 @@
|
||||
"aeat_code": "51"
|
||||
},
|
||||
{
|
||||
"name": "Rec. de equivalencia 1,75%",
|
||||
"name": "Rec. 1,75%",
|
||||
"code": "rec_1_75",
|
||||
"value": "175",
|
||||
"scale": "2",
|
||||
@ -200,7 +200,7 @@
|
||||
"aeat_code": "52"
|
||||
},
|
||||
{
|
||||
"name": "Rec. de equivalencia 1,4%",
|
||||
"name": "Rec. 1,4%",
|
||||
"code": "rec_1_4",
|
||||
"value": "140",
|
||||
"scale": "2",
|
||||
@ -209,7 +209,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "Rec. de equivalencia 1%",
|
||||
"name": "Rec. 1%",
|
||||
"code": "rec_1",
|
||||
"value": "100",
|
||||
"scale": "2",
|
||||
@ -218,7 +218,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "Rec. de equivalencia 0,62%",
|
||||
"name": "Rec. 0,62%",
|
||||
"code": "rec_0_62",
|
||||
"value": "62",
|
||||
"scale": "2",
|
||||
@ -227,7 +227,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "Rec. de equivalencia 0,5%",
|
||||
"name": "Rec. 0,5%",
|
||||
"code": "rec_0_5",
|
||||
"value": "50",
|
||||
"scale": "2",
|
||||
@ -236,7 +236,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "Rec. de equivalencia 0,26%",
|
||||
"name": "Rec. 0,26%",
|
||||
"code": "rec_0_26",
|
||||
"value": "26",
|
||||
"scale": "2",
|
||||
@ -245,7 +245,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "Rec. de equivalencia 0%",
|
||||
"name": "Rec. 0%",
|
||||
"code": "rec_0",
|
||||
"value": "0",
|
||||
"scale": "2",
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"rows_selected": "{{count}} fila(s) seleccionadas.",
|
||||
"rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas."
|
||||
},
|
||||
|
||||
"catalog": {
|
||||
"status": {
|
||||
"draft": "Draft",
|
||||
@ -198,6 +199,9 @@
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"datatable": {
|
||||
"actions": "Actions"
|
||||
},
|
||||
"customer_invoice_taxes_multi_select": {
|
||||
"label": "Taxes",
|
||||
"placeholder": "Select taxes",
|
||||
|
||||
@ -191,6 +191,9 @@
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"datatable": {
|
||||
"actions": "Acciones"
|
||||
},
|
||||
"customer_invoice_taxes_multi_select": {
|
||||
"label": "Impuestos",
|
||||
"placeholder": "Selecciona impuestos",
|
||||
|
||||
@ -4,41 +4,9 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { useCallback, useMemo } from 'react';
|
||||
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 {
|
||||
value: string[];
|
||||
value?: string[];
|
||||
onChange: (selectedValues: string[]) => void;
|
||||
className?: string;
|
||||
[key: string]: any; // Allow other props to be passed
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { formatCurrency } from '@erp/core';
|
||||
import { useMoney } from '@erp/core/hooks';
|
||||
import { Input } from '@repo/shadcn-ui/components';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import * as React from "react";
|
||||
import { findFocusableInCell, focusAndSelect } from './input-utils';
|
||||
@ -166,7 +167,7 @@ export function AmountInput({
|
||||
|
||||
if (readOnly && readOnlyMode === "textlike-input") {
|
||||
return (
|
||||
<input
|
||||
<Input
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
readOnly
|
||||
@ -186,13 +187,13 @@ export function AmountInput({
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
<Input
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
inputMode="decimal"
|
||||
pattern="[0-9]*[.,]?[0-9]*"
|
||||
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",
|
||||
"focus:bg-background",
|
||||
"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 { useCallback } from 'react';
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { useItemsTableNavigation } from '../../../hooks';
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useInvoiceContext } from '../../../context';
|
||||
import { useInvoiceAutoRecalc, useItemsTableNavigation } from '../../../hooks';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||
import { InvoiceFormData, InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||
import { ItemRow } from './item-row';
|
||||
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
||||
import { LastCellTabHook } from './last-cell-tab-hook';
|
||||
|
||||
interface ItemsEditorProps {
|
||||
onChange?: (items: InvoiceItemFormData[]) => void;
|
||||
@ -16,9 +16,11 @@ interface ItemsEditorProps {
|
||||
|
||||
const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
|
||||
|
||||
export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) => {
|
||||
export const ItemsEditor = ({ readOnly = false }: ItemsEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const context = useInvoiceContext();
|
||||
const form = useFormContext<InvoiceFormData>();
|
||||
const { control } = form;
|
||||
|
||||
// Navegación y operaciones sobre las filas
|
||||
const tableNav = useItemsTableNavigation(form, {
|
||||
@ -27,6 +29,8 @@ export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) =>
|
||||
firstEditableField: "description",
|
||||
});
|
||||
|
||||
const { fieldArray: { fields } } = tableNav;
|
||||
|
||||
const {
|
||||
selectedRows,
|
||||
selectedIndexes,
|
||||
@ -34,111 +38,83 @@ export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) =>
|
||||
toggleRow,
|
||||
setSelectAll,
|
||||
clearSelection,
|
||||
} = useRowSelection(tableNav.fa.fields.length);
|
||||
} = useRowSelection(fields.length);
|
||||
|
||||
const { control } = form;
|
||||
const items = useWatch({ control: control, name: "items" });
|
||||
useInvoiceAutoRecalc(form, context);
|
||||
|
||||
// propagar cambios a componente padre
|
||||
/*useEffect(() => {
|
||||
onChange?.(items ?? []);
|
||||
}, [items, onChange]);*/
|
||||
|
||||
const handleAdd = useCallback(() => {
|
||||
const handleAddSelection = useCallback(() => {
|
||||
if (readOnly) return;
|
||||
tableNav.addEmpty(true);
|
||||
}, [readOnly, tableNav]);
|
||||
|
||||
const handleDuplicate = useCallback(() => {
|
||||
const handleDuplicateSelection = useCallback(() => {
|
||||
if (readOnly || selectedIndexes.length === 0) return;
|
||||
// duplicar en orden ascendente no rompe índices
|
||||
selectedIndexes.forEach((i) => tableNav.duplicate(i));
|
||||
}, [readOnly, selectedIndexes, tableNav]);
|
||||
|
||||
const handleMoveUp = useCallback(() => {
|
||||
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 handleMoveDown = useCallback(() => {
|
||||
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 handleRemove = useCallback(() => {
|
||||
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]);
|
||||
|
||||
const hasSelection = selectedIndexes.length > 0;
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-0">
|
||||
{/* Toolbar selección múltiple */}
|
||||
<ItemsEditorToolbar
|
||||
readOnly={readOnly}
|
||||
selectedIndexes={selectedIndexes}
|
||||
onAdd={() => tableNav.addEmpty(true)}
|
||||
onDuplicate={() => selectedIndexes.forEach((i) => tableNav.duplicate(i))}
|
||||
onMoveUp={() => selectedIndexes.forEach((i) => tableNav.moveUp(i))}
|
||||
onMoveDown={() => [...selectedIndexes].reverse().forEach((i) => tableNav.moveDown(i))}
|
||||
onRemove={() => {
|
||||
[...selectedIndexes].reverse().forEach((i) => tableNav.remove(i));
|
||||
clearSelection();
|
||||
}} />
|
||||
|
||||
onAdd={handleAddSelection}
|
||||
onDuplicate={handleDuplicateSelection}
|
||||
onMoveUp={handleMoveUpSelection}
|
||||
onMoveDown={handleMoveDownSelection}
|
||||
onRemove={handleRemoveSelection} />
|
||||
<div className="bg-background">
|
||||
<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 ">
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
<div className='h-5'>
|
||||
<TableHead className='w-[1%] h-5'>
|
||||
<Checkbox
|
||||
aria-label={t("common.select_all")}
|
||||
checked={selectAllState}
|
||||
disabled={readOnly}
|
||||
onCheckedChange={(checked) => setSelectAll(checked)}
|
||||
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" />
|
||||
<TableHead className='w-[1%]' aria-hidden="true">#</TableHead>
|
||||
<TableHead className='w-[40%]'>{t("form_fields.item.description.label")}</TableHead>
|
||||
<TableHead className="w-[4%] text-right">{t("form_fields.item.quantity.label")}</TableHead>
|
||||
<TableHead className="w-[10%] text-right">{t("form_fields.item.unit_amount.label")}</TableHead>
|
||||
<TableHead className="w-[4%] text-right">{t("form_fields.item.discount_percentage.label")}</TableHead>
|
||||
<TableHead className="w-[16%] text-right">{t("form_fields.item.tax_codes.label")}</TableHead>
|
||||
<TableHead className="w-[8%] text-right">{t("form_fields.item.total_amount.label")}</TableHead>
|
||||
<TableHead className='w-[1%]' aria-hidden="true" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className='text-sm'>
|
||||
|
||||
{tableNav.fa.fields.map((f, rowIndex) => (
|
||||
{fields.map((f, rowIndex: number) => (
|
||||
<ItemRow
|
||||
key={f.id}
|
||||
control={control}
|
||||
item={form.watch(`items.${rowIndex}`)}
|
||||
rowIndex={rowIndex}
|
||||
isSelected={selectedRows.has(rowIndex)}
|
||||
isFirst={rowIndex === 0}
|
||||
isLast={rowIndex === tableNav.fa.fields.length - 1}
|
||||
isLast={rowIndex === fields.length - 1}
|
||||
readOnly={readOnly}
|
||||
onToggleSelect={() => toggleRow(rowIndex)}
|
||||
onDuplicate={() => tableNav.duplicate(rowIndex)}
|
||||
@ -151,22 +127,18 @@ export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) =>
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={9}>
|
||||
<TableCell colSpan={9} className='p-0 m-0'>
|
||||
<ItemsEditorToolbar
|
||||
readOnly={readOnly}
|
||||
selectedIndexes={selectedIndexes}
|
||||
onAdd={() => tableNav.addEmpty(true)}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
</TableRow>
|
||||
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Navegación por TAB: último campo de la fila */}
|
||||
<LastCellTabHook itemsLength={tableNav.fa.fields.length} onTabFromLast={tableNav.onTabFromLastCell} />
|
||||
</div >
|
||||
);
|
||||
}
|
||||
@ -1,157 +1,48 @@
|
||||
import { CheckedState, useRowSelection } from '@repo/rdx-ui/hooks';
|
||||
import { Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@repo/shadcn-ui/components";
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { DataTable } from '@repo/rdx-ui/components';
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useInvoiceContext } from '../../../context';
|
||||
import { useInvoiceAutoRecalc, useItemsTableNavigation } from '../../../hooks';
|
||||
import { useInvoiceAutoRecalc } from '../../../hooks';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { InvoiceFormData, InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||
import { ItemRow } from './item-row';
|
||||
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
||||
import { InvoiceFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||
import { ItemRowEditor } from './item-row-editor';
|
||||
import { useItemsColumns } from './use-items-columns';
|
||||
|
||||
interface ItemsEditorProps {
|
||||
onChange?: (items: InvoiceItemFormData[]) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
|
||||
|
||||
export const ItemsEditor = ({ readOnly = false }: ItemsEditorProps) => {
|
||||
export const ItemsEditor = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useInvoiceContext();
|
||||
const form = useFormContext<InvoiceFormData>();
|
||||
const { control } = 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);
|
||||
const { control, getValues } = form;
|
||||
|
||||
useInvoiceAutoRecalc(form, context);
|
||||
|
||||
const handleAddSelection = useCallback(() => {
|
||||
if (readOnly) return;
|
||||
tableNav.addEmpty(true);
|
||||
}, [readOnly, tableNav]);
|
||||
const { fields, append, remove, move, insert } = useFieldArray({
|
||||
control,
|
||||
name: "items",
|
||||
});
|
||||
|
||||
const handleDuplicateSelection = useCallback(() => {
|
||||
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]);
|
||||
const columns = useItemsColumns();
|
||||
|
||||
return (
|
||||
<div className="space-y-0">
|
||||
{/* Toolbar selección múltiple */}
|
||||
<ItemsEditorToolbar
|
||||
readOnly={readOnly}
|
||||
selectedIndexes={selectedIndexes}
|
||||
onAdd={handleAddSelection}
|
||||
onDuplicate={handleDuplicateSelection}
|
||||
onMoveUp={handleMoveUpSelection}
|
||||
onMoveDown={handleMoveDownSelection}
|
||||
onRemove={handleRemoveSelection} />
|
||||
<div className="bg-background">
|
||||
<Table className="w-full border-collapse text-sm">
|
||||
<colgroup>
|
||||
<col className='w-[1%]' />
|
||||
<col className='w-[1%]' />
|
||||
<col className='w-[42%]' />
|
||||
<col className="w-[4%]" />
|
||||
<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>
|
||||
|
||||
<DataTable columns={columns} data={fields}
|
||||
pageSize={999}
|
||||
getRowId={(r) => (r as any).id}
|
||||
meta={{
|
||||
rowOps: {
|
||||
//duplicate: (indexRow: number) => insert(indexRow + 1, { ...getValues(`items.${indexRow}`) /*, id: crypto.randomUUID()*/ }),
|
||||
remove: (indexRow: number) => remove(indexRow),
|
||||
move: (fromIndex: number, toIndex: number) => {
|
||||
if (toIndex < 0 || toIndex >= fields.length) return;
|
||||
move(fromIndex, toIndex);
|
||||
},
|
||||
canMoveUp: (indexRow: number) => indexRow > 0,
|
||||
canMoveDown: (indexRow: number, lastIndexRow: number) => indexRow < lastIndexRow,
|
||||
},
|
||||
}}
|
||||
renderRowEditor={(index, close) => <ItemRowEditor index={index} close={close} />} />
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Input } from '@repo/shadcn-ui/components';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import * as React from "react";
|
||||
import { findFocusableInCell, focusAndSelect } from './input-utils';
|
||||
@ -198,7 +199,7 @@ export function PercentageInput({
|
||||
|
||||
if (readOnly && readOnlyMode === "textlike-input") {
|
||||
return (
|
||||
<input
|
||||
<Input
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
readOnly
|
||||
@ -219,13 +220,13 @@ export function PercentageInput({
|
||||
|
||||
|
||||
return (
|
||||
<input
|
||||
<Input
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
inputMode="decimal"
|
||||
pattern="[0-9]*[.,]?[0-9]*%?"
|
||||
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",
|
||||
"focus:bg-background",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[2px]",
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// Comentarios en español. TS estricto.
|
||||
|
||||
import { useQuantity } from '@erp/core/hooks';
|
||||
import { Input } from '@repo/shadcn-ui/components';
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import * as React from "react";
|
||||
import { findFocusableInCell, focusAndSelect } from './input-utils';
|
||||
@ -196,7 +197,7 @@ export function QuantityInput({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<input
|
||||
<Input
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
readOnly
|
||||
@ -217,13 +218,13 @@ export function QuantityInput({
|
||||
|
||||
// ── Editable / readOnly normal ──────────────────────────────────────────
|
||||
return (
|
||||
<input
|
||||
<Input
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
inputMode="decimal"
|
||||
pattern="[0-9]*[.,]?[0-9]*"
|
||||
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",
|
||||
"focus:bg-background",
|
||||
"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 { useState } from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useDetailColumns } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { formatCurrency } from "../../pages/create/utils";
|
||||
import { CustomerInvoiceTaxesMultiSelect } from "../customer-invoice-taxes-multi-select";
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { TaxCatalogProvider } from '@erp/core';
|
||||
import { TFunction } from 'i18next';
|
||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react";
|
||||
import { useTranslation } from "../i18n";
|
||||
import { MODULE_NAME } from '../manifest';
|
||||
|
||||
export type InvoiceContextValue = {
|
||||
company_id: string;
|
||||
@ -12,6 +15,8 @@ export type InvoiceContextValue = {
|
||||
readOnly: boolean;
|
||||
taxCatalog: TaxCatalogProvider;
|
||||
|
||||
t: TFunction<typeof MODULE_NAME>;
|
||||
|
||||
changeLanguage: (lang: string) => void;
|
||||
changeCurrency: (currency: string) => void;
|
||||
changeIsProforma: (value: boolean) => void;
|
||||
@ -36,6 +41,8 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
|
||||
currency_code: initialCurrency = "EUR", readOnly: initialReadOnly = false,
|
||||
is_proforma: initialProforma = true, children }: PropsWithChildren<InvoiceProviderParams>) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Estado interno local para campos dinámicos
|
||||
const [language_code, setLanguage] = useState(initialLang);
|
||||
const [currency_code, setCurrency] = useState(initialCurrency);
|
||||
@ -53,6 +60,8 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
|
||||
const value = useMemo<InvoiceContextValue>(() => {
|
||||
|
||||
return {
|
||||
t,
|
||||
|
||||
invoice_id,
|
||||
company_id,
|
||||
status,
|
||||
@ -68,7 +77,7 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
|
||||
changeIsProforma: setIsProformaMemo,
|
||||
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>;
|
||||
};
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
export * from "./calcs";
|
||||
export * from "./use-create-customer-invoice-mutation";
|
||||
export * from "./use-customer-invoices-query";
|
||||
export * from "./use-detail-columns";
|
||||
export * from "./use-invoice-query";
|
||||
export * from "./use-items-table-navigation";
|
||||
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 { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { i18n } from "i18next";
|
||||
import { useTranslation as useI18NextTranslation } from "react-i18next";
|
||||
import { KeyPrefix, Namespace, i18n } from "i18next";
|
||||
import { UseTranslationResponse, useTranslation as useI18NextTranslation } from "react-i18next";
|
||||
import enResources from "../common/locales/en.json";
|
||||
import esResources from "../common/locales/es.json";
|
||||
import { MODULE_NAME } from "./manifest";
|
||||
@ -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();
|
||||
addMissingBundles(i18n);
|
||||
|
||||
return useI18NextTranslation(MODULE_NAME);
|
||||
return useI18NextTranslation(MODULE_NAME, { keyPrefix });
|
||||
};
|
||||
|
||||
@ -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 "./datatable-row-actions.tsx";
|
||||
export * from "./datatable-row-drag-handle-cell.tsx";
|
||||
export * from "./data-table-column-header.tsx";
|
||||
export * from "./data-table.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 "./scroll-to-top.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 {
|
||||
Button,
|
||||
Dialog,
|
||||
|
||||
@ -7,6 +7,13 @@
|
||||
"read_only": "Read only"
|
||||
},
|
||||
"components": {
|
||||
"datatable": {
|
||||
"asc": "Asc",
|
||||
"desc": "Desc",
|
||||
"hide": "Hide",
|
||||
"empty": "No results found",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"loading_indicator": {
|
||||
"title": "Loading...",
|
||||
"subtitle": "This may take a few seconds. Please do not close this page."
|
||||
|
||||
@ -7,6 +7,13 @@
|
||||
"search": "Buscar"
|
||||
},
|
||||
"components": {
|
||||
"datatable": {
|
||||
"asc": "Asc",
|
||||
"desc": "Desc",
|
||||
"hide": "Ocultar",
|
||||
"empty": "No hay resultados",
|
||||
"actions": "Acciones"
|
||||
},
|
||||
"loading_indicator": {
|
||||
"title": "Cargando...",
|
||||
"subtitle": "Esto puede tardar unos segundos. Por favor, no cierre esta página."
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
|
||||
"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"]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user