Facturas de cliente
This commit is contained in:
parent
d539e5b5f1
commit
0420286261
@ -9,11 +9,12 @@ interface CustomerInvoiceTaxesMultiSelect {
|
||||
value?: string[];
|
||||
onChange: (selectedValues: string[]) => void;
|
||||
className?: string;
|
||||
inputId?: string;
|
||||
[key: string]: any; // Allow other props to be passed
|
||||
}
|
||||
|
||||
export const CustomerInvoiceTaxesMultiSelect = (props: CustomerInvoiceTaxesMultiSelect) => {
|
||||
const { value, onChange, className, ...otherProps } = props;
|
||||
const { value, onChange, className, inputId, ...otherProps } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
||||
@ -38,6 +39,7 @@ export const CustomerInvoiceTaxesMultiSelect = (props: CustomerInvoiceTaxesMulti
|
||||
return (
|
||||
<div className={cn("w-full", "max-w-md")}>
|
||||
<MultiSelect
|
||||
id={inputId}
|
||||
options={catalogLookup}
|
||||
onValueChange={onChange}
|
||||
defaultValue={value}
|
||||
|
||||
@ -24,7 +24,7 @@ export const InvoiceEditForm = ({
|
||||
const form = useFormContext<InvoiceFormData>();
|
||||
|
||||
return (
|
||||
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
||||
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)} >
|
||||
<section className={cn("space-y-6", className)}>
|
||||
<div className="w-full border p-6 bg-background">
|
||||
<InvoiceBasicInfoFields className="flex flex-col" />
|
||||
@ -32,7 +32,7 @@ export const InvoiceEditForm = ({
|
||||
</div>
|
||||
|
||||
<div className='w-full gap-6'>
|
||||
<InvoiceItems className="border p-6 bg-background -p-6" />
|
||||
<InvoiceItems />
|
||||
</div>
|
||||
<div className="w-full border p-6 bg-background">
|
||||
<InvoiceTotals />
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
// columna de depuración: muestra el row.id interno de TanStack
|
||||
export const debugIdCol: ColumnDef<any> = ({
|
||||
id: "__debug_row_id",
|
||||
header: () => <span className="font-medium">row.id</span>,
|
||||
cell: ({ row }) => (
|
||||
<code
|
||||
// Texto monoespaciado, truncado si es largo
|
||||
className="font-mono text-xs text-muted-foreground inline-block max-w-[14rem] truncate tabular-nums"
|
||||
aria-label={`Row id ${row.id}`}
|
||||
title={row.id}
|
||||
>
|
||||
{row.id}<br />
|
||||
</code>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false, // ponlo en true si quieres que sea ocultable en ViewOptions
|
||||
size: 160,
|
||||
minSize: 120,
|
||||
maxSize: 260,
|
||||
});
|
||||
@ -1,8 +1,16 @@
|
||||
import { Button, Input, Label, Textarea } from "@repo/shadcn-ui/components";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { InvoiceFormData } from '../../../schemas';
|
||||
import { InvoiceFormData, InvoiceItemFormData } from '../../../schemas';
|
||||
|
||||
export function ItemRowEditor({ index, close }: { index: number; close: () => void }) {
|
||||
export function ItemRowEditor({
|
||||
row,
|
||||
index,
|
||||
onClose,
|
||||
}: {
|
||||
row: InvoiceItemFormData
|
||||
index: number
|
||||
onClose: () => void
|
||||
}) {
|
||||
// Editor simple reutilizando el mismo RHF
|
||||
const { register } = useFormContext<InvoiceFormData>();
|
||||
return (
|
||||
@ -27,7 +35,7 @@ export function ItemRowEditor({ index, close }: { index: number; close: () => vo
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={close}>Close</Button>
|
||||
<Button onClick={onClose}>OK</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { Row, Table } from "@tanstack/react-table";
|
||||
|
||||
|
||||
import { DataTableRowOps } from '@repo/rdx-ui/components';
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
@ -19,64 +20,95 @@ interface DataTableRowActionsProps<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 ops = (table.options.meta as any)?.rowOps as DataTableRowOps<TData>;
|
||||
const openEditor = (table.options.meta as any)?.openEditor as (i: number, table: Table<TData>) => void;
|
||||
const lastRow = table.getRowModel().rows.length - 1;
|
||||
const rowIndex = row.index;
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<div className="items-center gap-1 inline-flex">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label="Edit row" onClick={() => openEditor?.(rowIndex)}>
|
||||
<PencilIcon className="size-4" />
|
||||
</Button>
|
||||
{openEditor && (
|
||||
<Button
|
||||
type='button'
|
||||
className='cursor-pointer'
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
aria-label='Edit row'
|
||||
onClick={() => openEditor?.(rowIndex, table)}
|
||||
>
|
||||
<PencilIcon className='size-4 text-muted-foreground' />
|
||||
</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>
|
||||
{ops?.duplicate && (
|
||||
<Button
|
||||
type='button'
|
||||
className='cursor-pointer'
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
aria-label='Duplicate row'
|
||||
onClick={() => ops?.duplicate?.(rowIndex, table)}
|
||||
>
|
||||
<CopyIcon className='size-4 text-muted-foreground' />
|
||||
</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>
|
||||
{ops?.move && (
|
||||
<Button
|
||||
type='button'
|
||||
className='cursor-pointer'
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
aria-label='Move up'
|
||||
disabled={ops?.canMoveUp ? !ops.canMoveUp(rowIndex, table) : rowIndex === 0}
|
||||
onClick={() => ops?.move?.(rowIndex, rowIndex - 1, table)}
|
||||
>
|
||||
<ArrowUpIcon className='size-4 text-muted-foreground hover:cursor-pointer' />
|
||||
</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)}
|
||||
{ops?.move && <Button
|
||||
type='button'
|
||||
className='cursor-pointer'
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
aria-label='Move down'
|
||||
disabled={ops?.canMoveDown ? !ops.canMoveDown(rowIndex, lastRow, table) : rowIndex === lastRow}
|
||||
onClick={() => ops?.move?.(rowIndex, rowIndex + 1, table)}
|
||||
>
|
||||
<ArrowDownIcon className="size-4" />
|
||||
</Button>
|
||||
<ArrowDownIcon className='size-4 text-muted-foreground hover:cursor-pointer' />
|
||||
</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>
|
||||
{ops?.remove && (
|
||||
<Button
|
||||
className='cursor-pointer'
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
aria-label='Delete row'
|
||||
onClick={() => ops?.remove?.(rowIndex, table)}
|
||||
>
|
||||
<Trash2Icon className='size-4 text-muted-foreground hover:cursor-pointer' />
|
||||
</Button>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { DataTable } from '@repo/rdx-ui/components';
|
||||
import { DataTable, useWithRowSelection } from '@repo/rdx-ui/components';
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useInvoiceContext } from '../../../context';
|
||||
import { useInvoiceAutoRecalc } from '../../../hooks';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { InvoiceFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||
import { debugIdCol } from './debug-id-col';
|
||||
import { ItemRowEditor } from './item-row-editor';
|
||||
import { useItemsColumns } from './use-items-columns';
|
||||
|
||||
@ -18,31 +21,70 @@ export const ItemsEditor = () => {
|
||||
|
||||
useInvoiceAutoRecalc(form, context);
|
||||
|
||||
const { fields, append, remove, move, insert } = useFieldArray({
|
||||
const { fields, append, remove, move, insert, update } = useFieldArray({
|
||||
control,
|
||||
name: "items",
|
||||
});
|
||||
|
||||
const columns = useItemsColumns();
|
||||
console.log(fields);
|
||||
|
||||
const baseColumns = useWithRowSelection(useItemsColumns(), true);
|
||||
const columns = useMemo(
|
||||
() => [...baseColumns, debugIdCol],
|
||||
[baseColumns]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-0">
|
||||
<DataTable columns={columns} data={fields}
|
||||
pageSize={999}
|
||||
getRowId={(r) => (r as any).id}
|
||||
<DataTable columns={columns as any} data={fields}
|
||||
getRowId={row => String(row?.index)}
|
||||
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,
|
||||
tableOps: {
|
||||
onAdd: () => append({ ...createEmptyItem() }),
|
||||
appendItem: (item: any) => append(item),
|
||||
},
|
||||
rowOps: {
|
||||
remove: (i: number) => remove(i),
|
||||
move: (from: number, to: number) => move(from, to),
|
||||
insertItem: (index: number, item: any) => insert(index, item),
|
||||
duplicateItems: (indexes: number[], table: Table<InvoiceFormData>) => {
|
||||
const items = getValues("items") || [];
|
||||
// duplicate in descending order to keep indexes stable
|
||||
[...indexes].sort((a, b) => b - a).forEach(i => {
|
||||
const curr = items[i] as any;
|
||||
if (curr) {
|
||||
const { id, ...rest } = curr;
|
||||
append({ ...rest });
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteItems: (indexes: number[]) => {
|
||||
// remove in descending order to avoid shifting issues
|
||||
[...indexes].sort((a, b) => b - a).forEach(i => remove(i));
|
||||
},
|
||||
updateItem: (index: number, item: any) => update(index, item),
|
||||
},
|
||||
bulkOps: {
|
||||
duplicateSelected: (indexes, table) => {
|
||||
const originalData = indexes.map((i) => {
|
||||
const { id, ...original } = table.getRowModel().rows[i].original;
|
||||
return original;
|
||||
});
|
||||
|
||||
insert(indexes[indexes.length - 1] + 1, originalData, { shouldFocus: true });
|
||||
table.resetRowSelection();
|
||||
},
|
||||
removeSelected: (indexes) => indexes.sort((a, b) => b - a).forEach(remove),
|
||||
moveSelectedUp: (indexes) => indexes.forEach((i) => move(i, i - 1)),
|
||||
moveSelectedDown: (indexes) => [...indexes].reverse().forEach((i) => move(i, i + 1)),
|
||||
}
|
||||
}}
|
||||
renderRowEditor={(index, close) => <ItemRowEditor index={index} close={close} />} />
|
||||
enableRowSelection
|
||||
enablePagination={false}
|
||||
pageSize={999}
|
||||
readOnly={false}
|
||||
EditorComponent={ItemRowEditor}
|
||||
/>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ export function QuantityInputField<TFormValues extends FieldValues>({
|
||||
name={name}
|
||||
render={({ field }) => {
|
||||
const { value, onChange } = field;
|
||||
console.log(value);
|
||||
|
||||
return <FormItem>
|
||||
{label ? (
|
||||
<FormLabel htmlFor={inputId}>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DataTableColumnHeader } from '@repo/rdx-ui/components';
|
||||
import { Checkbox, Textarea } from "@repo/shadcn-ui/components";
|
||||
import { InputGroup, InputGroupTextarea } 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";
|
||||
@ -30,29 +30,6 @@ export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
||||
|
||||
// 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 }) => (
|
||||
@ -72,26 +49,39 @@ export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
||||
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
|
||||
/>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea {...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 />
|
||||
{/*<InputGroupAddon align="block-end">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
disabled
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>*/}
|
||||
</InputGroup>
|
||||
|
||||
)}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -47,22 +47,22 @@ import { CustomerInvoiceItemsSortableTableRow } from "./customer-invoice-items-s
|
||||
|
||||
export type RowIdData = { [x: string]: any };
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface TableMeta<TData extends RowData> {
|
||||
insertItem: (rowIndex: number, data?: unknown) => void;
|
||||
appendItem: (data?: unknown) => void;
|
||||
pickCatalogArticle?: () => void;
|
||||
pickBlock?: () => void;
|
||||
duplicateItems: (rowIndex?: number) => void;
|
||||
deleteItems: (rowIndex?: number | number[]) => void;
|
||||
updateItem: (
|
||||
rowIndex: number,
|
||||
rowData: TData & RowIdData,
|
||||
fieldName: string,
|
||||
value: unknown
|
||||
) => void;
|
||||
}
|
||||
|
||||
/*interface TableMeta<TData extends RowData> {
|
||||
insertItem: (rowIndex: number, data?: unknown) => void;
|
||||
appendItem: (data?: unknown) => void;
|
||||
pickCatalogArticle?: () => void;
|
||||
pickBlock?: () => void;
|
||||
duplicateItems: (rowIndex?: number) => void;
|
||||
deleteItems: (rowIndex?: number | number[]) => void;
|
||||
updateItem: (
|
||||
rowIndex: number,
|
||||
rowData: TData & RowIdData,
|
||||
fieldName: string,
|
||||
value: unknown
|
||||
) => void;
|
||||
}
|
||||
}*/
|
||||
|
||||
export interface CustomerInvoiceItemsSortableProps {
|
||||
id: UniqueIdentifier;
|
||||
|
||||
@ -50,7 +50,8 @@ export const InvoiceUpdateComp = ({
|
||||
return invoiceData
|
||||
? invoiceDtoToFormAdapter.fromDto(invoiceData, context)
|
||||
: defaultCustomerInvoiceFormData
|
||||
}, [invoiceData, context, defaultCustomerInvoiceFormData])
|
||||
}, [invoiceData, context]);
|
||||
|
||||
|
||||
const form = useHookForm<InvoiceFormData>({
|
||||
resolverSchema: InvoiceFormSchema,
|
||||
@ -58,7 +59,6 @@ export const InvoiceUpdateComp = ({
|
||||
disabled: !invoiceData || isUpdating
|
||||
});
|
||||
|
||||
|
||||
const handleSubmit = (formData: InvoiceFormData) => {
|
||||
mutate(
|
||||
{ id: invoice_id, data: formData },
|
||||
@ -81,8 +81,6 @@ export const InvoiceUpdateComp = ({
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
|
||||
console.log("InvoiceUpdateComp")
|
||||
|
||||
return (
|
||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
||||
<AppHeader>
|
||||
|
||||
@ -56,172 +56,3 @@ export const InvoiceUpdatePage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
|
||||
const invoiceId = useUrlParamId();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), [])
|
||||
|
||||
|
||||
// 1) Estado de carga de la factura (query)
|
||||
const {
|
||||
data: invoiceData,
|
||||
isLoading: isLoadingInvoice,
|
||||
isError: isLoadError,
|
||||
error: loadError,
|
||||
} = useInvoiceQuery(invoiceId, { enabled: !!invoiceId });
|
||||
|
||||
// 2) Estado de actualización (mutación)
|
||||
const {
|
||||
mutate,
|
||||
isPending: isUpdating,
|
||||
isError: isUpdateError,
|
||||
error: updateError,
|
||||
} = useUpdateCustomerInvoice();
|
||||
|
||||
|
||||
const context = useInvoiceContext();
|
||||
// 3) Form hook
|
||||
const form = useHookForm<InvoiceFormData>({
|
||||
resolverSchema: InvoiceFormSchema,
|
||||
defaultValues: defaultCustomerInvoiceFormData,
|
||||
values: invoiceData ? invoiceDtoToFormAdapter.fromDto(invoiceData, taxCatalog) : undefined,
|
||||
disabled: isUpdating,
|
||||
});
|
||||
|
||||
// 4) Activa recálculo automático de los totales de la factura cuando hay algún cambio en importes
|
||||
useInvoiceAutoRecalc(form, {
|
||||
taxCatalog,
|
||||
currency_code: invoiceData?.currency_code || 'EUR'
|
||||
});
|
||||
|
||||
const handleSubmit = (formData: InvoiceFormData) => {
|
||||
const { dirtyFields } = form.formState;
|
||||
|
||||
if (!formHasAnyDirty(dirtyFields)) {
|
||||
showWarningToast("No hay cambios para guardar");
|
||||
return;
|
||||
}
|
||||
|
||||
const patchData = pickFormDirtyValues(formData, dirtyFields);
|
||||
console.log(patchData);
|
||||
|
||||
mutate(
|
||||
{ id: invoiceId!, data: patchData },
|
||||
{
|
||||
onSuccess(data) {
|
||||
showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg"));
|
||||
|
||||
// 🔹 limpiar el form e isDirty pasa a false
|
||||
form.reset(data as unknown as InvoiceFormData);
|
||||
},
|
||||
onError(error) {
|
||||
showErrorToast(t("pages.update.errorTitle"), error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (isLoadingInvoice) {
|
||||
return <CustomerInvoiceEditorSkeleton />;
|
||||
}
|
||||
|
||||
if (isLoadError) {
|
||||
return (
|
||||
<>
|
||||
<AppBreadcrumb />
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
title={t("pages.update.loadErrorTitle", "No se pudo cargar la factura")}
|
||||
message={
|
||||
(loadError as Error)?.message ??
|
||||
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||
}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-end'>
|
||||
<BackHistoryButton />
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!invoiceData)
|
||||
return (
|
||||
<>
|
||||
<AppBreadcrumb />
|
||||
<AppContent>
|
||||
<NotFoundCard
|
||||
title={t("pages.update.notFoundTitle", "Factura de cliente no encontrada")}
|
||||
message={t("pages.update.notFoundMsg", "Revisa el identificador o vuelve al listado.")}
|
||||
/>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<InvoiceProvider
|
||||
taxCatalog={taxCatalog}
|
||||
company_id={invoiceData.company_id}
|
||||
status={invoiceData.status}
|
||||
language_code={invoiceData.language_code}
|
||||
currency_code={invoiceData.currency_code}
|
||||
>
|
||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
||||
<AppHeader>
|
||||
<AppBreadcrumb />
|
||||
<PageHeader
|
||||
status={invoiceData.status}
|
||||
title={
|
||||
<>
|
||||
{t("pages.edit.title")} {invoiceData.invoice_number}
|
||||
</>
|
||||
}
|
||||
description={t("pages.edit.description")}
|
||||
icon={<FilePenIcon className='size-12 text-primary stroke-1' aria-hidden />}
|
||||
rightSlot={
|
||||
<FormCommitButtonGroup
|
||||
isLoading={isUpdating}
|
||||
disabled={isUpdating}
|
||||
cancel={{ to: "/customer-invoices/list", disabled: isUpdating }}
|
||||
submit={{ formId: "customer-invoice-update-form", disabled: isUpdating }}
|
||||
onBack={handleBack}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</AppHeader>
|
||||
|
||||
<AppContent>
|
||||
|
||||
{
|
||||
isUpdateError && (
|
||||
<ErrorAlert
|
||||
title={t("pages.update.errorTitle", "No se pudo guardar los cambios")}
|
||||
message={
|
||||
(updateError as Error)?.message ??
|
||||
t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CustomerInvoiceEditForm
|
||||
formId={"customer-invoice-update-form"} // para que el botón del header pueda hacer submit
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
className='max-w-full'
|
||||
/>
|
||||
</FormProvider>
|
||||
</AppContent >
|
||||
</UnsavedChangesProvider >
|
||||
</InvoiceProvider >
|
||||
);
|
||||
};
|
||||
*/
|
||||
@ -34,6 +34,7 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="data-[state=open]:bg-accent -ml-3 h-8 text-xs text-muted-foreground text-nowrap cursor-pointer"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Column } from "@tanstack/react-table"
|
||||
import { Check, PlusCircle } from "lucide-react"
|
||||
import { Check, PlusCircleIcon } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
@ -36,8 +36,8 @@ export function DataTableFacetedFilter<TData, TValue>({
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
||||
<PlusCircle />
|
||||
<Button type="button" variant="outline" size="sm" className="h-8 border-dashed">
|
||||
<PlusCircleIcon />
|
||||
{title}
|
||||
{selectedValues?.size > 0 && (
|
||||
<>
|
||||
|
||||
@ -54,6 +54,7 @@ export function DataTablePagination<TData>({
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hidden size-8 lg:flex"
|
||||
@ -64,6 +65,7 @@ export function DataTablePagination<TData>({
|
||||
<ChevronsLeft />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
@ -74,6 +76,7 @@ export function DataTablePagination<TData>({
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
@ -84,6 +87,7 @@ export function DataTablePagination<TData>({
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hidden size-8 lg:flex"
|
||||
|
||||
@ -1,27 +1,169 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from '@repo/shadcn-ui/components'
|
||||
import { Button, Separator, Tooltip, TooltipContent, TooltipTrigger } from '@repo/shadcn-ui/components'
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils'
|
||||
import { Table } from "@tanstack/react-table"
|
||||
import { ArrowDownIcon, ArrowUpIcon, CopyPlusIcon, PlusIcon, ScanIcon, TrashIcon } from 'lucide-react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from "../../locales/i18n.ts"
|
||||
import { DataTableViewOptions } from './data-table-view-options.tsx'
|
||||
|
||||
import { DataTableMeta } from './data-table.tsx'
|
||||
|
||||
interface DataTableToolbarProps<TData> {
|
||||
table: Table<TData>
|
||||
showViewOptions?: boolean
|
||||
}
|
||||
|
||||
export function DataTableToolbar<TData>({
|
||||
table,
|
||||
showViewOptions = true,
|
||||
}: DataTableToolbarProps<TData>) {
|
||||
const isFiltered = table.getState().columnFilters.length > 0
|
||||
|
||||
const { t } = useTranslation()
|
||||
const meta = table.options.meta as DataTableMeta<TData>
|
||||
| undefined
|
||||
|
||||
const rowSelection = table.getSelectedRowModel().rows;
|
||||
const selectedCount = rowSelection.length;
|
||||
const hasSelection = selectedCount > 0;
|
||||
|
||||
const selectedRowIndexes = useMemo(() => rowSelection.map((row) => row.index), [rowSelection]);
|
||||
|
||||
const handleAdd = useCallback(() => meta?.tableOps?.onAdd?.(table), [meta])
|
||||
const handleDuplicateSelected = useCallback(
|
||||
() => meta?.bulkOps?.duplicateSelected?.(selectedRowIndexes, table),
|
||||
[meta, selectedRowIndexes]
|
||||
)
|
||||
const handleMoveSelectedUp = useCallback(
|
||||
() => meta?.bulkOps?.moveSelectedUp?.(selectedRowIndexes, table),
|
||||
[meta, selectedRowIndexes]
|
||||
)
|
||||
const handleMoveSelectedDown = useCallback(
|
||||
() => meta?.bulkOps?.moveSelectedDown?.(selectedRowIndexes, table),
|
||||
[meta, selectedRowIndexes]
|
||||
)
|
||||
const handleRemoveSelected = useCallback(
|
||||
() => meta?.bulkOps?.removeSelected?.(selectedRowIndexes, table),
|
||||
[meta, selectedRowIndexes]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 py-2",
|
||||
"border-b border-muted px-1 sm:px-2"
|
||||
)}
|
||||
>
|
||||
{/* IZQUIERDA: acciones globales y sobre selección */}
|
||||
<div className="flex flex-1 items-center gap-2 flex-wrap">
|
||||
{meta?.tableOps?.onAdd && (
|
||||
<Button
|
||||
type='button'
|
||||
size="sm"
|
||||
onClick={handleAdd}
|
||||
aria-label={t("components.datatable.actions.add")}
|
||||
>
|
||||
<PlusIcon className="size-4 mr-1" aria-hidden="true" />
|
||||
<span>{t("components.datatable.actions.add")}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{hasSelection && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-5 mx-1" />
|
||||
|
||||
{meta?.bulkOps?.duplicateSelected && (
|
||||
<Button
|
||||
type='button'
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleDuplicateSelected}
|
||||
aria-label={t("components.datatable.actions.duplicate")}
|
||||
>
|
||||
<CopyPlusIcon className="size-4 mr-1" aria-hidden="true" />
|
||||
<span>{t("components.datatable.actions.duplicate")}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{meta?.bulkOps?.moveSelectedUp && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleMoveSelectedUp}
|
||||
aria-label={t("components.datatable.actions.move_up")}
|
||||
>
|
||||
<ArrowUpIcon className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("components.datatable.actions.move_up")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
)}
|
||||
|
||||
{meta?.bulkOps?.moveSelectedDown && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleMoveSelectedDown}
|
||||
aria-label={t("components.datatable.actions.move_down")}
|
||||
>
|
||||
<ArrowDownIcon className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("components.datatable.actions.move_down")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
)}
|
||||
|
||||
{meta?.bulkOps?.removeSelected && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-5 mx-1 w-1 bg-red-500" />
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={handleRemoveSelected}
|
||||
aria-label={t("components.datatable.actions.remove")}
|
||||
>
|
||||
<TrashIcon className="size-4 mr-1" aria-hidden="true" />
|
||||
<span>{t("components.datatable.actions.remove")}</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator orientation="vertical" className="h-6 mx-1 bg-muted/50" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!table.getSelectedRowModel().rows.length}
|
||||
onClick={() => table.resetRowSelection()}
|
||||
>
|
||||
<ScanIcon className="size-4 mr-1" aria-hidden="true" />
|
||||
<span>Quitar selección</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Quita la selección</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* DERECHA: opciones de vista / filtros */}
|
||||
<div className="flex items-center gap-2">
|
||||
<DataTableViewOptions table={table} />
|
||||
<Button size="sm">Add Task</Button>
|
||||
{showViewOptions && <DataTableViewOptions table={table} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -21,6 +21,7 @@ export function DataTableViewOptions<TData>({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto hidden h-8 lg:flex"
|
||||
|
||||
@ -4,7 +4,10 @@ import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
ColumnSizingState,
|
||||
Row,
|
||||
SortingState,
|
||||
Table,
|
||||
TableMeta,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
@ -18,68 +21,91 @@ import {
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Table, TableBody,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
TableBody,
|
||||
TableCell,
|
||||
Table as TableComp,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
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"
|
||||
|
||||
export type DataTableOps<TData> = {
|
||||
onAdd?: (table: Table<TData>) => void;
|
||||
}
|
||||
|
||||
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;
|
||||
export type DataTableRowOps<TData> = {
|
||||
duplicate?(index: number, table: Table<TData>): void;
|
||||
remove?(index: number, table: Table<TData>): void;
|
||||
move?(from: number, to: number, table: Table<TData>): void;
|
||||
canMoveUp?(index: number, table: Table<TData>): boolean;
|
||||
canMoveDown?(index: number, lastIndex: number, table: Table<TData>): boolean;
|
||||
};
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
export type DataTableBulkRowOps<TData> = {
|
||||
duplicateSelected?: (indexes: number[], table: Table<TData>) => void;
|
||||
removeSelected?: (indexes: number[], table: Table<TData>) => void;
|
||||
moveSelectedUp?: (indexes: number[], table: Table<TData>) => void;
|
||||
moveSelectedDown?: (indexes: number[], table: Table<TData>) => void;
|
||||
};
|
||||
|
||||
export type DataTableMeta<TData> = TableMeta<TData> & {
|
||||
tableOps?: DataTableOps<TData>
|
||||
rowOps?: DataTableRowOps<TData>
|
||||
bulkOps?: DataTableBulkRowOps<TData>
|
||||
}
|
||||
|
||||
export interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[],
|
||||
meta?: Record<string, any>,
|
||||
data: TData[]
|
||||
meta?: DataTableMeta<TData>
|
||||
|
||||
getRowId?: (row: TData, index: number) => string;
|
||||
pageSize?: number;
|
||||
enableRowSelection?: boolean;
|
||||
// Configuración
|
||||
readOnly?: boolean
|
||||
enablePagination?: boolean
|
||||
pageSize?: number
|
||||
enableRowSelection?: boolean
|
||||
EditorComponent?: React.ComponentType<{ row: TData; index: number; onClose: () => void }>
|
||||
|
||||
renderRowEditor?: (index: number, close: () => void) => React.ReactNode; // editor modal opcional. Se muestra dentro de un Dialog.
|
||||
getRowId?: (row: Row<TData>, index: number) => string;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
meta,
|
||||
getRowId,
|
||||
readOnly = false,
|
||||
enablePagination = true,
|
||||
pageSize = 25,
|
||||
enableRowSelection = true,
|
||||
renderRowEditor,
|
||||
enableRowSelection = false,
|
||||
EditorComponent,
|
||||
}: 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 [rowSelection, setRowSelection] = React.useState({})
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
||||
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({})
|
||||
const [editIndex, setEditIndex] = React.useState<number | null>(null)
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
columnResizeMode: "onChange",
|
||||
onColumnSizingChange: setColSizes,
|
||||
getRowId: getRowId ?? ((row: any, idx) => (row?.id ? String(row.id) : String(idx))),
|
||||
getRowId: (row: any, i) => row.id ?? String(i),
|
||||
meta,
|
||||
state: {
|
||||
columnSizing: colSizes,
|
||||
sorting,
|
||||
@ -87,15 +113,15 @@ export function DataTable<TData, TValue>({
|
||||
rowSelection,
|
||||
columnFilters,
|
||||
},
|
||||
initialState: { pagination: { pageSize } },
|
||||
meta: { ...meta, openEditor },
|
||||
|
||||
initialState: {
|
||||
pagination: { pageSize },
|
||||
},
|
||||
enableRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
@ -103,12 +129,14 @@ export function DataTable<TData, TValue>({
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
})
|
||||
|
||||
const handleCloseEditor = React.useCallback(() => setEditIndex(null), [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<DataTableToolbar table={table} />
|
||||
<div className='flex flex-col gap-0'>
|
||||
<DataTableToolbar table={table} showViewOptions={false} />
|
||||
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table className="w-full text-sm">
|
||||
<TableComp 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}>
|
||||
@ -136,8 +164,8 @@ export function DataTable<TData, TValue>({
|
||||
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
table.getRowModel().rows.map((row, i) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"} className='group'>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const w = cell.column.getSize();
|
||||
const minW = cell.column.columnDef.minSize;
|
||||
@ -160,24 +188,47 @@ export function DataTable<TData, TValue>({
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
{t("components.datatabla.empty")}
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className='h-24 text-center text-muted-foreground'
|
||||
>
|
||||
{t("components.datatable.empty")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TableFooter>
|
||||
|
||||
</TableFooter>
|
||||
</TableComp>
|
||||
</div>
|
||||
|
||||
<DataTablePagination table={table} />
|
||||
{enablePagination && <DataTablePagination table={table} />}
|
||||
|
||||
{!!renderRowEditor && editIndex !== null && (
|
||||
<Dialog open onOpenChange={(open) => (!open ? closeEditor() : null)}>
|
||||
<DialogContent className="max-w-xl">
|
||||
{renderRowEditor(editIndex, closeEditor)}
|
||||
{EditorComponent && editIndex !== null && (
|
||||
<Dialog open onOpenChange={handleCloseEditor}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("components.datatable.editor.title")}</DialogTitle>
|
||||
<DialogDescription>{t("components.datatable.editor.subtitle")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4">
|
||||
<EditorComponent
|
||||
row={data[editIndex]}
|
||||
index={editIndex}
|
||||
onClose={handleCloseEditor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={handleCloseEditor}>
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./data-table-column-header.tsx";
|
||||
export * from "./data-table.tsx";
|
||||
|
||||
export * from "./with-row-selection.tsx";
|
||||
|
||||
@ -19,7 +19,7 @@ export function UserNav() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Button type="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>
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import { Checkbox } from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import * as React from "react";
|
||||
|
||||
// Columna estable (definida una vez)
|
||||
const selectionCol: ColumnDef<any, unknown> = {
|
||||
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: 36, minSize: 36, maxSize: 36,
|
||||
};
|
||||
|
||||
// Función pura (sin hooks)
|
||||
export function withRowSelection<T>(
|
||||
base: ColumnDef<T, unknown>[],
|
||||
enabled: boolean
|
||||
): ColumnDef<T, unknown>[] {
|
||||
if (!enabled) return base; // misma referencia si está desactivado
|
||||
// Evita duplicar si ya viene incluida
|
||||
if (base.length > 0 && base[0].id === selectionCol.id) return base;
|
||||
return [selectionCol as ColumnDef<T, unknown>, ...base];
|
||||
}
|
||||
|
||||
// Custom hook ergonómico
|
||||
export function useWithRowSelection<T>(
|
||||
baseColumns: ColumnDef<T, unknown>[],
|
||||
enabled: boolean
|
||||
) {
|
||||
return React.useMemo(
|
||||
() => withRowSelection(baseColumns, enabled),
|
||||
[baseColumns, enabled]
|
||||
);
|
||||
}
|
||||
@ -12,7 +12,12 @@
|
||||
"desc": "Desc",
|
||||
"hide": "Hide",
|
||||
"empty": "No results found",
|
||||
"actions": "Actions"
|
||||
"columns": {
|
||||
"actions": "Actions"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Add new line"
|
||||
}
|
||||
},
|
||||
"loading_indicator": {
|
||||
"title": "Loading...",
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"actions": "Actions",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar",
|
||||
"close": "Cerrar",
|
||||
"actions": "Acciones",
|
||||
"invalid_date": "Fecha incorrecta o no válida",
|
||||
"required": "•",
|
||||
"modified": "modificado",
|
||||
@ -12,7 +15,16 @@
|
||||
"desc": "Desc",
|
||||
"hide": "Ocultar",
|
||||
"empty": "No hay resultados",
|
||||
"actions": "Acciones"
|
||||
"columns": {
|
||||
"actions": "Acciones"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Añadir línea",
|
||||
"duplicate": "Duplicar",
|
||||
"remove": "Eliminar",
|
||||
"move_up": "Subir",
|
||||
"move_down": "Bajar"
|
||||
}
|
||||
},
|
||||
"loading_indicator": {
|
||||
"title": "Cargando...",
|
||||
|
||||
@ -8,9 +8,6 @@
|
||||
|
||||
"plugins": [{ "name": "typescript-plugin-css-modules" }]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"../../modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx"
|
||||
],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user