import React, { useState } from "react"; import { Ban, Pencil, RotateCcw } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "./card"; import { Button } from "./button"; import { Spinner } from "./spinner"; import { Badge } from "./badge"; import { Chip } from "./chip"; import { CategoryEditDialog, type CategoryItem } from "./category-edit-dialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./tooltip"; import { cn } from "@/lib/utils"; import { formatCurrency } from "@/lib/format-currency"; import { formatDateShort } from "@/lib/format-date"; // ─── Types ─────────────────────────────────────────────────────────────────── export interface DashboardTransaction { id: string; /** ISO date string — "2024-04-15" or full ISO */ date: string; description: string; /** Optional merchant / location sub-label */ merchant?: string; /** Signed dollar amount — positive = credit, negative = debit */ amount: number; /** Display label for the category chip */ category?: string; /** Category ID used for filter matching */ categoryId?: string; /** User-overridden category name — displayed instead of `category` when set */ editedCategoryName?: string; /** When true, the row is excluded from calculations (dimmed, amount struck through) */ isExcluded?: boolean; } export type { CategoryItem }; export interface DashboardTransactionsTableProps { transactions?: DashboardTransaction[]; title?: string; /** Show a "Load More" button when more pages are available */ hasNextPage?: boolean; isLoadingMore?: boolean; onLoadMore?: () => void; isLoading?: boolean; className?: string; /** * Active category filter ID. Rows whose categoryId does not match are dimmed. * Pass null / undefined to show all rows at full opacity. */ selectedCategoryId?: string | null; /** * Category tree for the built-in CategoryEditDialog. * When provided, clicking a category chip opens the dialog inline. */ categories?: CategoryItem[]; /** * Called after the user saves a category change via the built-in dialog. */ onCategoryChange?: ( transactionId: string, categoryId: string, applyToFuture: boolean, ) => void; /** * When provided, each row shows an exclude/restore icon button that toggles * whether the transaction is counted in calculations. */ onToggleExclude?: (transaction: DashboardTransaction) => void; /** * Show or hide the card header (title + tab row). * Set to `false` when the surrounding layout already provides context. * Defaults to `true`. */ showHeader?: boolean; /** * Show or hide the "Account Transaction" underlined tab below the title. * Set to `false` when the surrounding layout already provides tab context. * Defaults to `true`. */ showTab?: boolean; /** * Colour scheme for the editable category chip hover state. * - `"primary"` (default) — green hover * - `"secondary"` — dark navy hover; use when the surrounding context uses the secondary palette (e.g. expense view) */ colorScheme?: "primary" | "secondary"; } // ─── Category chip ──────────────────────────────────────────────────────────── function CategoryChip({ label, canEdit, colorScheme = "primary", onClick, }: { label: string; canEdit: boolean; colorScheme?: "primary" | "secondary"; onClick?: () => void; }) { if (canEdit) { return ( ); } return {label}; } // ─── Transaction row ────────────────────────────────────────────────────────── function TransactionRow({ tx, isDimmed, canEdit, colorScheme = "primary", onChipClick, onToggleExclude, }: { tx: DashboardTransaction; isDimmed: boolean; canEdit: boolean; colorScheme?: "primary" | "secondary"; onChipClick?: () => void; onToggleExclude?: (transaction: DashboardTransaction) => void; }) { const isCredit = tx.amount >= 0; const categoryLabel = tx.editedCategoryName ?? tx.category; const excludeLabel = tx.isExcluded ? "Restore transaction" : "Exclude from calculations"; return (

{formatDateShort(tx.date)}

{tx.description}

{tx.merchant && (

{tx.merchant}

)} {categoryLabel && (
)}
{isCredit ? "+" : ""} {formatCurrency(tx.amount, { showSign: false })} {onToggleExclude && ( onToggleExclude(tx)} className="flex size-7 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground" > {tx.isExcluded ? ( ) : ( )} } /> {tx.isExcluded ? "Restore transaction" : "Exclude from calculations"} )}
); } // ─── Component ──────────────────────────────────────────────────────────────── export function DashboardTransactionsTable({ transactions = [], title = "Transaction History", hasNextPage = false, isLoadingMore = false, onLoadMore, isLoading = false, className, selectedCategoryId, categories, onCategoryChange, onToggleExclude, showHeader = true, showTab = true, colorScheme = "primary", }: DashboardTransactionsTableProps) { const isFiltering = selectedCategoryId != null; const canEdit = !!categories?.length; const [editingTx, setEditingTx] = useState(null); const handleSave = (categoryId: string, applyToFuture: boolean) => { if (editingTx) { onCategoryChange?.(editingTx.id, categoryId, applyToFuture); } setEditingTx(null); }; return ( <> {showHeader && ( {title} {showTab && (
Account Transaction
)}
)} {isLoading ? (
) : transactions.length === 0 ? (

No transactions found

) : (
{transactions.map((tx) => ( setEditingTx(tx)} onToggleExclude={onToggleExclude} /> ))}
)} {hasNextPage && onLoadMore && (
)}
{canEdit && ( !open && setEditingTx(null)} transactionDescription={editingTx?.description} currentCategoryId={editingTx?.categoryId} categories={categories!} onSave={handleSave} /> )} ); }