Add behavior documentation covering all SSR and legacy SPA pages: - Testing strategy and type definitions (unit/integration/UI) - Dashboard, Invoice, Payment, Transaction, Ledger pages - Company/Settings, POS, Admin, Search, Auth pages - Legacy SPA behavior docs (no UI tests until migrated) - Edge cases, test data requirements, and dependencies per subsystem Total: 3,600+ lines of behavior documentation to guide test authorship.
548 lines
20 KiB
Markdown
548 lines
20 KiB
Markdown
# Legacy SPA Behaviors
|
|
|
|
## Overview
|
|
|
|
These pages are rendered client-side via Reagent/Re-frame. They use GraphQL for data fetching. They are being migrated to HTMX SSR. Behavior docs exist for reference but **NO UI tests should be written for these pages until migrated**.
|
|
|
|
### Architecture
|
|
- **Routing**: Bidi client-side routes (`src/cljc/auto_ap/client_routes.cljc`)
|
|
- **State**: Re-frame subscriptions and events
|
|
- **Data**: GraphQL via custom `graphql` effect (`auto-ap.effects`)
|
|
- **Permissions**: `auto-ap.permissions/can?` with role-based checks
|
|
|
|
### Role Permissions Summary
|
|
| Role | Transaction Page | Ledger Page | Vendor Create/Edit |
|
|
|------|-----------------|-------------|-------------------|
|
|
| admin | full | full | full |
|
|
| power-user | full | full | full |
|
|
| manager | full | blocked | full |
|
|
| user | full | full | full |
|
|
| read-only | blocked | full | blocked |
|
|
|
|
---
|
|
|
|
## Pages
|
|
|
|
### Home (`/`)
|
|
|
|
#### Purpose
|
|
Dashboard showing financial overview for the selected client: top expense categories (pie chart), upcoming bills (bar chart), and cash flow projection (interactive bar chart with table).
|
|
|
|
#### User Flows
|
|
1. User lands on `/`
|
|
2. Page loads data for currently selected client
|
|
3. If user has access to multiple clients, shows note: "these reports are for [client]. Please choose a specific customer for their report."
|
|
4. User can switch cash flow range: 7/30/60/90/120/150/180 days
|
|
5. Cash flow bars are clickable and redirect to unpaid invoices for that date
|
|
|
|
#### GraphQL Queries Used
|
|
```graphql
|
|
query HomeDashboard($clientId: id) {
|
|
expense_account_stats(client_id: $clientId) {
|
|
account { id name }
|
|
total
|
|
}
|
|
invoice_stats(client_id: $clientId) {
|
|
name
|
|
paid
|
|
unpaid
|
|
}
|
|
cash_flow(client_id: $clientId) {
|
|
beginning_balance
|
|
outstanding_payments
|
|
invoices_due_soon { due outstanding_balance invoice_number vendor { id name } }
|
|
upcoming_credits { date amount identifier }
|
|
upcoming_debits { date amount identifier }
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Dashboard loads with all three chart sections
|
|
- **Multi-client note**: Displays when user hasn't selected a specific client
|
|
- **Cash flow ranges**: Switching between 7-180 day views updates chart and table
|
|
- **Cash flow table**: Shows invoices, upcoming debits/credits with days-until
|
|
- **Edge case**: No data for client shows empty charts gracefully
|
|
- **Error states**: GraphQL errors show loading state appropriately
|
|
|
|
---
|
|
|
|
### Login (`/login`)
|
|
|
|
#### Purpose
|
|
Authentication page with Google OAuth login.
|
|
|
|
#### User Flows
|
|
1. User visits `/login`
|
|
2. Shows "Login with Google" button
|
|
3. Button links to Google OAuth with optional `redirect-to` query param
|
|
4. After auth failure/logout, may show logout reason notification
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
None.
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Shows login button linking to Google OAuth
|
|
- **Redirect**: Preserves `redirect-to` query parameter in OAuth URL
|
|
- **Logout reason**: Displays warning notification when `logout-reason` is set
|
|
- **Edge case**: Already authenticated user visiting login page
|
|
|
|
---
|
|
|
|
### Needs Activation (`/needs-activation`)
|
|
|
|
#### Purpose
|
|
Shown when authenticated user's account is not yet activated.
|
|
|
|
#### User Flows
|
|
1. User logs in but account is inactive
|
|
2. Shows message: "Sorry, your user is not activated yet. Please have Ben Skinner enable your account."
|
|
3. "here" link clears user state and redirects to login
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
None.
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Shows activation required message
|
|
- **Relogin link**: Clears user state and redirects to login
|
|
|
|
---
|
|
|
|
### Transactions (`/transactions/`, `/transactions/unapproved`, `/transactions/approved`, `/transactions/requires-feedback`, `/transactions/excluded`)
|
|
|
|
#### Purpose
|
|
Transaction list with filtering, editing, and bulk admin operations. Different routes show different approval statuses.
|
|
|
|
#### User Flows
|
|
1. User navigates to transactions page
|
|
2. Default date filter: last 1 month
|
|
3. Side bar allows filtering by: vendor, account, bank account, date range, amount range, location, import batch, description, linked status
|
|
4. Table shows transactions with checkbox selection (admin only)
|
|
5. Clicking row opens edit side bar
|
|
6. Admin can: bulk code, bulk delete, suppress, or manual Yodlee import
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
```graphql
|
|
query TransactionPage($filters: transaction_filters) {
|
|
transaction_page(filters: $filters) {
|
|
data {
|
|
id amount memo location approval_status check_number is_locked
|
|
matched_rule { note id }
|
|
vendor { name id }
|
|
accounts { id amount location account { name id location numeric_code } }
|
|
date yodlee_merchant { name yodlee_id id } plaid_merchant { name id }
|
|
post_date expected_deposit { id date } forecast_match { id identifier }
|
|
status description_original
|
|
payment { check_number s3_url id date }
|
|
client { name id }
|
|
bank_account { name yodlee_account_id current_balance }
|
|
}
|
|
total start end count
|
|
}
|
|
}
|
|
|
|
mutation EditTransaction($transaction: edit_transaction) {
|
|
edit_transaction(transaction: $transaction) { ... }
|
|
}
|
|
|
|
mutation DeleteTransactions($filters: transaction_filters, $ids: [id], $suppress: Boolean) {
|
|
delete_transactions(filters: $filters, ids: $ids, suppress: $suppress) { message }
|
|
}
|
|
|
|
mutation BulkCodeTransactions($filters: transaction_filters, $ids: [id], $vendor: id, $approval_status: transaction_approval_status, $accounts: [edit_percentage_account]) {
|
|
bulk_code_transactions(filters: $filters, ids: $ids, vendor: $vendor, approval_status: $approval_status, accounts: $accounts) { message }
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Loads transaction list with default 1-month filter
|
|
- **Route variants**: Each approval-status route applies correct default filter
|
|
- **Filtering**: Side bar filters trigger debounced data refresh (800ms)
|
|
- **Pagination**: Start/per-page params work correctly
|
|
- **Edit transaction**: Update vendor, approval status, memo, expense accounts
|
|
- **Edit validation**: Account total must equal transaction amount; locations must be valid
|
|
- **Bulk delete (admin)**: Delete selected transactions or all visible transactions
|
|
- **Bulk suppress (admin)**: Mark transactions as suppressed instead of deleting
|
|
- **Bulk code (admin)**: Apply vendor/account rules to multiple transactions
|
|
- **Manual import (admin)**: Import transactions via manual Yodlee import dialog
|
|
- **Error states**: Locked transactions cannot be edited/deleted; validation errors display inline
|
|
- **Permissions**: Non-admin users don't see checkboxes or action buttons
|
|
|
|
---
|
|
|
|
### Ledger (`/ledger/`)
|
|
|
|
#### Purpose
|
|
General ledger showing journal entries with line items. Supports filtering and CSV export.
|
|
|
|
#### User Flows
|
|
1. User navigates to `/ledger/`
|
|
2. Default date filter: last 1 month
|
|
3. Side bar filters: vendor, account, bank account, date range, amount range, location
|
|
4. Table shows journal entries with expandable line items
|
|
5. Admin can export to CSV
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
```graphql
|
|
query LedgerPage($filters: ledger_filters) {
|
|
ledger_page(filters: $filters) {
|
|
journal_entries {
|
|
id source original_entity amount note cleared_against alternate_description
|
|
vendor { name id } client { name id }
|
|
line_items { id debit credit location running_balance account { id name } }
|
|
date
|
|
}
|
|
total start end
|
|
}
|
|
}
|
|
|
|
query LedgerCsv($filters: ledger_filters) {
|
|
ledger_csv(filters: $filters) { csv_content_b64 }
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Loads ledger with default 1-month filter
|
|
- **Filtering**: Date range, vendor, account, location filters work
|
|
- **CSV export**: Admin can export filtered results as base64 CSV download
|
|
- **Pagination**: Virtual pagination controls
|
|
- **Permissions**: Manager role sees "Not authorized"
|
|
|
|
---
|
|
|
|
### Profit and Loss (`/ledger/profit-and-loss`)
|
|
|
|
#### Purpose
|
|
Financial report showing revenue and expenses over configurable periods. Supports multi-company and PDF export.
|
|
|
|
#### User Flows
|
|
1. User selects companies (multi-select typeahead)
|
|
2. User selects period preset (13 periods, 12 months, last week, week-to-date, last month, month-to-date, year-to-date, last calendar year, full year)
|
|
3. Optional: Include deltas, Column per location
|
|
4. Click "Run" to generate report
|
|
5. Admin can "Export" to PDF
|
|
6. Clicking report cells opens ledger detail side bar
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
```graphql
|
|
query ProfitAndLoss($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
|
|
profit_and_loss(client_ids: $clientIds, periods: $periods, include_deltas: $includeDeltas, column_per_location: $columnPerLocation) {
|
|
periods { accounts { name amount client_id account_type id count numeric_code location } }
|
|
}
|
|
}
|
|
|
|
query ProfitAndLossPdf($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
|
|
profit_and_loss_pdf(...) { url name }
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Generate P&L for single client with default period
|
|
- **Multi-company**: Select multiple clients and generate combined report
|
|
- **Period presets**: All preset buttons populate correct date ranges
|
|
- **Advanced mode**: Custom period date pickers
|
|
- **Include deltas**: Shows period-over-period changes
|
|
- **Column per location**: Breaks out by location (mutually exclusive with deltas)
|
|
- **PDF export**: Admin can generate and download PDF; single-client shows email link
|
|
- **Ledger drill-down**: Clicking cells opens ledger entries side bar for that account/period
|
|
- **Permissions**: Manager role sees "Not authorized"
|
|
|
|
---
|
|
|
|
### Cash Flows (`/ledger/cash-flows`)
|
|
|
|
#### Purpose
|
|
Statement of cash flows report. Same control structure as P&L but different report format.
|
|
|
|
#### User Flows
|
|
Same as Profit and Loss but generates cash flow statement.
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
```graphql
|
|
query CashFlows($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
|
|
profit_and_loss(client_ids: $clientIds, periods: $periods, include_deltas: $includeDeltas, column_per_location: $columnPerLocation) {
|
|
periods { accounts { name amount debits credits client_id account_type id count numeric_code location } }
|
|
}
|
|
}
|
|
|
|
query CashFlowsPdf($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
|
|
cash_flows_pdf(...) { url name }
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Generate cash flows report
|
|
- **Same controls as P&L**: Companies, periods, deltas, location columns
|
|
- **PDF export**: Admin-only export button
|
|
- **Ledger drill-down**: Clicking cells opens ledger detail
|
|
- **Permissions**: Manager role sees "Not authorized"
|
|
|
|
---
|
|
|
|
### Profit and Loss Detail (`/ledger/profit-and-loss-detail`)
|
|
|
|
#### Purpose
|
|
Detailed journal entry report broken down by category (sales, COGS, payroll, controllable, fixed overhead, ownership controllable).
|
|
|
|
#### User Flows
|
|
1. Select companies
|
|
2. Select date range (start/end date pickers)
|
|
3. Click "Run" to generate detail report
|
|
4. Shows journal entries grouped by category and account with running balances
|
|
5. Admin can export to PDF
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
```graphql
|
|
query JournalDetailReport($clientIds: [id], $dateRange: date_range, $categories: [category]) {
|
|
journal_detail_report(client_ids: $clientIds, date_range: $dateRange, categories: $categories) {
|
|
categories {
|
|
category client_id location
|
|
account { numeric_code name }
|
|
journal_entries { description date debit credit location running_balance account { id name } }
|
|
total
|
|
}
|
|
}
|
|
}
|
|
|
|
query JournalDetailReportPdf($clientIds: [id], $dateRange: date_range, $categories: [category]) {
|
|
journal_detail_report_pdf(...) { url name }
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Generate detail report with default 2-week range
|
|
- **Category breakdown**: Report includes all 6 category sections plus Gross Profit, Overhead, Net Profit summaries
|
|
- **Date range filtering**: Start/end dates filter journal entries
|
|
- **PDF export**: Admin can export to PDF with email link for single client
|
|
- **Permissions**: Manager role sees "Not authorized"
|
|
|
|
---
|
|
|
|
### Balance Sheet (`/ledger/balance-sheet`)
|
|
|
|
#### Purpose
|
|
Balance sheet report as of a specific date, with optional prior-year comparison.
|
|
|
|
#### User Flows
|
|
1. Select companies
|
|
2. Select report date
|
|
3. Optionally enable "Include comparison" and select comparison date
|
|
4. Click "Run" to generate balance sheet
|
|
5. Admin can export to PDF
|
|
6. Clicking cells opens ledger detail side bar
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
```graphql
|
|
query BalanceSheet($clientIds: [id], $date: iso_date, $comparisonDate: iso_date, $includeComparison: Boolean) {
|
|
balance_sheet(client_ids: $clientIds, date: $date, comparison_date: $comparisonDate, include_comparison: $includeComparison) {
|
|
balance_sheet_accounts { name amount account_type id numeric_code client_id }
|
|
comparable_balance_sheet_accounts { name amount account_type id client_id numeric_code }
|
|
}
|
|
}
|
|
|
|
query BalanceSheetPdf($clientIds: [id], $date: iso_date, $comparisonDate: iso_date, $includeComparison: Boolean) {
|
|
balance_sheet_pdf(...) { url name }
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Generate balance sheet for current date
|
|
- **Comparison mode**: Shows prior period side-by-side
|
|
- **Multi-company**: Combines multiple clients
|
|
- **PDF export**: Admin-only with email composition link for single client
|
|
- **Ledger drill-down**: Clicking cells opens ledger entries filtered by account and date range
|
|
- **Permissions**: Manager role sees "Not authorized"
|
|
|
|
---
|
|
|
|
### External Ledger (`/ledger/external`)
|
|
|
|
#### Purpose
|
|
Shows only externally-imported journal entries. Admin can delete entries.
|
|
|
|
#### User Flows
|
|
1. Admin navigates to `/ledger/external`
|
|
2. Shows external journal entries with external_id
|
|
3. Can select entries and delete them
|
|
4. Admin can export to CSV
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
```graphql
|
|
query ExternalLedger($filters: ledger_filters) {
|
|
ledger_page(filters: $filters) {
|
|
journal_entries { id external_id source ... }
|
|
total start end
|
|
}
|
|
}
|
|
|
|
mutation DeleteExternalLedger($filters: ledger_filters, $ids: [id]) {
|
|
delete_external_ledger(filters: $filters, ids: $ids) { message }
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Loads external-only ledger entries
|
|
- **Delete**: Admin can delete selected entries (max 1000 at once)
|
|
- **CSV export**: Same as main ledger
|
|
- **Permissions**: Admin-only; non-admin sees "Not authorized"
|
|
|
|
---
|
|
|
|
### External Import (`/ledger/external-import`)
|
|
|
|
#### Purpose
|
|
Manual import of ledger entries via tab-separated text paste.
|
|
|
|
#### User Flows
|
|
1. Admin pastes tab-separated data into textarea
|
|
2. Optional: indicates whether first row is header
|
|
3. Click "Parse" to convert to table
|
|
4. Table shows parsed rows: Id, Client, Source, Vendor, Date, Account, Location, Debit, Credit, Note, Cleared against
|
|
5. Click "Import" to submit
|
|
6. Results show: success count, ignored count, error count
|
|
7. Errors show inline with dropdown explanations
|
|
8. "Only show errors" filter available
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
```graphql
|
|
mutation ImportLedger($entries: [ledger_entry_input]) {
|
|
import_ledger(entries: $entries) {
|
|
successful { external_id }
|
|
existing { external_id }
|
|
ignored { external_id }
|
|
errors { external_id error }
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Paste TSV data, parse, import successfully
|
|
- **Header row**: Checkbox to skip first row
|
|
- **Validation**: Client code must exist, vendor must exist, date must be MM/dd/yyyy, debits=credits, amounts > 0
|
|
- **Lock check**: Entries must be after client's locked-until date
|
|
- **Location validation**: Location must belong to client or be "A"
|
|
- **Account validation**: Account must exist and location must match account's required location
|
|
- **Error display**: Errors show per-row with dropdown explanation
|
|
- **Success/ignored/existing**: Status icons show import result per row
|
|
- **Permissions**: Admin-only
|
|
|
|
---
|
|
|
|
### Payments (`/payments/`)
|
|
|
|
#### Purpose
|
|
**Already migrated to SSR.** Legacy client route exists for navbar highlighting only. Actual payments page is served by `payment` SSR routes at `/payment/`.
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
N/A (SSR page)
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
Already migrated. Test via SSR routes.
|
|
|
|
---
|
|
|
|
### Reports (`/reports/`)
|
|
|
|
#### Purpose
|
|
**Already migrated to SSR.** Legacy client route exists for navbar highlighting only. Actual reports page is served by `company/reports` SSR routes.
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
N/A (SSR page)
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
Already migrated. Test via SSR routes.
|
|
|
|
---
|
|
|
|
### Admin Vendors (`/admin/vendors`)
|
|
|
|
#### Purpose
|
|
**Already migrated to SSR.** Legacy client route exists for navbar highlighting only. Actual vendor management is served by `admin/vendor` SSR routes.
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
N/A (SSR page)
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
Already migrated. Test via SSR routes.
|
|
|
|
---
|
|
|
|
### New Vendor (`/vendor/new`)
|
|
|
|
#### Purpose
|
|
Opens the home dashboard with the vendor creation dialog pre-opened.
|
|
|
|
#### User Flows
|
|
1. User navigates to `/vendor/new`
|
|
2. Loads home dashboard
|
|
3. Opens vendor dialog if user has `:vendor :create` permission
|
|
4. Dialog allows creating new vendor with name, terms, address, contacts, default account, etc.
|
|
|
|
#### GraphQL Queries/Mutations Used
|
|
```graphql
|
|
mutation UpsertVendor($vendor: add_vendor) {
|
|
upsert_vendor(vendor: $vendor) { id name ... }
|
|
}
|
|
```
|
|
|
|
#### Behaviors to Test (when migrated)
|
|
- **Happy path**: Home loads with vendor dialog open
|
|
- **Permission check**: Dialog only opens if user can `:vendor :create`
|
|
- **Vendor creation**: Form submission creates vendor via GraphQL mutation
|
|
- **Validation**: Only one terms override, schedule payment DOM override, and account override per client
|
|
|
|
---
|
|
|
|
## Cross-Cutting Behaviors
|
|
|
|
### GraphQL Query Patterns
|
|
- All data fetching uses re-frame `graphql` effect with JWT token from `:user` in app-db
|
|
- Queries use `owns-state` to track loading status per page
|
|
- Results flow through `::data-page/received` event which stores data and syncs URL query params
|
|
- Filter changes are debounced (800ms) via `dispatch-debounce`
|
|
|
|
### Client-Side State Management (Re-frame)
|
|
- **Data pages**: `data-page` namespace provides reusable pagination/filtering state
|
|
- `::data-page/params` - merged filters + table params + query params
|
|
- `::data-page/data` - GraphQL response data
|
|
- `::data-page/checked` - selected rows for bulk operations
|
|
- **Forms**: `forms` namespace manages edit dialogs with `start-form`, `change-handler`, `save-succeeded`
|
|
- **Status**: `status` namespace tracks async operation states (`:loading`, `:complete`, `:error`)
|
|
|
|
### Navigation Between Legacy Pages
|
|
- Navbar uses Bidi `path-for` with `routes/routes` for legacy SPA links
|
|
- Some navbar items (Payments, POS, Invoices) now link to `ssr-routes/only-routes` instead
|
|
- Page components dispatch `::mounted` on mount and `::unmounted` on unmount to set up/tear down:
|
|
- Data subscriptions
|
|
- Forward event listeners
|
|
- Track subscriptions (parameter change watchers)
|
|
|
|
---
|
|
|
|
## Migration Notes
|
|
|
|
### Actively Migrated / Already SSR
|
|
- **Payments** (`/payments/`) - Fully migrated to `/payment/` SSR routes
|
|
- **Reports** (`/reports/`) - Fully migrated to `/company/reports` SSR routes
|
|
- **Admin Vendors** (`/admin/vendors`) - Fully migrated to `/admin/vendor` SSR routes
|
|
|
|
### Still Legacy SPA (prioritized by complexity)
|
|
1. **Transactions** - High complexity (filters, edit form, bulk operations, manual import)
|
|
2. **Home/Dashboard** - Medium complexity (charts, cash flow calculations)
|
|
3. **Ledger** - Medium complexity (filters, CSV export)
|
|
4. **External Ledger** - Low-medium complexity (subset of ledger + delete)
|
|
5. **External Import** - Medium complexity (TSV parsing, validation, batch import)
|
|
6. **Profit and Loss** - High complexity (multi-company, periods, PDF export)
|
|
7. **Cash Flows** - High complexity (shares P&L infrastructure)
|
|
8. **Profit and Loss Detail** - Medium complexity (category filtering, running balances)
|
|
9. **Balance Sheet** - Medium complexity (comparison mode, drill-down)
|
|
10. **Login** - Low complexity (static page)
|
|
11. **Needs Activation** - Low complexity (static page)
|
|
12. **New Vendor** - Low complexity (dialog on home page)
|
|
|
|
### Migration Risks
|
|
- **Chart libraries**: Home page uses Recharts (React). Replacement needed for SSR.
|
|
- **PDF generation**: P&L, Cash Flows, Balance Sheet, P&L Detail all support PDF export via server-side generation.
|
|
- **Bulk operations**: Transactions page has complex bulk coding/deletion with validation.
|
|
- **External import**: TSV parsing and validation logic lives entirely in SPA; server-side equivalent needed.
|