Usability overhaul: payment workflow, person-centric contacts, command palette, and systemic fixes - #2
Merged
Conversation
Make the scoped external API (/api/external) usable by AI agents and codegen tools. - openapi.yaml: OpenAPI 3.1 contract for /api/external — bearer API-key auth, the resource+id paths, CRUD operations, response/error shapes, the full resource enum, the resource→permission-group mapping, and request/response examples. Validates clean. Linked from api-examples.md. - mcp-server/: a local stdio MCP server (TypeScript) wrapping the API so an agent can list/get/create/update/delete records through an API key. Six tools (list_resources, list_records, get_record, create_record, update_record, delete_record) with proper read-only/destructive/ idempotent annotations and actionable error mapping (401/403/400/404). Config via CLIENTFLOW_API_URL/CLIENTFLOW_API_KEY; refuses to start without them and never prints the key. Includes README with a Claude Desktop config block and unit tests for the api-client (9 passing). All access remains gated server-side by the key's scopes and the owning user's role — this adds no privileges. No changes to existing app behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…DE.md - package.json: rename vite_react_shadcn_ts -> client-flow, version 0.1.0 - remove @supabase/supabase-js (unused; the app uses the local client shim) - make db:reset cross-platform (node fs.rmSync instead of `rm -f`) - delete the dead supabase/ directory (Deno edge functions + migrations superseded by server/routes/ and server/db/) and the stale bun.lockb (the project uses npm) - add a tracked CLAUDE.md documenting architecture, the security model, numbering, conventions, and gotchas (AGENTS.md is gitignored, so its guidance did not travel with the repo) Build and the 23-test suite still pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Since the local SQLite file holds real business/financial data, add an online backup (better-sqlite3 db.backup(), safe while the app serves requests): - server/utils/backup.ts: backupDatabase() writes a timestamped copy to BACKUP_DIR and prunes to the newest BACKUP_RETENTION files; it never throws, so a backup failure can't affect request handling. startBackupSchedule() runs one backup on boot and every BACKUP_INTERVAL_HOURS (unref'd so it doesn't block shutdown; disabled when BACKUP_INTERVAL_HOURS=0). - index.ts starts the schedule after listen (skipped under test). - Defaults: BACKUP_DIR=./data/backups (already gitignored), 24h, keep 7. - Documented in .env.example and README; tested (valid independently- openable copy + retention). Suite now 26 passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
better-sqlite3 does not create the parent directory of the database file, and data/ is gitignored — so `npm run dev` on a clean clone crashed with "Cannot open database because the directory does not exist". getDatabase() now mkdir -p's the DB's parent directory before opening. (Tests were unaffected because the test helper creates its own temp dir.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TanStack Query was installed but unused; every page hand-rolled useState/useEffect fetching. Start adopting it, beginning with the read-only Dashboard as the template: - App.tsx: give the QueryClient sensible defaults (30s staleTime, 5m gcTime, retry once, refetch on window focus) - Dashboard: replace the big fetch-everything useEffect + seven useState slices with a single useQuery(['dashboard']); the queryFn returns the same composed data and every query/filter/derivation is unchanged. Manual refresh after "generate invoices" now calls refetch(). Behavior and rendered output are identical. Verified in the running app: login, dashboard renders (stat cards, activity, setup wizard branch) with no query errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Continue the TanStack rollout from Dashboard to the two simplest high-traffic list pages. Both are read-only: extract the fetch into a module-level queryFn and replace useState+useEffect with useQuery(['clients']) / useQuery(['jobs']). Client-side search filtering and all rendering are unchanged. Verified both render in the running app with no query errors. Invoices is intentionally left for a separate pass — it has bulk-mutation actions (status update, send) that warrant useMutation + invalidation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Continue the rollout to the remaining read-only list pages, following the Clients/Jobs pattern: module-level queryFn + useQuery, local UI state and rendering unchanged. Locations composes its multi-step fetch (locations + per-location counts) into one queryFn. Vendors was intentionally skipped — it has an inline insert action that needs the useMutation pattern instead. Verified Locations and Assets render in the running app with no query errors. Build passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add src/lib/dates.ts local-date helpers and replace every
toISOString().split('T')[0] call site; date defaults and derived
dates (due dates, start-of-month, rental billing) no longer resolve
to yesterday in UTC+ timezones
- Setup wizard now previews and saves invoice numbers with the exact
server formula (prefix includes separator, 5-digit padding), so the
first invoice matches the wizard's promise
- New-account form sends billable defaults explicitly so the saved
record matches what the toggles displayed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Extract the Record Payment dialog into RecordPaymentDialog.tsx (self-fetching, optional preselected invoice, form reset on close) - Invoice page: one-click "Mark as Sent" on drafts and "Record Payment" on payable invoices, refreshing totals on success - Payment dialog shows an explanatory empty state instead of a silently empty invoice dropdown, with submit disabled - Rename ambiguous Payments-page buttons to "Receive Payment" / "Pay Vendor"; hide Void status on new invoices; fix stat-card pluralization Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ones - parseSelect now keeps nested relation tokens (e.g. invoices(invoice_number, clients(name))) and attachRelations resolves them recursively, capped at depth 3 - Complete relationMappings for every relation the frontend requests (payments->invoices, issues->clients, user_roles->roles, expenses-> vendors, issue/kb link tables, inventory_movements, job_assignments) - Add issue_bookmark_links to the table allowlist; requests to it previously failed with 400 Invalid table - Unknown relations now return 400 with a precise message instead of silently omitting the key from the response - Add server/__tests__/relations.test.ts covering nested joins, hasMany nesting, and the 400 paths (32 tests total pass) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Outstanding-invoice count/amount and open-issue count were derived from the same limit(10) queries that feed the display lists, so the headline numbers were wrong past ten rows. Counts now use head+count queries and the outstanding amount sums a narrow no-limit projection; the display lists are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add ConfirmProvider/useConfirm (promise-based, queue-safe, destructive styling, multi-line descriptions) and migrate all twelve window.confirm call sites plus BankAccountDetail's two inline AlertDialogs to it, so destructive actions get a consistent in-app dialog instead of the browser popup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The nav, client list, client detail, and docs called clients "Accounts" while the rest of the app said "Client". Rename the client-entity copy to Clients everywhere; bank/user/service account wording is untouched. (The invoice form and list finish the rename in the following formatting commit.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Replace 24 hardcoded en-AU/AUD Intl.NumberFormat formatters (list pages, detail pages, dialogs, all reports) with useBranding()'s formatCurrency, so the currency chosen in Settings applies everywhere; ActivityDetails threads it into its module-level formatter as a parameter - Add formatDisplayDate to src/lib/dates.ts and use it where tables rendered raw ISO strings (invoice list, payments/purchases tables, job timesheets/expenses/rentals, asset rental history); date inputs keep ISO values - Complete the Client rename in the invoice form label, required-field toast, credit panel, and invoice-list column header Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Sidebar nav is now sectioned (Work / Finance / Operations / Insights / Admin, with Dashboard and Help & Docs ungrouped); section headers hide when permissions filter out every item in the group - New CommandPalette (cmdk) opens via Ctrl/Cmd+K or a Search button at the top of the sidebar: quick actions (gated by write permission), page navigation, and fuzzy search across clients, jobs, invoices, vendors, inventory, assets, issues, KB articles, locations, bank accounts, and team members (each gated by read permission; data fetched lazily on first open) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New contacts and contact_affiliations tables: a contact is a person, affiliated with a client OR a vendor (CHECK-enforced) for a period (end_date NULL = current), with title and primary flag per affiliation. People who change companies keep their identity and history. One-time migration converts client_contacts rows (ids preserved) and leaves the old table as a frozen archive. Shim gets table/relation mappings; integration tests cover nested selects, the change-of-company flow, CHECK rejections, and the migration itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New /contacts list (search across name/email/phone/current org) and /contacts/:id detail with the affiliation timeline: add an affiliation to a client or vendor, end one (person keeps their history), set the primary contact per client - New shared AffiliatedContacts panel (New Person / Link Existing) replaces the client-only ClientContacts component; ClientDetail and the Clients list now read primary contacts from current affiliations - Contacts appear in the sidebar (Work) and the command palette - Docs describe the person-centric model Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate vendor_contacts rows into contacts + vendor affiliations (ids preserved, idempotent via id-overlap guard) and freeze the legacy table like client_contacts. VendorDetail drops the old vendor_contacts tab and gets a single Contacts tab backed by AffiliatedContacts. Migration tests now cover the vendor path and client/vendor coexistence (47 tests total). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SQLite triggers keep clients/vendors contact_name/email/phone mirroring each org's current primary contact (recompute-from-scratch on affiliation insert/update/delete and on contact edits), so invoice emailing and the external API stay correct without code changes. A boot-time recompute backfills pre-trigger rows, and orphaned free-text contact info (no matching affiliation) is first converted into a real contact + primary affiliation - reusing an existing person on a normalized name+email match - so nothing is wiped. Covered by contact-sync and orphan-migration test suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Affiliation dialogs take an optional end date (record past stints in one step) and every affiliation row on the contact page has an Edit dialog for title/start/end - clearing the end date makes it current again; start/end order is validated - Overlapping affiliations are explicitly supported: adding an org never ends another, so contractors can be current at several clients/vendors at once - Primary contacts now work for vendors like clients (star, badge, scoped clear-then-set) - ClientDetail and VendorDetail replace free-text contact fields with a PrimaryContactSelector dropdown of currently affiliated people, linked to their contact page with live email/phone Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Below the md breakpoint each list renders tappable cards (identifier + status badge, secondary line, dates, money right-aligned) instead of a horizontally-scrolling table that hid Status and Total off-screen; tables are unchanged on md+. Invoices keeps bulk selection working via a checkbox outside the card's link target; Payments-made cards open the same edit dialog as the desktop row. Also let the Payments header actions wrap - the three buttons overflowed narrow viewports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sweeps the stragglers the original branding pass missed: hardcoded $/toFixed and toLocaleString currency in Assets, ClientDetail, BankAccountDetail, JobHistory, TeamMemberActivity, purchase dialogs, bill import, and the setup tax preview now use useBranding().formatCurrency; raw new Date(...).toLocaleDateString on date-only columns (incl. the Dashboard due-date UTC off-by-one) now go through formatDisplayDate/parseDateOnly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pills show live counts per status (only statuses that occur), combine with the search box, and drive the desktop table, mobile cards, and select-all alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New shared EmptyState component (icon, title, description, action) on all eleven primary list pages. A truly empty list now explains the page and offers the same permission-gated create action as the header; a search/filter with no matches says so and offers one-click clearing (resetting the status pills too on Invoices, type filter on Locations). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shared src/lib/csv.ts (escaping, CRLF, UTF-8 BOM for Excel, blob download) plus an Export CSV button on all eleven reports. Exports the full underlying rows - raw numbers and YYYY-MM-DD dates, not display strings - including rows the UI truncates, with date-ranged filenames. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
next-themes ThemeProvider (class attribute, system default) wraps the app; a Light/Dark/System dropdown lives in the sidebar footer. Swept feature components for light-only colors: gray tints now use semantic tokens, status hues gained dark: variants, and report chart grids use the border token instead of #ccc. Auth pages and the setup wizard were already token-based. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pdfkit renders real PDFs (server/utils/invoicePdf.ts): paginated line tables, totals with credit/paid/balance, payment details, notes/terms. Single download from the invoice page; bulk download is now ONE request returning one combined PDF instead of N print popups that silently no-op under popup blockers. Currency comes from company settings (validated locale/code, en-AU/AUD fallback) instead of hardcoded AUD. Also fixed while in the area: bulk-email failures now aggregate into a single summary toast instead of one toast per invoice; all invoice and invite email HTML now escapes interpolated data (shared escapeHtml in utils/email.ts); generate-job-invoices no longer derives dates via toISOString (server-side UTC off-by-one); invoice-detail chips gained dark-mode variants. Six new endpoint tests in invoice-pdf.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recharts defaults (grey #666 ticks, white tooltip, light hover band) now use muted-foreground/border/popover tokens; the Time by Job non-billable bar swaps --muted for --muted-foreground so it stays visible on dark cards, and its legend text uses the foreground token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch is the outcome of a full usability review of the app (driven live in the browser, not just read from code), followed by three tiers of fixes and two larger features. 13 commits, each scoped to one concern.
Verified bug fixes
toISOString().split('T')[0], 19 call sites). Newsrc/lib/dates.tslocal-date helpers used everywhere, including derived-date math (due dates, start-of-month, rental billing).INV-0001previewed,INV00001created): wizard now uses the exact server numbering formula.relationMappingscovers everything the frontend requests, unknown relations fail loudly with a 400, andissue_bookmark_linkswas added to the table allowlist (requests to it always 400'd before).Workflow / UX improvements
confirm()calls replaced with a promise-baseduseConfirm()AlertDialog provider (queue-safe, destructive styling, multi-line descriptions).en-AU/AUD formatters replaced withuseBranding().formatCurrency(verified by flipping Settings to GBP and watching lists re-render); tables show localized dates instead of raw ISO strings.Person-centric contacts (new feature)
People change companies — contacts are now first-class entities:
contacts+contact_affiliationstables: a contact is affiliated with a client or vendor (CHECK-enforced) for a period (end_date NULL= current), with per-org title and primary flag. Concurrent affiliations are supported (contractors can be current at several orgs), stints can be back-dated or ended, and every affiliation's title/dates are editable — clearing an end date makes it current again./contactslist (search by name/email/phone/current org) and/contacts/:idwith the full affiliation timeline; contacts searchable from the command palette.AffiliatedContactspanel (New Person / Link Existing) and aPrimaryContactSelectordropdown replacing the old free-text contact fields.client_contacts,vendor_contacts) convert into the new model (ids preserved, idempotent) and remain as frozen archives; orphaned free-text contact info on clients/vendors is converted into real contacts rather than wiped.contact_name/email/phonecolumns mirroring each org's current primary contact, so invoice emailing and the external API needed no changes.Reviewer notes
npm run test:run,npm run typecheck, andnpm run buildare all green.server/__tests__/backup.test.ts's retention test is occasionally flaky under full-suite parallel load (timing-sensitive, pre-existing); it passes in isolation.🤖 Generated with Claude Code