This commit is contained in:
David Arranz 2024-08-25 22:06:24 +02:00
parent 910d84aa76
commit ffd35bee3b
19 changed files with 359 additions and 407 deletions

View File

@ -29,27 +29,19 @@ export const CatalogDataTable = () => {
{ {
id: "id" as const, id: "id" as const,
accessorKey: "id", accessorKey: "id",
enableResizing: false,
size: 10,
}, },
{ {
id: "article_id" as const, id: "article_id" as const,
accessorKey: "id_article", accessorKey: "id_article",
enableResizing: false,
size: 10,
}, },
{ {
id: "catalog_name" as const, id: "catalog_name" as const,
accessorKey: "catalog_name", accessorKey: "catalog_name",
enableResizing: false,
size: 10,
}, },
{ {
id: "description" as const, id: "description" as const,
accessorKey: "description", accessorKey: "description",
header: () => <>{t("catalog.list.columns.description")}</>, header: () => <>{t("catalog.list.columns.description")}</>,
enableResizing: false,
size: 100,
}, },
{ {
id: "points" as const, id: "points" as const,
@ -58,8 +50,6 @@ export const CatalogDataTable = () => {
cell: ({ renderValue }: { renderValue: () => any }) => ( cell: ({ renderValue }: { renderValue: () => any }) => (
<div className='text-right'>{renderValue()}</div> <div className='text-right'>{renderValue()}</div>
), ),
enableResizing: false,
size: 20,
}, },
{ {
id: "retail_price" as const, id: "retail_price" as const,
@ -69,8 +59,6 @@ export const CatalogDataTable = () => {
const price = MoneyValue.create(row.original.retail_price).object; const price = MoneyValue.create(row.original.retail_price).object;
return <div className='text-right'>{price.toFormat()}</div>; return <div className='text-right'>{price.toFormat()}</div>;
}, },
enableResizing: false,
size: 20,
}, },
], ],
[] []

View File

@ -1,4 +1,3 @@
import { cn } from "@/lib/utils";
import { Button, ButtonProps } from "@/ui"; import { Button, ButtonProps } from "@/ui";
import { t } from "i18next"; import { t } from "i18next";
import { PackagePlusIcon } from "lucide-react"; import { PackagePlusIcon } from "lucide-react";
@ -13,16 +12,8 @@ export const AppendCatalogArticleRowButton = ({
className, className,
...props ...props
}: AppendCatalogArticleRowButtonProps): JSX.Element => ( }: AppendCatalogArticleRowButtonProps): JSX.Element => (
<Button <Button type='button' variant='outline' {...props}>
type='button' {" "}
variant='outline'
size='icon'
className={cn(
"w-full gap-1 border-dashed text-muted-foreground border-muted-foreground/50",
className
)}
{...props}
>
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} /> <PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>} {label && <>{label}</>}
</Button> </Button>

View File

@ -1,4 +1,3 @@
import { cn } from "@/lib/utils";
import { Button, ButtonProps } from "@/ui"; import { Button, ButtonProps } from "@/ui";
import { t } from "i18next"; import { t } from "i18next";
import { PlusCircleIcon } from "lucide-react"; import { PlusCircleIcon } from "lucide-react";
@ -13,16 +12,7 @@ export const AppendEmptyRowButton = ({
className, className,
...props ...props
}: AppendEmptyRowButtonProps): JSX.Element => ( }: AppendEmptyRowButtonProps): JSX.Element => (
<Button <Button type='button' variant='outline' {...props}>
type='button'
variant='outline'
size='icon'
className={cn(
"w-full gap-1 border-dashed text-muted-foreground border-muted-foreground/50",
className
)}
{...props}
>
<PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} /> <PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>} {label && <>{label}</>}
</Button> </Button>

View File

@ -4,10 +4,11 @@ import { useCatalogList } from "@/app/catalog/hooks";
import { DataTable } from "@/components"; import { DataTable } from "@/components";
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar"; import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
import { useDataTable, useDataTableContext } from "@/lib/hooks"; import { useDataTable, useDataTableContext } from "@/lib/hooks";
import { cn } from "@/lib/utils"; import { Button } from "@/ui";
import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts"; import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts";
import { ColumnDef, Row } from "@tanstack/react-table"; import { ColumnDef, Row } from "@tanstack/react-table";
import { t } from "i18next"; import { t } from "i18next";
import { PackagePlusIcon } from "lucide-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -28,58 +29,45 @@ export const CatalogPickerDataTable = ({ onSelect }: { onSelect: (data: unknown)
{ {
id: "description" as const, id: "description" as const,
accessorKey: "description", accessorKey: "description",
enableResizing: false, header: () => <>{t("catalog.list.columns.description")}</>,
header: () => null, },
cell: ({ {
row, id: "points" as const,
renderValue, accessorKey: "points",
}: { header: () => <div className='text-right'>{t("catalog.list.columns.points")}</div>,
row: Row<IListArticles_Response_DTO>; cell: ({ renderValue }: { renderValue: () => any }) => (
renderValue: () => any; <div className='text-right'>{renderValue()}</div>
}) => { ),
},
{
id: "retail_price" as const,
accessorKey: "retail_price",
header: () => <div className='text-right'>{t("catalog.list.columns.retail_price")}</div>,
cell: ({ row }: { row: Row<IListArticles_Response_DTO> }) => {
const price = MoneyValue.create(row.original.retail_price).object; const price = MoneyValue.create(row.original.retail_price).object;
const points = row.original.points; return <div className='text-right'>{price.toFormat()}</div>;
return (
<button
key={row.id}
className={cn(
"rounded-lg border p-3 w-full transition delay-150 duration-200 active:scale-95 hover:bg-primary ",
""
)}
onClick={
(event) => {
event.preventDefault();
onSelect && onSelect(row.original);
}
/*setMail({
...mail,
selected: item.id,
})*/
}
>
<div className='flex flex-row justify-between w-full space-x-6'>
<div className='w-3/4 text-left grow line-clamp-2 text-muted-foreground hover:text-foreground'>
{renderValue()}
</div>
<div className='w-1/4 text-right'>
<dl className='flex flex-row justify-end space-x-1'>
<dt className='text-xs font-medium text-accent-foreground/75'>
{t("catalog.list.columns.points")}:
</dt>
<dd className='text-xs font-semibold'>{points}</dd>
</dl>
<dl className='flex flex-row justify-end space-x-1'>
<dt className='text-xs font-medium text-accent-foreground/75'>
{t("catalog.list.columns.retail_price")}:
</dt>
<dt className='text-xs font-semibold'>{price.toFormat()}</dt>
</dl>
</div>
</div>
</button>
);
}, },
}, },
{
id: "row-actions",
header: () => null,
cell: ({ row }: { row: Row<IListArticles_Response_DTO> }) => (
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={(event) => {
event.preventDefault();
onSelect && onSelect(row.original);
}}
>
<PackagePlusIcon className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
{t("common.add")}
</span>
</Button>
),
},
]; ];
}, []); }, []);
@ -119,12 +107,8 @@ export const CatalogPickerDataTable = ({ onSelect }: { onSelect: (data: unknown)
<DataTable <DataTable
className='bg-transparent border-0 shadow-none' className='bg-transparent border-0 shadow-none'
table={table} table={table}
headerOptions={{ visible: false }}
paginationOptions={{ visible: true, enablePageSizeSelector: false }} paginationOptions={{ visible: true, enablePageSizeSelector: false }}
contentClassName='p-0' footerClassName='px-10 pt-2 border-t'
footerClassName='p-0'
rowClassName='border-b-0'
cellClassName='px-0'
> >
<DataTableToolbar fullWidthFilter={true} table={table} /> <DataTableToolbar fullWidthFilter={true} table={table} />
</DataTable> </DataTable>

View File

@ -1,14 +1,6 @@
import { DataTableColumnHeader } from "@/components"; import { ButtonGroup, DataTableColumnHeader } from "@/components";
import { Badge } from "@/ui"; import { Badge, Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/ui";
import { import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/ui/table";
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/ui/table";
import { import {
DndContext, DndContext,
DragEndEvent, DragEndEvent,
@ -30,6 +22,7 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { import {
ColumnDef, ColumnDef,
InitialTableState,
Row, Row,
RowData, RowData,
RowSelectionState, RowSelectionState,
@ -65,6 +58,7 @@ export type QuoteItemsSortableDataTableProps = {
columns: ColumnDef<unknown, unknown>[]; columns: ColumnDef<unknown, unknown>[];
data: Record<"id", string>[]; data: Record<"id", string>[];
defaultValues: Readonly<{ [x: string]: any }> | undefined; defaultValues: Readonly<{ [x: string]: any }> | undefined;
initialState?: InitialTableState;
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields"> & Record<string, unknown>; actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields"> & Record<string, unknown>;
}; };
@ -101,13 +95,17 @@ export function QuoteItemsSortableDataTable({
columns, columns,
data, data,
defaultValues, defaultValues,
initialState,
actions, actions,
}: QuoteItemsSortableDataTableProps) { }: QuoteItemsSortableDataTableProps) {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(); const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}); const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
initialState?.columnVisibility || {}
);
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]); const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
const table = useReactTable({ const table = useReactTable({
@ -116,6 +114,8 @@ export function QuoteItemsSortableDataTable({
enableColumnResizing: false, enableColumnResizing: false,
columnResizeMode: "onChange", columnResizeMode: "onChange",
//defaultColumn, //defaultColumn,
initialState,
state: { state: {
rowSelection, rowSelection,
columnVisibility, columnVisibility,
@ -124,16 +124,21 @@ export function QuoteItemsSortableDataTable({
enableMultiRowSelection: true, enableMultiRowSelection: true,
enableSorting: false, enableSorting: false,
enableHiding: true, enableHiding: true,
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getRowId: (originalRow: unknown) => originalRow?.id, getRowId: (originalRow: unknown) => originalRow?.id,
debugTable: false,
debugHeaders: false, debugHeaders: false,
debugColumns: false, debugColumns: false,
defaultColumn: { defaultColumn: {
size: 8, //starting column size minSize: 0, //starting column size
minSize: 1, //enforced during column resizing size: Number.MAX_SAFE_INTEGER, //enforced during column resizing
maxSize: 96, //enforced during column resizing maxSize: Number.MAX_SAFE_INTEGER, //enforced during column resizing
}, },
meta: { meta: {
insertItem: (rowIndex: number, data?: unknown) => { insertItem: (rowIndex: number, data?: unknown) => {
actions.insert(rowIndex, data || defaultValues?.items[0], { shouldFocus: true }); actions.insert(rowIndex, data || defaultValues?.items[0], { shouldFocus: true });
@ -154,7 +159,7 @@ export function QuoteItemsSortableDataTable({
const data = table const data = table
.getSelectedRowModel() .getSelectedRowModel()
.rows.map((row) => ({ ...row.original, id: undefined })); .rows.map((row: Row<any>) => ({ ...row.original, id: undefined }));
if (table.getRowModel().rows.length < lastIndex + 1) { if (table.getRowModel().rows.length < lastIndex + 1) {
actions.append(data); actions.append(data);
@ -184,7 +189,7 @@ export function QuoteItemsSortableDataTable({
actions.remove(); actions.remove();
} }
}, },
updateItem: (rowIndex: number, rowData: unknown, fieldName: string, value: unknown) => { updateItem: (rowIndex: number, rowData: any, fieldName: string, value: unknown) => {
// Skip page index reset until after next rerender // Skip page index reset until after next rerender
// skipAutoResetPageIndex(); // skipAutoResetPageIndex();
actions.update(rowIndex, { ...rowData, [`${fieldName}`]: value }); actions.update(rowIndex, { ...rowData, [`${fieldName}`]: value });
@ -264,169 +269,189 @@ export function QuoteItemsSortableDataTable({
onDragCancel={handleDragCancel} onDragCancel={handleDragCancel}
collisionDetection={closestCenter} collisionDetection={closestCenter}
> >
<QuoteItemsSortableDataTableToolbar table={table} /> <Card>
<Table className='table-fixed'> <CardHeader className='sticky z-10 border-b top-16 bg-card'>
<TableHeader> <CardTitle>
{table.getHeaderGroups().map((headerGroup) => ( <QuoteItemsSortableDataTableToolbar table={table} />
<TableRow key={headerGroup.id} className='hover:bg-transparent'> </CardTitle>
{headerGroup.headers.map((header) => { </CardHeader>
return ( <CardContent>
<TableHead key={header.id} className={`px-2 py-1 w-${header.getSize()}`}> <Table className='table-fixed'>
{header.isPlaceholder ? null : ( <TableHeader className='sticky top-0 z-10 bg-background'>
<DataTableColumnHeader table={table} header={header} /> {table.getHeaderGroups().map((headerGroup) => (
)} <TableRow key={headerGroup.id} className='hover:bg-transparent'>
</TableHead> {headerGroup.headers.map((header) => {
); return (
})} <TableHead
</TableRow> key={header.id}
))} className='px-2 py-1'
</TableHeader> style={{
<TableBody> width:
<SortableContext header.getSize() === Number.MAX_SAFE_INTEGER
items={filterItems(sorteableRowIds)} ? "auto"
strategy={verticalListSortingStrategy} : header.getSize(),
> }}
{filterItems(table.getRowModel().rows).map((row) => ( >
<QuoteItemsSortableTableRow key={row.id} id={row.id}> {header.isPlaceholder ? null : (
{row.getVisibleCells().map((cell) => ( <DataTableColumnHeader table={table} header={header} />
<TableCell key={cell.id} className='px-2 py-1 align-top'> )}
{flexRender(cell.column.columnDef.cell, cell.getContext())} </TableHead>
</TableCell> );
})}
</TableRow>
))}
</TableHeader>
<TableBody>
<SortableContext
items={filterItems(sorteableRowIds)}
strategy={verticalListSortingStrategy}
>
{filterItems(table.getRowModel().rows).map((row) => (
<QuoteItemsSortableTableRow key={(row as Row<any>).id} id={(row as Row<any>).id}>
{(row as Row<any>).getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className='px-2 py-2 align-top'
style={{
width:
cell.column.getSize() === Number.MAX_SAFE_INTEGER
? "auto"
: cell.column.getSize(),
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</QuoteItemsSortableTableRow>
))} ))}
</QuoteItemsSortableTableRow> </SortableContext>
))} </TableBody>
</SortableContext> </Table>
</TableBody>
<TableFooter className='bg-default'>
<TableRow className='hover:bg-default'>
<TableCell colSpan={3} className='py-6'></TableCell>
<TableCell colSpan={5} className='py-6'>
<div className='grid grid-cols-2 gap-6'>
<AppendEmptyRowButton onClick={() => table.options.meta?.appendItem()} />
<AppendCatalogArticleRowButton
onClick={() => table.options.meta?.pickCatalogArticle()}
/>
</div>
</TableCell>
<TableCell className='py-6'></TableCell>
</TableRow>
</TableFooter>
</Table>
{createPortal( {createPortal(
<DragOverlay dropAnimation={dropAnimationConfig} className={"z-40 opacity-100"}> <DragOverlay dropAnimation={dropAnimationConfig} className={"z-40 opacity-100"}>
{activeId && ( {activeId && (
<div className='relative flex flex-wrap'> <div className='relative flex flex-wrap'>
{table.getSelectedRowModel().rows.length ? ( {table.getSelectedRowModel().rows.length ? (
<Badge <Badge
variant='destructive' variant='destructive'
className='absolute z-50 flex items-center justify-center w-2 h-2 p-3 rounded-full top left -left-2 -top-2' className='absolute z-50 flex items-center justify-center w-2 h-2 p-3 rounded-full top left -left-2 -top-2'
> >
{table.getSelectedRowModel().rows.length} {table.getSelectedRowModel().rows.length}
</Badge> </Badge>
) : null} ) : null}
<div className='absolute z-40 bg-white border rounded shadow opacity-100 top left hover:bg-white border-muted-foreground/50'> <div className='absolute z-40 bg-white border rounded shadow opacity-100 top left hover:bg-white border-muted-foreground/50'>
<Table> <Table>
<TableBody> <TableBody>
{table.getRowModel().rows.map( {table.getRowModel().rows.map(
(row) => (row) =>
row.id === activeId && ( row.id === activeId && (
<TableRow key={row.id} id={row.id}> <TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell
className='p-1 align-top' className='p-1 align-top'
key={cell.id} key={cell.id}
style={{ width: cell.column.getSize() }} style={{ width: cell.column.getSize() }}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
) )
)} )}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
{table.getSelectedRowModel().rows.length > 1 && ( {table.getSelectedRowModel().rows.length > 1 && (
<div className='absolute z-30 transform -translate-x-1 translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-1'> <div className='absolute z-30 transform -translate-x-1 translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-1'>
<Table> <Table>
<TableBody> <TableBody>
{table.getRowModel().rows.map( {table.getRowModel().rows.map(
(row) => (row) =>
row.id === activeId && ( row.id === activeId && (
<TableRow key={row.id} id={row.id}> <TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell
className='p-1 align-top' className='p-1 align-top'
key={cell.id} key={cell.id}
style={{ width: cell.column.getSize() }} style={{ width: cell.column.getSize() }}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
) )
)} )}
</TableBody> </TableBody>
</Table> </Table>
</div>
)}
{table.getSelectedRowModel().rows.length > 2 && (
<div className='absolute z-20 transform translate-x-1 -translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left -rotate-1'>
<Table>
<TableBody>
{table.getRowModel().rows.map(
(row) =>
row.id === activeId && (
<TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
className='p-1 align-top'
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
)}
</TableBody>
</Table>
</div>
)}
{table.getSelectedRowModel().rows.length > 3 && (
<div className='absolute z-10 transform translate-x-2 -translate-y-2 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-2'>
<Table>
<TableBody>
{table.getRowModel().rows.map(
(row) =>
row.id === activeId && (
<TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
className='p-1 align-top'
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
)}
</TableBody>
</Table>
</div>
)}
</div> </div>
)} )}
</DragOverlay>,
{table.getSelectedRowModel().rows.length > 2 && ( document.body
<div className='absolute z-20 transform translate-x-1 -translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left -rotate-1'>
<Table>
<TableBody>
{table.getRowModel().rows.map(
(row) =>
row.id === activeId && (
<TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
className='p-1 align-top'
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
)}
</TableBody>
</Table>
</div>
)}
{table.getSelectedRowModel().rows.length > 3 && (
<div className='absolute z-10 transform translate-x-2 -translate-y-2 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-2'>
<Table>
<TableBody>
{table.getRowModel().rows.map(
(row) =>
row.id === activeId && (
<TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
className='p-1 align-top'
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
)}
</TableBody>
</Table>
</div>
)}
</div>
)} )}
</DragOverlay>, </CardContent>
document.body <CardFooter>
)} <ButtonGroup>
<AppendEmptyRowButton onClick={() => table.options.meta?.appendItem()} />
<AppendCatalogArticleRowButton
onClick={() => table.options.meta?.pickCatalogArticle()}
/>
</ButtonGroup>
</CardFooter>
</Card>
</DndContext> </DndContext>
); );
} }

View File

@ -1,102 +1,92 @@
import { Button, Separator, Tooltip, TooltipContent, TooltipTrigger } from "@/ui"; import { Button, Separator, Tooltip, TooltipContent, TooltipTrigger } from "@/ui";
import { Table } from "@tanstack/react-table"; import { Table } from "@tanstack/react-table";
import { t } from "i18next"; import { t } from "i18next";
import { CirclePlusIcon, CopyPlusIcon, PackagePlusIcon, ScanIcon, Trash2Icon } from "lucide-react"; import { CopyPlusIcon, ScanIcon, Trash2Icon } from "lucide-react";
import { AppendCatalogArticleRowButton } from "./AppendCatalogArticleRowButton";
import { AppendEmptyRowButton } from "./AppendEmptyRowButton";
export const QuoteItemsSortableDataTableToolbar = ({ table }: { table: Table<unknown> }) => { export const QuoteItemsSortableDataTableToolbar = ({ table }: { table: Table<unknown> }) => {
const selectedRowsCount = table.getSelectedRowModel().rows.length; const selectedRowsCount = table.getSelectedRowModel().rows.length;
if (selectedRowsCount) { if (selectedRowsCount) {
return ( return (
<nav className='sticky z-10 pt-4 bg-background top-16'> <nav className='flex items-center h-12 p-1 rounded-md text-muted-foreground bg-muted '>
<div className='flex items-center h-12 p-1 rounded-md text-muted-foreground bg-primary '>
<div className='flex items-center gap-2'>
<Tooltip>
<TooltipTrigger asChild>
<Button
type='button'
variant='ghost'
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.options.meta?.duplicateItems()}
>
<CopyPlusIcon className='w-4 h-4 sm:mr-2' />
<span className='sr-only sm:not-sr-only'>
{t("common.duplicate_selected_rows")}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.duplicate_selected_rows_tooltip")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type='button'
variant='ghost'
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.options.meta?.deleteItems()}
>
<Trash2Icon className='w-4 h-4 sm:mr-2' />
<span className='sr-only sm:not-sr-only'>{t("common.remove_selected_rows")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.remove_selected_rows_tooltip")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type='button'
variant='ghost'
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.resetRowSelection()}
>
<ScanIcon className='w-4 h-4 sm:mr-2' />
<span className='sr-only sm:not-sr-only'>{t("common.reset_selected_rows")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.reset_selected_rows_tooltip")}</TooltipContent>
</Tooltip>
<Separator orientation='vertical' className='h-6 mx-1 bg-muted-foreground' />
<p className='text-sm'>{t("common.rows_selected", { count: selectedRowsCount })}</p>
</div>
</div>
</nav>
);
}
return (
<nav className='sticky z-10 pt-4 bg-background top-16'>
<div className='flex items-center h-12 p-1 rounded-md bg-accent text-muted-foreground'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
type='button' type='button'
variant='ghost' variant='ghost'
onClick={() => table.options.meta?.appendItem()} disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.options.meta?.duplicateItems()}
> >
<CirclePlusIcon className='w-4 h-4 mr-2' /> <CopyPlusIcon className='w-4 h-4 sm:mr-2' />
<span>{t("common.append_empty_row")}</span> <span className='sr-only sm:not-sr-only'>
{t("common.duplicate_selected_rows")}
</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{t("common.append_empty_row_tooltip")}</TooltipContent> <TooltipContent>{t("common.duplicate_selected_rows_tooltip")}</TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
type='button' type='button'
variant='ghost' variant='ghost'
onClick={() => table.options.meta?.pickCatalogArticle()} disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.options.meta?.deleteItems()}
> >
<PackagePlusIcon className='w-4 h-4 mr-2' /> <Trash2Icon className='w-4 h-4 sm:mr-2' />
<span>{t("common.append_article")}</span> <span className='sr-only sm:not-sr-only'>{t("common.remove_selected_rows")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{t("common.append_article_tooltip")}</TooltipContent> <TooltipContent>{t("common.remove_selected_rows_tooltip")}</TooltipContent>
</Tooltip> </Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type='button'
variant='ghost'
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.resetRowSelection()}
>
<ScanIcon className='w-4 h-4 sm:mr-2' />
<span className='sr-only sm:not-sr-only'>{t("common.reset_selected_rows")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.reset_selected_rows_tooltip")}</TooltipContent>
</Tooltip>
<Separator orientation='vertical' className='h-6 mx-1 bg-muted-foreground' />
<p className='text-sm'>{t("common.rows_selected", { count: selectedRowsCount })}</p>
</div> </div>
<div className='flex items-center gap-2 ml-auto'></div> </nav>
);
}
return (
<nav className='flex items-center h-12 p-1 rounded-md bg-accent/75 text-muted-foreground'>
<div className='flex space-x-2'>
<Tooltip>
<TooltipTrigger asChild>
<AppendEmptyRowButton
variant='ghost'
onClick={() => table.options.meta?.appendItem()}
/>
</TooltipTrigger>
<TooltipContent>{t("common.append_empty_row_tooltip")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<AppendCatalogArticleRowButton
variant='ghost'
onClick={() => table.options.meta?.pickCatalogArticle()}
/>
</TooltipTrigger>
<TooltipContent>{t("common.append_article_tooltip")}</TooltipContent>
</Tooltip>
</div> </div>
<div className='flex items-center gap-2 ml-auto'></div>
</nav> </nav>
); );
}; };

View File

@ -1,4 +1,5 @@
import { import {
ButtonGroup,
ColorBadge, ColorBadge,
DataTable, DataTable,
DataTableSkeleton, DataTableSkeleton,
@ -74,7 +75,7 @@ export const QuotesDataTable = ({
<Button <Button
size='sm' size='sm'
variant='link' variant='link'
className='h-8 gap-1 text-left text-ellipsis' className='h-8 gap-1 px-0 text-left text-ellipsis'
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
navigate(`/quotes/edit/${original.id}`, { relative: "path" }); navigate(`/quotes/edit/${original.id}`, { relative: "path" });
@ -83,7 +84,6 @@ export const QuotesDataTable = ({
<div className=''>{renderValue()}</div> <div className=''>{renderValue()}</div>
</Button> </Button>
), ),
enableResizing: false,
}, },
{ {
@ -92,7 +92,6 @@ export const QuotesDataTable = ({
header: () => <>{t("quotes.list.columns.status")}</>, header: () => <>{t("quotes.list.columns.status")}</>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
cell: ({ row: { original } }) => <ColorBadge label={original.status} />, cell: ({ row: { original } }) => <ColorBadge label={original.status} />,
enableResizing: false,
}, },
{ {
id: "date" as const, id: "date" as const,
@ -108,14 +107,12 @@ export const QuotesDataTable = ({
</div> </div>
); );
}, },
enableResizing: false,
}, },
{ {
id: "customer_reference" as const, id: "customer_reference" as const,
accessorKey: "customer_reference", accessorKey: "customer_reference",
header: () => <>{t("quotes.list.columns.customer_reference")}</>, header: () => <>{t("quotes.list.columns.customer_reference")}</>,
cell: ({ renderValue }) => <div className='text-left text-ellipsis'>{renderValue()}</div>, cell: ({ renderValue }) => <div className='text-left text-ellipsis'>{renderValue()}</div>,
enableResizing: false,
}, },
{ {
id: "customer_information" as const, id: "customer_information" as const,
@ -138,8 +135,7 @@ export const QuotesDataTable = ({
})} })}
</div> </div>
), ),
enableResizing: false, size: 600,
size: 640,
}, },
{ {
@ -152,13 +148,12 @@ export const QuotesDataTable = ({
<div className='text-right'>{price.isSuccess ? price.object.toFormat() : "-"}</div> <div className='text-right'>{price.isSuccess ? price.object.toFormat() : "-"}</div>
); );
}, },
enableResizing: false,
}, },
{ {
id: "row-actions", id: "row-actions",
header: () => null, header: () => null,
cell: ({ row }: { row: Row<IListQuotes_Response_DTO> }) => ( cell: ({ row }: { row: Row<IListQuotes_Response_DTO> }) => (
<div className='flex items-center gap-1 ml-auto'> <ButtonGroup>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@ -200,9 +195,8 @@ export const QuotesDataTable = ({
<DropdownMenuItem>{t("common.archive")}</DropdownMenuItem> <DropdownMenuItem>{t("common.archive")}</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </ButtonGroup>
), ),
enableResizing: false,
}, },
], ],
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -33,7 +33,9 @@ export const CatalogPickerDialog = ({
</DataTableProvider> </DataTableProvider>
<DialogFooter> <DialogFooter>
<Button type='submit'>{t("common.close")}</Button> <Button type='submit' onClick={() => onOpenChange(false)}>
{t("common.close")}
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -11,7 +11,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui";
import { CurrencyData, Language, Quantity } from "@shared/contexts"; import { CurrencyData, Language, Quantity } from "@shared/contexts";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { t } from "i18next"; import { t } from "i18next";
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react"; import { ChevronDownIcon, ChevronUpIcon, Trash2Icon } from "lucide-react";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useFieldArray, useFormContext } from "react-hook-form"; import { useFieldArray, useFormContext } from "react-hook-form";
import { useDetailColumns } from "../../hooks"; import { useDetailColumns } from "../../hooks";
@ -46,21 +46,20 @@ export const QuoteDetailsCardEditor = ({
header: () => ( header: () => (
<HashIcon aria-label='Orden de fila' className='items-center justify-center w-4 h-4' /> <HashIcon aria-label='Orden de fila' className='items-center justify-center w-4 h-4' />
), ),
accessorFn: (originalRow: unknown, index: number) => index + 1, accessorFn: (_: unknown, index: number) => index + 1,
size: 5, size: 5,
enableHiding: false, enableHiding: false,
enableSorting: false, enableSorting: false,
enableResizing: false, enableResizing: false,
},*/ },*/
{ {
id: "description" as const, id: "description" as const,
accessorKey: "description", accessorKey: "description",
header: t("quotes.form_fields.items.description.label"), header: t("quotes.form_fields.items.description.label"),
size: 24,
cell: ({ row: { index } }) => { cell: ({ row: { index } }) => {
return <FormTextAreaField autoSize {...register(`items.${index}.description`)} />; return <FormTextAreaField autoSize {...register(`items.${index}.description`)} />;
}, },
size: 500,
}, },
{ {
id: "quantity" as const, id: "quantity" as const,
@ -68,7 +67,6 @@ export const QuoteDetailsCardEditor = ({
header: () => ( header: () => (
<div className='text-right'>{t("quotes.form_fields.items.quantity.label")}</div> <div className='text-right'>{t("quotes.form_fields.items.quantity.label")}</div>
), ),
size: 6,
cell: ({ row: { index } }) => { cell: ({ row: { index } }) => {
return ( return (
<FormQuantityField <FormQuantityField
@ -103,7 +101,6 @@ export const QuoteDetailsCardEditor = ({
header: () => ( header: () => (
<div className='text-right'>{t("quotes.form_fields.items.subtotal_price.label")}</div> <div className='text-right'>{t("quotes.form_fields.items.subtotal_price.label")}</div>
), ),
size: 12,
cell: ({ row: { index } }) => { cell: ({ row: { index } }) => {
return ( return (
<FormCurrencyField <FormCurrencyField
@ -120,7 +117,6 @@ export const QuoteDetailsCardEditor = ({
{ {
id: "discount" as const, id: "discount" as const,
accessorKey: "discount", accessorKey: "discount",
size: 6,
header: () => ( header: () => (
<div className='text-right'>{t("quotes.form_fields.items.discount.label")}</div> <div className='text-right'>{t("quotes.form_fields.items.discount.label")}</div>
), ),
@ -140,7 +136,6 @@ export const QuoteDetailsCardEditor = ({
header: () => ( header: () => (
<div className='text-right'>{t("quotes.form_fields.items.total_price.label")}</div> <div className='text-right'>{t("quotes.form_fields.items.total_price.label")}</div>
), ),
size: 12,
cell: ({ row: { index } }) => { cell: ({ row: { index } }) => {
return ( return (
<FormCurrencyField <FormCurrencyField
@ -157,17 +152,17 @@ export const QuoteDetailsCardEditor = ({
}, },
], ],
{ {
enableDragHandleColumn: true, enableDragHandleColumn: false, // <--- Desactivado temporalmente
enableSelectionColumn: true, enableSelectionColumn: true,
enableActionsColumn: true, enableActionsColumn: true,
rowActionFn: (props) => { rowActionFn: (props) => {
const { table, row } = props; const { table, row } = props;
return [ return [
{ /*{
label: t("common.duplicate_row"), label: t("common.duplicate_row"),
icon: <CopyIcon className='w-4 h-4 mr-2' />, icon: <CopyIcon className='w-4 h-4 mr-2' />,
onClick: () => table.options.meta?.duplicateItems(row.index), onClick: () => table.options.meta?.duplicateItems(row.index),
}, },*/
{ {
label: t("common.insert_row_above"), label: t("common.insert_row_above"),
icon: <ChevronUpIcon className='w-4 h-4 mr-2' />, icon: <ChevronUpIcon className='w-4 h-4 mr-2' />,

View File

@ -36,12 +36,9 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
),*/ ),*/
header: () => null, header: () => null,
cell: (info) => <DataTableRowDragHandleCell rowId={info.row.id} />, cell: (info) => <DataTableRowDragHandleCell rowId={info.row.id} />,
size: 2,
minSize: 2,
maxSize: 2,
enableSorting: false, enableSorting: false,
enableHiding: false, enableHiding: false,
size: 40,
}); });
} }
@ -74,9 +71,7 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
), ),
enableSorting: false, enableSorting: false,
enableHiding: false, enableHiding: false,
size: 2, size: 40,
minSize: 2,
maxSize: 2,
}); });
} }
@ -90,18 +85,11 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
/> />
),*/ ),*/
cell: (props) => { cell: (props) => {
return ( return <DataTableRowActions rowContext={props} actions={rowActionFn} />;
<div className='w-full mx-auto text-center'>
<DataTableRowActions rowContext={props} actions={rowActionFn} />
</div>
);
}, },
size: 4,
minSize: 4,
maxSize: 4,
enableSorting: false, enableSorting: false,
enableHiding: false, enableHiding: false,
size: 48,
}); });
} }

View File

@ -1,14 +1,9 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import React from "react"; import React from "react";
export const ButtonGroup = React.forwardRef< export const ButtonGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("flex items-center gap-2", className)} {...props} />
>(({ className, ...props }, ref) => ( )
<div );
ref={ref}
className={cn("ml-auto flex items-center gap-2", className)}
{...props}
/>
));
ButtonGroup.displayName = "ButtonGroup"; ButtonGroup.displayName = "ButtonGroup";

View File

@ -89,17 +89,18 @@ export function DataTable<TData>({
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className={rowClassName}> <TableRow key={headerGroup.id} className={rowClassName}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => (
return ( <TableHead
<TableHead key={header.id}
key={header.id} colSpan={header.colSpan}
colSpan={header.colSpan} style={{
style={{ width: header.getSize() }} width:
> header.getSize() === Number.MAX_SAFE_INTEGER ? "auto" : header.getSize(),
<DataTableColumnHeader table={table} header={header} /> }}
</TableHead> >
); <DataTableColumnHeader table={table} header={header} />
})} </TableHead>
))}
</TableRow> </TableRow>
))} ))}
</TableHeader> </TableHeader>
@ -124,7 +125,16 @@ export function DataTable<TData>({
)} )}
> >
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className={cellClassName}> <TableCell
key={cell.id}
className={cellClassName}
style={{
width:
cell.column.getSize() === Number.MAX_SAFE_INTEGER
? "auto"
: cell.column.getSize(),
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}

View File

@ -32,7 +32,7 @@ export function DataTableColumnHeader<TData, TValue>({
: flexRender(header.column.columnDef.header, header.getContext())} : flexRender(header.column.columnDef.header, header.getContext())}
</div> </div>
{header.column.getCanResize() && ( {false && header.column.getCanResize() && (
<Separator <Separator
orientation='vertical' orientation='vertical'
className={cn( className={cn(

View File

@ -75,7 +75,7 @@ export function DataTablePagination<TData>({
enablePageSizeSelector ? "justify-end" : "justify-between" enablePageSizeSelector ? "justify-end" : "justify-between"
)} )}
> >
<div className='flex w-[100px] items-center justify-center'> <div className='flex w-[150px] items-center justify-start'>
<p className='text-sm font-medium '> <p className='text-sm font-medium '>
{t("common.num_page_of_total", { {t("common.num_page_of_total", {
count: table.getState().pagination.pageIndex + 1, count: table.getState().pagination.pageIndex + 1,

View File

@ -1,6 +1,5 @@
"use client"; "use client";
import { cn } from "@/lib/utils";
import { import {
Button, Button,
DropdownMenu, DropdownMenu,
@ -42,13 +41,8 @@ export function DataTableRowActions<TData = any, TValue = unknown>({
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button size='icon' variant='outline' className='w-8 h-8'>
aria-haspopup='true' <MoreVerticalIcon className='h-3.5 w-3.5' />
size='icon'
variant='link'
className={cn("w-4 h-4 mt-2 text-ring hover:text-muted-foreground", className)}
>
<MoreVerticalIcon className='w-4 h-4' />
<span className='sr-only'>{t("common.open_menu")}</span> <span className='sr-only'>{t("common.open_menu")}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>

View File

@ -134,7 +134,11 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
columns, columns,
enableColumnResizing: false, enableColumnResizing: false,
columnResizeMode: "onChange", columnResizeMode: "onChange",
//defaultColumn, defaultColumn: {
minSize: 0,
size: Number.MAX_SAFE_INTEGER,
maxSize: Number.MAX_SAFE_INTEGER,
},
state: { state: {
rowSelection, rowSelection,
columnVisibility, columnVisibility,

View File

@ -207,9 +207,9 @@ export function useDataTable<TData, TValue>({
debugColumns: false, debugColumns: false,
defaultColumn: { defaultColumn: {
size: 96, //starting column size minSize: 0, //starting column size
minSize: 96, //enforced during column resizing size: Number.MAX_SAFE_INTEGER, //enforced during column resizing
maxSize: 500, //enforced during column resizing maxSize: Number.MAX_SAFE_INTEGER, //enforced during column resizing
}, },
}); });

View File

@ -13,6 +13,7 @@
"upload": "Upload", "upload": "Upload",
"continue": "Continue", "continue": "Continue",
"close": "Close", "close": "Close",
"add": "Add",
"sort_asc": "Asc", "sort_asc": "Asc",
"sort_asc_description": "In ascending order. Click to sort descending order.", "sort_asc_description": "In ascending order. Click to sort descending order.",
"sort_desc": "Desc", "sort_desc": "Desc",

View File

@ -13,6 +13,7 @@
"upload": "Cargar", "upload": "Cargar",
"continue": "Continuar", "continue": "Continuar",
"close": "Cerrar", "close": "Cerrar",
"add": "Añadir",
"sort_asc": "Asc", "sort_asc": "Asc",
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.", "sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
"sort_desc": "Desc", "sort_desc": "Desc",