Add bulk actions, multi-currency, expenses, templates, and reports

Schema (migration 0001):
- clients: add currency column (default USD)
- invoices: add currency column (default USD)
- New expenses table: amount, currency, category, billable, reimbursable,
  client/invoice/business relations, notes
- New invoice_templates table: name, type (notes|terms), content, isDefault

API:
- invoices: add bulkUpdateStatus and bulkDelete procedures (ownership-safe)
- invoices: currency field threaded through create/update schemas
- clients: currency field added to create/update schemas
- New expenses router: full CRUD with authorization
- New invoiceTemplates router: full CRUD, isDefault management per type
- Root router: wire in expenses and invoiceTemplates

Currency (src/lib/currency.ts):
- Shared formatCurrency(amount, currency) utility replacing hardcoded USD
- SUPPORTED_CURRENCIES list (17 currencies)
- Invoice form: currency selector in Config card, auto-fills from client
- Client form: currency selector in Billing Information card

Bulk actions (invoices list):
- Checkbox column with select-all support
- Selection toolbar: Mark as Sent/Paid/Draft dropdown, Delete (N) button
- DataTable: new selectionActions prop renders toolbar when rows selected

Notes templates:
- Invoice form: Notes card with textarea in Details tab
- Template dropdown button appears when templates exist
- /dashboard/invoices/templates: full CRUD page for notes and terms templates

New pages:
- /dashboard/expenses: expense list with summary cards, add/edit dialog
- /dashboard/reports: KPI cards, 12-month revenue area chart, top clients
  bar chart, status breakdown, recent activity
- Navigation: Expenses and Reports added to Main section

https://claude.ai/code/session_012sqEgNQpx676isepeoX4Mi
This commit is contained in:
Claude
2026-04-05 02:34:06 +00:00
parent ba14526fc5
commit e6b79ce2c2
19 changed files with 3233 additions and 214 deletions
+23
View File
@@ -72,6 +72,11 @@ interface DataTableProps<TData, TValue> {
options: { label: string; value: string }[];
}[];
onRowClick?: (row: TData) => void;
/** Render bulk-action buttons when rows are selected. Receives selected rows and a clear function. */
selectionActions?: (
selectedRows: TData[],
clearSelection: () => void,
) => React.ReactNode;
}
export function DataTable<TData, TValue>({
@@ -89,6 +94,7 @@ export function DataTable<TData, TValue>({
actions,
filterableColumns = [],
onRowClick,
selectionActions,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
@@ -335,6 +341,23 @@ export function DataTable<TData, TValue>({
</Card>
)}
{/* Selection Toolbar */}
{selectionActions && table.getSelectedRowModel().rows.length > 0 && (
<Card className="bg-primary/5 border-primary/20 border py-2">
<CardContent className="flex items-center justify-between gap-3 px-3 py-0">
<span className="text-foreground text-sm font-medium">
{table.getSelectedRowModel().rows.length} selected
</span>
<div className="flex items-center gap-2">
{selectionActions(
table.getSelectedRowModel().rows.map((r) => r.original),
() => table.resetRowSelection(),
)}
</div>
</CardContent>
</Card>
)}
{/* Table Content Card */}
<Card className="bg-card border-border overflow-hidden border p-0">
<div className="w-full overflow-x-auto">