mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 10:28:48 +02:00
Migrate all components and pages to shadcn/ui with DataTable
All checks were successful
Deploy to VPS / deploy (push) Successful in 28s
All checks were successful
Deploy to VPS / deploy (push) Successful in 28s
Replace custom markup across all pages and components with shadcn/ui primitives (Dialog, Sheet, Select, Card, Tabs, etc.). Add reusable DataTable component powered by @tanstack/react-table with sortable column headers and client-side pagination. Introduce TransactionList with responsive mobile cards and desktop DataTable, dashboard section customization (DashboardSection, SectionConfigDialog), and settings API types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
137
frontend/src/components/transactions/transaction-columns.tsx
Normal file
137
frontend/src/components/transactions/transaction-columns.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { type ColumnDef } from '@tanstack/react-table';
|
||||
import { Pencil, Trash2, TrendingDown, TrendingUp } from 'lucide-react';
|
||||
|
||||
import { type Transaction } from '@/api';
|
||||
import { formatAmount } from '@/lib/format';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { DataTableColumnHeader } from '@/components/ui/data-table-column-header';
|
||||
|
||||
interface TransactionColumnOptions {
|
||||
showCategory: boolean;
|
||||
onEdit: (tx: Transaction) => void;
|
||||
onDelete: (txId: number) => void;
|
||||
}
|
||||
|
||||
export function getTransactionColumns({
|
||||
showCategory,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: TransactionColumnOptions): ColumnDef<Transaction, unknown>[] {
|
||||
const columns: ColumnDef<Transaction, unknown>[] = [
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Date" />,
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono text-muted-foreground text-xs whitespace-nowrap">
|
||||
{new Date(row.original.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'merchant',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Merchant" />,
|
||||
cell: ({ row }) => {
|
||||
const tx = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
'w-6 h-6 rounded flex items-center justify-center shrink-0',
|
||||
tx.transaction_type === 'DEVOLUCION'
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'bg-destructive/10 text-destructive',
|
||||
)}
|
||||
>
|
||||
{tx.transaction_type === 'DEVOLUCION' ? (
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
) : (
|
||||
<TrendingDown className="w-3 h-3" />
|
||||
)}
|
||||
</div>
|
||||
<span className="truncate">{tx.merchant}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (showCategory) {
|
||||
columns.push({
|
||||
accessorFn: (row) => row.category?.name ?? '',
|
||||
id: 'category',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Category" />,
|
||||
cell: ({ row }) => {
|
||||
const category = row.original.category;
|
||||
return category ? (
|
||||
<Badge variant="secondary">{category.name}</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
accessorKey: 'amount',
|
||||
meta: { className: 'text-right' },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Amount" className="justify-end" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const tx = row.original;
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'font-mono font-medium',
|
||||
tx.transaction_type === 'DEVOLUCION' && 'text-primary',
|
||||
)}
|
||||
>
|
||||
{tx.transaction_type === 'DEVOLUCION' ? '+' : '-'}
|
||||
{formatAmount(tx.amount, tx.currency)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
meta: { className: 'text-right' },
|
||||
size: 80,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const tx = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Edit transaction"
|
||||
aria-label="Edit transaction"
|
||||
onClick={() => onEdit(tx)}
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Delete transaction"
|
||||
aria-label="Delete transaction"
|
||||
onClick={() => onDelete(tx.id)}
|
||||
className="hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return columns;
|
||||
}
|
||||
Reference in New Issue
Block a user