Transactions table: Show/hide columns

This commit is contained in:
Víctor Falcón 2025-11-09 19:09:38 +00:00
parent 3795e46d4f
commit f0f062468d
5 changed files with 101 additions and 6 deletions

View File

@ -1,5 +1,5 @@
import { ColumnDef } from '@tanstack/react-table';
import { ArrowDown, ArrowUpDown, MoreHorizontal, Pencil, Trash2 } from 'lucide-react';
import { ArrowDown, MoreHorizontal } from 'lucide-react';
import { format, parseISO } from 'date-fns';
import { Button } from '@/components/ui/button';
@ -36,6 +36,7 @@ export function createTransactionColumns({
return [
{
accessorKey: 'transaction_date',
meta: { label: 'Date' },
header: ({ column }) => {
return (
<Button
@ -56,9 +57,11 @@ export function createTransactionColumns({
</div>
);
},
enableHiding: true,
},
{
accessorKey: 'category_id',
meta: { label: 'Category' },
header: 'Category',
cell: ({ row }) => {
return (
@ -74,6 +77,7 @@ export function createTransactionColumns({
},
{
accessorKey: 'decryptedDescription',
meta: { label: 'Description' },
header: 'Description',
cell: ({ row }) => {
const transaction = row.original;
@ -90,6 +94,7 @@ export function createTransactionColumns({
},
{
accessorKey: 'bank',
meta: { label: 'Bank' },
header: 'Bank',
cell: ({ row }) => {
const bank = row.original.bank;
@ -109,15 +114,28 @@ export function createTransactionColumns({
},
{
accessorKey: 'account',
meta: { label: 'Account' },
header: 'Account',
cell: ({ row }) => {
const account = row.original.account;
return <div>{account?.name || 'N/A'}</div>;
if (!account) {
return <div>N/A</div>;
}
return (
<EncryptedText
encryptedText={account.name}
iv={account.name_iv}
length={{ min: 5, max: 10 }}
className="max-w-[100px] truncate"
/>
);
},
enableHiding: true,
},
{
accessorKey: 'amount',
meta: { label: 'Amount' },
header: () => {
return (
<div className="w-full text-right">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { type ReactNode, useState } from 'react';
import * as Icons from 'lucide-react';
import { format } from 'date-fns';
@ -22,6 +22,7 @@ interface TransactionFiltersProps {
categories: Category[];
accounts: Account[];
isKeySet: boolean;
actions?: ReactNode;
}
export function TransactionFilters({
@ -30,6 +31,7 @@ export function TransactionFilters({
categories,
accounts,
isKeySet,
actions,
}: TransactionFiltersProps) {
const [isOpen, setIsOpen] = useState(false);
const UncategorizedIcon = resolveIconComponent('CircleHelp');
@ -284,6 +286,11 @@ export function TransactionFilters({
Clear
</Button>
)}
{actions ? (
<div className="ml-auto flex items-center gap-2">
{actions}
</div>
) : null}
</div>
</div>
);

View File

@ -0,0 +1,66 @@
import { useEffect, useState } from 'react';
import { type Table as TableType } from '@tanstack/react-table';
import { SlidersHorizontal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
interface DataTableViewOptionsProps<TData> {
table: TableType<TData>;
}
export function DataTableViewOptions<TData>({
table,
}: DataTableViewOptionsProps<TData>) {
const columns = table
.getAllLeafColumns()
.filter((column) => column.getCanHide());
const visibilityStates = columns.map((col) => col.getIsVisible()).join(',');
const [, forceUpdate] = useState(0);
useEffect(() => {
forceUpdate((prev) => prev + 1);
}, [visibilityStates]);
if (columns.length === 0) {
return null;
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
Columns
<SlidersHorizontal className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{columns.map((column) => {
const label: string =
typeof column.columnDef.header === 'string'
? column.columnDef.header
: ((column.columnDef.meta as Record<string, unknown>)?.label as string) ?? column.id.replace(/_/g, ' ');
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{label}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -44,6 +44,8 @@ export function DataTable<TData, TValue>({
virtualRows.length > 0
? totalSize - virtualRows[virtualRows.length - 1].end
: 0;
const visibleColumnCount =
table.getVisibleLeafColumns().length || columns.length || 1;
return (
<div className="overflow-hidden rounded-md border">
@ -76,7 +78,7 @@ export function DataTable<TData, TValue>({
{paddingTop > 0 && (
<TableRow className="border-none hover:bg-transparent">
<TableCell
colSpan={columns.length}
colSpan={visibleColumnCount}
style={{ height: paddingTop }}
/>
</TableRow>
@ -110,7 +112,7 @@ export function DataTable<TData, TValue>({
{paddingBottom > 0 && (
<TableRow className="border-none hover:bg-transparent">
<TableCell
colSpan={columns.length}
colSpan={visibleColumnCount}
style={{ height: paddingBottom }}
/>
</TableRow>
@ -119,7 +121,7 @@ export function DataTable<TData, TValue>({
) : (
<TableRow>
<TableCell
colSpan={columns.length}
colSpan={visibleColumnCount}
className="h-24 text-center"
>
{emptyMessage}

View File

@ -15,6 +15,7 @@ import AppSidebarLayout from '@/layouts/app/app-sidebar-layout';
import { DataTable } from '@/components/ui/data-table';
import HeadingSmall from '@/components/heading-small';
import { DataTablePagination } from '@/components/ui/data-table-pagination';
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
import { Skeleton } from '@/components/ui/skeleton';
import { TransactionFilters } from '@/components/transactions/transaction-filters';
import { EditTransactionDialog } from '@/components/transactions/edit-transaction-dialog';
@ -452,6 +453,7 @@ export default function Transactions({ categories, accounts, banks }: Props) {
categories={categories}
accounts={accounts}
isKeySet={isKeySet}
actions={<DataTableViewOptions table={table} />}
/>
{isLoading ? (