Bulk selection UI and import-review modal

Transactions gain a checkbox column with select-all; a bulk bar offers
assign-category, defer/restore and delete (confirmed) via the new bulk
endpoint, with toasts and invalidation. The paste-import modal now
lists each skipped duplicate with the matched existing row and offers
'Importar duplicados de todos modos' — re-importing ONLY the skipped
lines so first-pass rows can't double-import (UX-20). Two real fixes
found en route: the mobile list ignored undo-pending deletes and used
en-US dates; the desktop date column also lost its en-US stray. Note:
UX-11 (no mobile layout) was a false positive — a card layout already
existed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-11 21:00:19 -06:00
parent ce3cc69846
commit 0dfac6a285
4 changed files with 291 additions and 10 deletions

View File

@@ -8,12 +8,20 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { DataTableColumnHeader } from '@/components/ui/data-table-column-header';
export interface RowSelection {
selected: Set<number>;
allSelected: boolean;
onToggle: (id: number) => void;
onToggleAll: () => void;
}
interface TransactionColumnOptions {
showCategory: boolean;
showSourceIcon?: boolean;
onEdit: (tx: Transaction) => void;
onDelete: (txId: number) => void;
onToggleDeferred?: (tx: Transaction) => void;
selection?: RowSelection;
}
export function getTransactionColumns({
@@ -22,14 +30,43 @@ export function getTransactionColumns({
onEdit,
onDelete,
onToggleDeferred,
selection,
}: TransactionColumnOptions): ColumnDef<Transaction, unknown>[] {
const columns: ColumnDef<Transaction, unknown>[] = [
const columns: ColumnDef<Transaction, unknown>[] = [];
if (selection) {
columns.push({
id: 'select',
size: 36,
enableSorting: false,
header: () => (
<input
type="checkbox"
className="accent-primary w-4 h-4 cursor-pointer align-middle"
aria-label="Seleccionar todas las transacciones"
checked={selection.allSelected}
onChange={selection.onToggleAll}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="accent-primary w-4 h-4 cursor-pointer align-middle"
aria-label={`Seleccionar ${row.original.merchant}`}
checked={selection.selected.has(row.original.id)}
onChange={() => selection.onToggle(row.original.id)}
/>
),
});
}
columns.push(
{
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', {
{new Date(row.original.date).toLocaleDateString('es-CR', {
month: 'short',
day: 'numeric',
year: 'numeric',
@@ -74,7 +111,7 @@ export function getTransactionColumns({
);
},
},
];
);
if (showCategory) {
columns.push({