Commit Graph

297 Commits (6f50799d3c0f713c1a515a2079c71e0e2df6d0db)
 

Author SHA1 Message Date
Thomas Joise 6f50799d3c test(db-resolve): remove duplicated production password from test file
The test never connects to Postgres, it only asserts resolution
behaviour, so the real prod URL had no reason to be duplicated here.
Swapped for a dummy postgres://u:p@h:5432/db throughout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise b1e1420b4c docs: refresh test count (528)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise a0225b2ba3 merge: frontend audit fixes + UI completion (status-line links, tickets guards, dashboard error state)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise f66bab122b fix(scripts): harden apply-ltd-numbers/apply-name-fixes/import-employees DB resolution
Same pre-ship audit fix as apply-onboarding-pipeline.ts and migrate-onboarding-
templates.ts: replace the raw `DATABASE_URL ?? ''` read with the shared
requireDbUrlForEnv() resolver, log the resolved engine/target before mutating,
hard-fail instead of silently opening a throwaway SQLite file in production, and
close the DB handle in a finally block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 0c3f6c2e3a fix(client-360): audit fixes for onboarding status line, tickets, dashboard
Pre-ship audit swarm findings on the Client Detail redesign:

- StatusLine's isPaymentStep now also matches the legacy advance_paid/
  balance_paid keys (the generic fallback template's vocabulary on modules
  with no seeded per-module tail — WHATSAPP, ATM, AMC, ...), so ticking the
  payment step there opens the picker instead of silently stamping a date.
- Every milestone-tick path (plain toggle, payment link, credentials,
  document link) now reloads the client-modules list so the module block
  header, summary table and status select never go stale after a tick.
- A done payment/document milestone step now surfaces its link (linked
  document -> /documents/<id>; linked payment -> date/amount from the
  loaded ledger) instead of showing no evidence the link landed.
- Tickets: guard TICKET_STATUS_LABEL lookups against non-status chip
  values (e.g. the synthetic "overdue" chip) that were throwing and
  blanking the whole console.
- Dashboard: distinguish a failed "Onboarding stalled" fetch from a
  genuine empty result instead of reporting "Nothing stalled." on error.
- Client 360 tickets tab: add the Type column, same rendering as the
  Tickets desk.
- Drop the redundant read-only status badge next to the editable
  ClientModuleStatusSelect in the module block header.
- Only mount NewTicketDialog while actually open, so it stops firing
  getModules/getTicketTypes on every Client 360 load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 9457b0f145 fix(milestones): fresh projects false-flagged as stalled, fallback modules stuck at quoted, bulk ops skipped status/link logic (FIX D, E, F)
FIX D: stalledProjects treated a no-tick project's missing done_on as the
sentinel '0000-00-00', always older than any cutoff, so every
freshly-assigned project immediately showed up on the "Onboarding stalled"
tile. It's now measured against client_module.created_at (stamped by
assignModule); when that's also unknown (predates the column, or an import
path that doesn't stamp it) the project is excluded rather than assumed
ancient.

FIX E: STEP_STATUS only mapped the new template's keys, but TAIL_FALLBACK
(any module with no seeded/owner-configured tail -- WHATSAPP, ATM, AMC, ...)
still uses the legacy 'installed' key, so ticking "Installation done" on
those modules left status at 'quoted' forever. Added installed->installed
to STEP_STATUS (checked: no legacy payment key needs a mapping, since
payment never drove status even in the new vocabulary).

FIX F: bulkSetMilestone did a bare UPDATE, so a bulk go_live never advanced
client_module.status and a bulk untick never cleared a stale
payment_id/document_id link. Each id now routes through setMilestone itself
(same status-advance + link-clearing path a single tick uses), inside the
existing transaction -- nested transaction() calls become savepoints on
both engines, so a project without the milestone key is caught and skipped
without losing "bulk ops only touch projects that have the milestone".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise e8baa1111e fix(onboarding-migration): deterministic survivor when two old ticks collapse onto one key (FIX C)
advance_paid/balance_paid (and the interim advance_payment/balance_payment)
all collapse onto the single new payment_received key, and the carry used
Map.set -- whichever row was visited second silently overwrote the first's
done_on/payment_id, counted as neither carried nor dropped, with the audit
`before` left `undefined` so the loss was unreconstructible.

pickSurvivingTick() now picks deterministically (prefers the tick with a
real linked payment_id; otherwise the later done_on), the losing tick is
counted in a new `collapsed` field, and the audit `before` payload is now
the full pre-migration row set instead of `undefined` -- any discarded tick
is reconstructible from the append-only log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 04aef0aa36 fix(api): validate ?days= and guard the onboarding-project routes against a crash (FIX B)
GET /projects/stalled had no try/catch, and a caller-supplied ?days= flowed
straight into `new Date(...).toISOString()`. Confirmed the exact crash:
Number('1e300') passes Number.isFinite (so the old guard let it through),
but the resulting cutoff math overflows the valid Date range and
.toISOString() throws RangeError. Uncaught in an async Express handler this
is an unhandled rejection, which kills the whole process on Node 22 -- any
authenticated role, including plain staff, could take the console down with
one GET.

`days` is now validated (integer, 0-3650) with an explicit 4xx on anything
hostile, falling back to the configured default only when the param is
absent (matches the frontend, which never sends ?days= at all). Audited
every other route this feature added to the same section: /projects/board
and /projects/pending/:key had the identical no-try/catch shape and are now
wrapped too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 31335cd4ad fix(deploy): one shared DB-engine resolver so scripts can't silently target a throwaway SQLite file (FIX A)
apply-onboarding-pipeline.ts and migrate-onboarding-templates.ts resolved the
engine as `DATABASE_URL ?? ''`, diverging from server.ts's production
fallback. On the prod box (DATABASE_URL unset) each script opened a brand-new
empty SQLite file, "succeeded" against it, and Postgres was never touched.

resolveDbUrl()/requireDbUrlForEnv()/pgTargetDescription() now live in
db-pg.ts as the one place this is decided; server.ts and both scripts call
it. The scripts additionally hard-fail (never fall back to SQLite) when
NODE_ENV=production resolves nothing, log their target engine before doing
any work, and close the DB handle in a finally.

Also lands client_module.created_at (nullable, additive) — schema groundwork
FIX D uses to measure a no-tick project's age from its real creation date
instead of treating a missing done_on as infinitely old.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 0bcbf8d410 fix(responsive): stop desktop tables and mobile rows from silently clipping content
Red-team pass on the redesigned Client 360 / Tickets / Dashboard / Modules screens found
real overlap/clipping bugs, all fixed:

- Client header still broke at 768px (tablet): the button cluster only stacked below the
  name under 640px, so at 768 the name got squeezed to ~38px and force-wrapped mid-word.
  Bumped the stacking breakpoint to 860px.
- Per-module Documents rows and the Payments Outstanding-panel rows were plain nowrap flex
  rows with no scroll; at 375px the amount/status badge silently clipped past the card
  edge with no way to reach them. Both now wrap.
- Biggest one: every DataTable silently lost its rightmost columns at desktop width
  (>=901px) — table.wf's display:block/table toggle assumed overflow-x:auto still worked
  once display flipped to table, but Chromium computes overflow-x as visible on a
  display:table box, so wide tables (Tickets' 11 columns) overflowed the viewport with no
  scrollbar. Fixed at the root: DataTable now wraps its <table> in a .wf-table-wrap scroll
  container instead of relying on the table element to scroll itself.

Verified against screenshots at 375/768/1440, light+dark, before and after; typecheck and
the full test suite (498 passed) stay clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise b146de990f fix(scripts): audit + guard apply-onboarding-pipeline's setting writes
Final whole-branch review FIX 2.

The live-deploy script upserted the onboarding template settings with raw
SQL (ON CONFLICT DO UPDATE), bypassing setSetting -> no audit row, violating
the append-only-audit rule -- and it unconditionally overwrote templates an
owner may have already edited in the Modules onboarding-template editor,
making the "Idempotent" header comment false and dangerous for a script that
runs against ~167 real projects.

Upserts now go through setSetting(db, 'system', key, value) (audited), and
only write a key that is still absent, unless run with an explicit --force
argument (documented). Header comment corrected to say idempotency holds
only while the templates haven't been owner-edited, and that --force
overwrites unconditionally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 4ee8f17b37 fix(onboarding-migration): preserve ad-hoc custom steps across the template swap
Final whole-branch review FIX 3.

migrateOnboardingTemplates previously DELETEd every project_milestone row and
reinserted only template keys, so a project-specific step added via
addProjectMilestone ("+ Add step", custom_* keys) vanished with no trace if
un-ticked, or was miscounted as `dropped` if ticked.

Old rows are now partitioned before the DELETE into template-mapped vs
ad-hoc custom (not a template key, not in KEY_MAP, and not a known old
generic FRONT_FALLBACK/TAIL_FALLBACK key like amc_start — those keep their
existing drop behavior). Custom rows are re-inserted unchanged after the
template block, sort continuing past it, preserving
label/done/done_on/payment_id/document_id. The return type and the audit
`after` payload both gained a `preserved` count/list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise e8e2a202c1 fix(milestones): reject front/tail key collisions; dedupe on collision; stamp installed/trained dates
Final whole-branch review FIX 1 + FIX 5.

- setFrontTemplate / setModuleTail now reject a key that collides across the
  front/tail halves (checked against the default tail, every configured
  per-module tail, and the shared front respectively) instead of letting
  milestoneTemplate silently compose a duplicate-key list.
- ensureProjectMilestones adds each inserted key to its own `existing` set
  inside the loop, so a template that still ends up with a duplicate key
  (a pre-existing bad setting) is deduped instead of hitting the
  UNIQUE (client_module_id, key) violation a second time.
- GET /client-modules/:id/milestones now wraps ensureProjectMilestones +
  listProjectMilestones in try/catch -> 400, matching its POST sibling,
  instead of an unhandled async rejection that hung the request.
- advanceStatusForStep now stamps client_module.installed_on / trained_on
  from the triggering milestone's own done_on the first time status
  advances into 'installed' / 'trained' (never overwriting an existing
  date), replacing the UI date-writers the Client Detail redesign removed
  without a replacement. Reflected in the same audit row as the status
  change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 7494f53e2f fix(client-detail): payment step gate matches shipped key payment_received
The status-line payment picker only fired for advance_payment/balance_payment,
which no shipped template contains (the composed tails and the migration both
standardise on payment_received) — so payment linking was unreachable and
project_milestone.payment_id was never written by the UI. Keeps the legacy keys
for projects not yet migrated. Also refreshes the stale STATUS test count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise ac16d8c896 merge: frontend — quotation-step dialog, ticket types + deep-links, cleanup
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 3ac4e2a47d merge: backend — quotation-step document link + ticket classification
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 7838ced6c2 feat(client-detail): quotation-step document link + ticket classification (backend)
Backend half of the Client Detail redesign follow-ups (#7, #6, #4).

#7 — quotation step links its document
- project_milestone gains a nullable `document_id`, landed on BOTH engines:
  a PRAGMA-guarded ALTER in db.ts migrate() and pg migration 016.
- setMilestone(..., doneOn?, paymentId?, documentId?) validates the document
  belongs to the same client as the project (mirroring the paymentId check),
  stores it on tick and clears it on untick; Milestone gains `documentId`.
- POST /client-modules/:id/milestones accepts `documentId`.
- migrateOnboardingTemplates carries document_id across the template swap so a
  linked quotation is not lost.

#6 — ticket classification
- ticket gains `type TEXT NOT NULL DEFAULT 'service'` (db.ts SCHEMA + guarded
  ALTER, pg migration 017); imported APEX rows fall to the default.
- Config over code: the list is the `ticket.types` setting (seeded on first
  boot) resolved by ticketTypes(), with TICKET_TYPE_FALLBACK in code.
- GET /tickets/types; create/update accept + validate `type`; list rows carry
  it and GET /tickets?type= filters (blank/omitted = every type).

#4 — cleanup
- Refreshed the stale file comment atop repos-milestones.ts and the
  project_milestone comment in db.ts: both now describe the composed
  front + per-module tail model (they still described the old flat template).
- Migration tests for the interim keys advance_payment / balance_payment ->
  payment_received, plus the document-link carry.

npm run typecheck clean; npm test 473 passed / 5 skipped (was 453/5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 357735e04d feat(client-detail): quotation step links a document; tickets get a type
Frontend half of two Client-Detail follow-ups, plus the StatusLine doc refresh.

Quotation step -> document link:
- Milestone gains documentId; setMilestone threads an optional trailing
  documentId (contract: doneOn, paymentId, documentId).
- Ticking `quotation_sent` on the status line now opens DocumentPickerDialog
  instead of stamping a bare date. It mirrors PaymentPickerDialog exactly —
  same state shape in ClientDetail, same Dialog props — and lists the client's
  QUOTATION/PROFORMA documents straight from the already-loaded ledger (no
  extra fetch), with "Create new document…" handing off to the composer.

Ticket classification:
- Ticket gains `type`; getTicketTypes() reads the `ticket.types` setting
  (config over code) with a code fallback only for the load window.
- New-ticket dialog gets a Type select; Tickets page gets Type filter chips
  wired to the ?type= list param and a Type column.
- Every ticket's module cell deep-links to /clients/<id>?tab=modules, and each
  Client 360 module block gets "Raise ticket for this module", which opens the
  new-ticket dialog pre-filled with that client + module code.

Also refreshes the now-stale StatusLine JSDoc so it describes all three
hand-off dialogs — payment, credentials (account_creation) and document.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 88a19f154e chore(client-detail): drop the dead .cdr-out .ofoot CSS rule
The outstanding panel never rendered a footer row — the rule has had no
matching markup since the panel landed. Removing it keeps app.css honest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 83633c86ee feat(dashboard): add Onboarding stalled tile
Surfaces stalledProjects() on the Dashboard bento grid, listing active
projects parked past the stall threshold; each row jumps to the
client's Modules tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise d6faf0d5bd merge: Client Detail redesign (D38) — composed onboarding pipeline, status line, payments context, mobile fixes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

# Conflicts:
#	STATUS.md
#	docs/06-DECISIONS.md
1 day ago
Thomas Joise 4e747427d2 feat(onboarding): owner editor for shared front + per-module tail templates
Adds owner-only, audited routes for the two config settings milestoneTemplate
composes (project.milestone_template.front and project.milestone_template:<CODE>),
plus a Modules page section to edit them: staged label rows with add/remove/
reorder/Save, keys auto-derived from labels when blank, uniqueness enforced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 45cfb83d03 chore(deploy): apply prod DB URL + onboarding-pipeline deploy script
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 371cdb1c69 fix(ui): drop redundant table-scroll CSS, rely on existing app.css mechanism
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise db8ec9c546 feat(client-detail): prompt for module credentials on account_creation tick
Ticking the "Account creation & registration" onboarding step now opens a
small credentials dialog (mirrors the existing payment-step picker) instead
of a bare toggle. Saving reuses ServiceDataCard's exact patchClientModule
call and managerial password gate, then marks the milestone done; "Skip"
marks it done without touching credentials so the step never hard-blocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 0050f73a70 fix(ui): stack record-header actions and scroll wide tables on mobile
RecordHeader's action-button cluster crowded/overlapped the client name on
narrow phones, and DataTable (Tickets, etc.) ran wider than the viewport,
clipping columns with no way to reach them.

Add a <=640px media query: .wf-rec wraps so .wf-rec-actions drops to its
own full-width row below the avatar/name/meta; desktop layout is untouched
outside the query. Wrap DataTable's <table> in a new .wf-table-wrap div
that scrolls horizontally on overflow at that breakpoint (nowrap cells so
the table can exceed 100% instead of being squeezed illegibly), keeping
the page body itself from scrolling sideways.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 025b334b09 feat(client-detail): drop mandatory assign creds, add per-module status control, trim summary table
- AssignModuleForm: remove SMS-forced username/password gate at assign time
  (creds now captured later via module Access -> Edit); reword to "Add a
  module" with a one-line hint on what assigning starts.
- Modules summary table: drop Installed/Completed/Trained columns (dates
  live in the per-module status line); keep Module/Kind/Status/Billed/Settled.
- Each module's collapsible block now has an inline ClientModuleStatusSelect
  next to its status badge, so status is editable without opening the table.
- Payments tab: remove the Outstanding panel's duplicate "Advance on
  account" footer line -- the pre-existing StatCard is the single source.
- Remove RowDate, now unused after the summary-table trim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 78ed947422 feat(payments): outstanding panel + settled-to-document column
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise f1a7eac9dd feat(onboarding): composed milestone templates (shared front + per-module tail)
Supersedes the flat per-module onboarding templates: milestoneTemplate(db, code)
now returns [...front, ...tail]. front is a shared setting
(project.milestone_template.front, FRONT_FALLBACK code default: enquiry ->
visit/meeting -> quotation sent -> quotation approved) prepended to every
module's checklist. tail resolves per-module setting -> global setting ->
TAIL_FALLBACK (the old generic list), unchanged in precedence.

- STEP_STATUS gains quotation_approved -> ordered (never-downgrade guard kept).
- seed.ts seeds the front once plus SMS/RTGS/MOBILEAPP tails (advance/balance
  payment steps collapsed into a single payment_received tail step); no longer
  seeds a bare project.milestone_template default.
- migrate-onboarding-templates.ts KEY_MAP updated so advance_paid, balance_paid,
  advance_payment and balance_payment all carry forward to payment_received;
  its idempotency check holds against the longer composed list.
- Updated seed-templates, milestones-templates, milestone-status,
  migrate-onboarding-templates, milestones and milestone-payment tests to the
  composed content/mappings (TDD: reverted the implementation, confirmed the
  updated tests red, restored the implementation, confirmed green).

Backend + tests only; the live-DB migration run is a separate step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise e8f00503a4 feat(client-detail): per-module documents list + bulk SMS purchase action
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise e5ed3517dc feat(client-detail): onboarding as horizontal status line with payment-step linking
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 343fa94efd docs(client-detail): D38 redesign decision + STATUS in-progress note
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 8c653eb41a fix(payments): exclude draft proformas from outstanding (owner decision)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise a9e5aa79d9 docs: bring README/STATUS/CLAUDE/DECISIONS current (D34-D37)
Records the work since the last doc pass: employee master import + invited/first-login
flag (D34), client type PACS/Store + directory-sourced LTD numbers and the name-correction
session (D35), login history with IP/device (D36), and the society category matching the
Kerala co-op directory taxonomy (D37). Adds the login_event table, the Login history
screen, and the client type/category fields; bumps the schema table count 31->32. Decisions
D34-D37 appended to 06-DECISIONS.md.
2 days ago
Thomas Joise 9d3087c1fe feat(client-detail): module details as 50/50 report with single edit per group
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise d05c7ee31b docs: bring README/STATUS/CLAUDE/DECISIONS current (D34-D37)
Records the work since the last doc pass: employee master import + invited/first-login
flag (D34), client type PACS/Store + directory-sourced LTD numbers and the name-correction
session (D35), login history with IP/device (D36), and the society category matching the
Kerala co-op directory taxonomy (D37). Adds the login_event table, the Login history
screen, and the client type/category fields; bumps the schema table count 31->32. Decisions
D34-D37 appended to 06-DECISIONS.md.
2 days ago
Thomas Joise 67f69fc999 style(client-detail): CSS for 50/50 sheet, status line, outstanding panel
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 75fb41c366 feat(onboarding): stalled-project query for the dashboard/MIS tile
Adds stalledProjects(db, olderThanDays, today) to repos-milestones.ts:
active projects with a pending step whose last progress (MAX done_on,
or module creation if nothing ticked) is older than N days. GET
/projects/stalled reads ?days= with a project.stall_days setting
fallback (default 30).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 195d821bef feat(sms): bulk SMS purchase proforma + document source tagging (renewal vs top-up)
Threads an optional `source` through DraftInput/insertDocRow (default 'hq',
preserving current behaviour when omitted), tags module renewals as
'module_renewal', and adds generateBulkSmsPurchaseQuote — an ad-hoc bulk SMS
credit top-up priced via the existing usage rate card, tagged
'bulk_sms_topup' so the client-detail document list can distinguish it from a
subscription renewal. New route: POST /client-modules/:id/bulk-sms-quote.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise fdf31b4dbd feat(payments): ledger exposes outstanding docs + per-payment allocations
clientLedger now attaches allocations (docNo + amount) to each payment and
returns an outstanding[] list — issued invoices with outstandingPaise > 0
plus open (draft/sent/accepted) proformas, oldest-first by docNo. Read-only
extension over the existing settlement math (outstandingPaise, allocations);
no money/allocation logic changed. Mirrors the widened Payment/Ledger shapes
onto apps/hq-web/src/api.ts for the upcoming Payments tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 4b2e44c3ea test(onboarding-migration): add coverage for dropped ticked milestone keys
Add one test case to cover the scenario where old milestone keys that
don't map to the new template (amc_start, setup_done for SMS) are ticked
and should be counted in the migration's dropped tally. Verifies both
dropped and carried counts are accurate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 6bce248c2e feat(onboarding): one-time migration swapping seeded modules to new templates (ticks preserved)
Idempotent migrateOnboardingTemplates(db): for every client_module whose module
has a per-module onboarding template (task 2), rebuilds its milestone rows from
that template, carrying mapped ticks (installed->installation, go_live->go_live,
advance_paid->advance_payment, balance_paid->balance_payment, setup_done->
setup_configuration when present) with done_on and payment_id intact. Unmapped
ticks (amc_start, or setup_done where the new template has no matching step) are
dropped and counted. Audited per project; a second run is a no-op via an
identical-key-set short-circuit. Adds the CLI wrapper (mirrors
apply-name-fixes.ts's open-DB idiom, Postgres or SQLite) for the real one-time
run against production data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise cd235b622b feat(onboarding): link advance/balance payment steps to real payments
Milestone.paymentId + project_milestone.payment_id (SQLite + Postgres,
additive). setMilestone(..., paymentId?) resolves the payment's
received_on to stamp done_on when a payment-step tick links a real
payment; the linked payment must belong to the same client. api.ts
threads paymentId from the milestone POST body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise b2c49832d5 feat(onboarding): status line auto-drives client_module.status (never downgrades)
Ticking the installation/training/go_live onboarding milestone steps now
advances the coarse client_module.status (installed/trained/live), audited
in the same transaction. A STATUS_RANK guard blocks downgrades from
unticking or ticking an earlier step after a later one already landed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 9ac2ce9d50 feat(onboarding): seed SMS/RTGS/MobileApp onboarding templates
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 1e77792beb feat(onboarding): resolve milestone template per module (config over code)
Widens milestoneTemplate(db, moduleCode?) to resolve project.milestone_template:<CODE>
before the global project.milestone_template setting, falling back to FALLBACK_TEMPLATE.
ensureProjectMilestones now looks up the client_module's module code and resolves the
template through it, so each module can carry its own onboarding checklist as a dated
setting row instead of one hardcoded global list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 6bc92bace3 docs(client-detail): redesign spec + backend/frontend implementation plans
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 9cb5127faf feat(d37): society category (Kerala co-op directory taxonomy) on clients
Adds client.category (SQLite migrate + PG migration 014), derived from the name to match
the official Kerala co-op classification: Service Bank / Urban Bank / Vanitha / Housing /
Employees / Agricultural / Marketing / Society (other) / Store / Other. Backfilled by a
portable CASE (most-specific-first); new/edited clients derive it via categoryFromName.
Clients screen gains a Category filter dropdown + column. Live backfill: 135 Service
Bank, 38 Employees, 15 Vanitha, 14 Store, 12 Agricultural, 11 Urban, … typecheck + full
suite (416) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 1c375fdf01 chore(d35): script to append directory-sourced LTD numbers to client names
Appends ' LTD NO. <n>' where the Kerala co-op directory lookup found a registration
number (found=true). Guarded: only found rows, skips names that already contain the
number (space/punct-insensitive), appends the number only (no name/casing overwrite),
audited via updateClient. Applied on live data after backup: 91 names updated (K-Z half).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 809976fd8b feat(d36): login history (IP + device) + first-login change nudge
Captures every /auth/login attempt in a login_event table (SQLite schema + PG migration
013): staff_id (or null for an unknown user), username tried, outcome (success/failed/
locked), IP, and user-agent — best-effort, never breaks login. New owner 'Login history'
page (Admin): who signed in / failed, when, from which IP + parsed device, outcome filter,
paginated. login() now returns mustChangePassword; an invited user on their default
password is sent to Profile to change it on sign-in. typecheck + auth/employee tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago