Commit Graph

310 Commits (551c9e3d30adbf0957bfc1927affb46877f6fe38)
 

Author SHA1 Message Date
Thomas Joise 551c9e3d30 merge: readable linked payment/document chip + SMS alert loading/error state 1 day ago
Thomas Joise a267cd07e6 merge: readable linked payment/document chip + SMS alert loading/error state
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

# Conflicts:
#	apps/hq-web/src/pages/Dashboard.tsx
1 day ago
Thomas Joise 23b0b64de2 merge: script DATABASE_URL docs + SQLite opt-in, always-stamp install/train dates, keep links on re-tick, backup resolves prod DB
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 0adce5844b fix(backup): resolve the production database the way the server does
`npm run backup` ran scripts/backup.mjs, which read process.env.DATABASE_URL
directly and exited when it was unset. On the production box DATABASE_URL is NOT
exported -- the connection is resolved (D19) -- so the backup command could never
reach the real data.

The dump now lives in apps/hq (src/backup.ts + a thin scripts/backup.ts CLI, the
same split the other maintenance scripts use, so it is typechecked and unit
tested) and resolves its target through the shared resolveDbUrl. It logs the
resolved host/db -- never the password -- before pg_dump runs, and a SQLite
outcome is a loud error naming backup-hq.sh instead of a dump of nothing.
Behaviour otherwise unchanged: -Fc dump under ./backups (or HQ_BACKUP_DIR),
rotation to the newest HQ_BACKUP_KEEP (default 14), same pg_dump discovery.

The .mjs entry could not import the TS resolver, and duplicating the resolution
would have re-introduced the copy of the production connection string that
6f50799 deliberately removed -- hence the move to TS. `npm run backup` is
unchanged for the operator; prod already runs `npx tsx apps/hq/scripts/...` for
maintenance (see DEPLOY-HQ.md), so tsx is on the box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 4c1514c315 fix(milestones): stamp installed_on/trained_on on every tick, keep links on a re-tick
Two bugs in the same tick path.

1. installed_on / trained_on were written only as a side effect of a status
   UPGRADE. The ~300 APEX-imported projects are already at 'live' -- the furthest
   status -- so ticking Installation or Training upgraded nothing and therefore
   recorded nothing, and the UI's own date writers were removed in the Client
   Detail redesign, leaving those columns unfillable and the Module Directory's
   "Installed" column blank forever. The date is now stamped whenever the step is
   ticked, independent of any status move: same transaction, same audit payload,
   still write-once (an existing date is never overwritten) and still no downgrade.
   A tick that changes neither status nor a date now writes nothing at all.

2. setMilestone wrote payment_id/document_id as `done ? linked : null`, so any tick
   that did not resend the ids cleared them. bulkSetMilestone routes through it and
   never passes ids (one key, many projects), so a single bulk re-tick silently cut
   every project's step loose from the payment or quotation it had been linked to.
   An already-done step now keeps the link it carries when nothing is supplied.
   An explicit empty id is still an unlink, and unticking still clears both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 77f135f0e2 fix(scripts): document the DATABASE_URL-prefixed invocation, make SQLite an explicit opt-in
apply-onboarding-pipeline.ts documented its own command with no `DATABASE_URL=`
prefix -- the only hardened data script missing it. Followed literally on the
production host it resolves to SQLite, not the real database. Header corrected to
match its siblings.

The deeper problem is that landing on SQLite was a DEFAULT, not a decision: with no
DATABASE_URL, resolution quietly returned '' and the run was only saved by the
"never CREATE a database" guard -- which passes as soon as any stale ./data/hq.db
exists under the operator shell's cwd. requireDbUrlForEnv now refuses a SQLite
outcome unless it was explicitly asked for (HQ_DB_TARGET=sqlite, a --sqlite flag,
or HQ_DATA_DIR=:memory:), and the error names both escape hatches. The create guard
is unchanged and still applies after the opt-in. server.ts calls resolveDbUrl
directly, so first-boot DB creation is untouched.

All five maintenance-script headers now state the SQLite opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 367fc0324e fix(dashboard): never report "all good" when the fetch failed
The SMS low-balance alert rendered only when the request succeeded with
lowCount > 0, so a FAILED check looked exactly like "no client is low" --
the dangerous direction. It now has three distinct states: a skeleton
while the check is in flight, an ErrorState with retry when it fails, and
a quiet positive caption only for a loaded, genuinely-empty result (which
also spells out `unknownCount`, since balances the gateway could not
return are not "fine" either).

Same three-state audit on the rest of the page: the "Onboarding stalled"
tile had the error case fixed already but still showed count 0 and
"Nothing stalled." while its fetch was in flight -- it now takes a
`loading` prop (new on Card) and shows a skeleton, and its count appears
only once the list has loaded. Every other bento card is fed by the one
dashboard payload, whose loading/error/empty states are already
distinguished at page level.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise b32f7c32c8 fix(client-detail): make the linked payment/document reference legible
The status-line reference under a done step was `white-space: nowrap` +
`overflow: hidden` inside a 104px node, so the linked payment's amount --
the only reason the chip exists -- was ellipsised away, and the `title`
that could have revealed it held static text instead.

- the reference now wraps (up to two short lines) rather than clipping;
  it grows the node downwards only, so the track keeps its 112px columns
  and its horizontal scroll on narrow screens is unchanged
- `title` + `aria-label` now carry the real detail: payment date, amount
  and reference / document type, number, date and payable
- the document chip (quotation_sent), which clipped the same way, is
  resolved against the ledger too and shows the document number instead
  of the static "view document ->"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 1717900e02 merge: restore sticky table headers (bounded 70vh wrapper), dashboard loading state, ticket-type over-fetch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise aa541270d6 fix(onboarding): APEX-imported projects were invisible on the stalled tile
client_module.created_at was added and stamped in assignModule, and
stalledProjects now correctly excludes no-tick projects whose created_at
is NULL — but the APEX importer (the path that created the real ~300
clients' projects) inserted client_module WITHOUT created_at. Every
imported project with zero ticks was therefore permanently invisible on
the "Onboarding stalled" tile, exactly the population it exists for.

Three parts:
- Stamp created_at in the APEX importer's client_module insert.
- Backfill existing NULL rows from installed_on where known (both
  db.ts migrate() for SQLite and migrations-pg.ts for Postgres); never
  fabricate a date when installed_on is also unknown.
- stalledProjects now returns { rows, unknownAgeExcluded } instead of a
  bare array, so projects excluded for unprovable age are counted, not
  silently dropped. GET /projects/stalled responds with
  { ok, projects, unknownAgeExcluded }; UI wiring left to the parallel
  agent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 4de152a7f4 fix(ui): restore sticky table headers, dashboard loading flash, ticket-type over-fetch
- packages/ui/src/tokens.css: bound .wf-table-wrap to a viewport-derived max-height
  and matching overflow-y so it's a real scroll container again. overflow-x: auto
  alone left overflow-y at its computed "auto" (per the CSS overflow spec, a
  visible/non-visible axis pairing promotes visible to auto) with no height bound,
  so its scrollTop was permanently 0 and table.wf th's `position: sticky` never had
  anything to stick against — the header just scrolled away with the page on every
  table screen. Chose the bounded-wrapper fix (over deleting the dead sticky rule)
  since it keeps the header pinned as advertised; short tables render unchanged,
  long ones scroll inside the box exactly like the existing .dash-list pattern.
  Also moved the .wf-table-wrap rule out of apps/hq-web/src/app.css into tokens.css
  next to the other table.wf rules, since it's part of the @sims/ui component.
- apps/hq-web/src/pages/Dashboard.tsx: the "Onboarding stalled" card treated
  "still loading" the same as "genuinely empty", flashing "Nothing stalled."
  before the fetch resolved. Card now takes a loading prop and shows the usual
  skeleton placeholder while pending; the positive empty copy and error state
  are unchanged.
- apps/hq-web/src/pages/ClientDetail.tsx: getTicketTypes was fetched unconditionally
  on every Client 360 load for the tickets tab's Type column. Gated it behind the
  tickets tab being active (moved the tab computation earlier so it's available to
  gate the loader); NewTicketDialog's own getTicketTypes call for the new-ticket
  pre-fill is untouched.
- STATUS.md: prose under the verification table still said "The 416 figure..." while
  the table itself says 528 — updated the prose to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 591d0e9b59 fix(onboarding-migration): idempotency check must ignore ad-hoc custom_* rows
alreadyNew was set false for EVERY row whose key wasn't a template key,
including custom_* rows that the same function deliberately preserves
verbatim. So a project with even one ad-hoc "+ Add step" entry was
never "identical" and got DELETE-and-rebuilt on every single run (new
row ids, a fresh audit row each time) — contradicting the documented
"safe to re-run / a second run does nothing".

Idempotency is now computed over non-custom rows only, using the same
custom_* discriminator addProjectMilestone's key generation relies on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise ee0367dfb5 fix(db-pg): make requireDbUrlForEnv guard outcome-based, not NODE_ENV-gated
The guard only threw when NODE_ENV==='production' AND resolution came
back empty. But NODE_ENV is exactly what isn't reliably set for the
maintenance scripts this guards (they're run by hand from an operator
shell with no service-definition env and no dotenv), so on the real
prod box the guard never fired: resolution silently returned '', the
script opened a brand-new empty SQLite file under <cwd>/data, and
printed a false "0 project(s) rebuilt" success.

Now the guard fires on the OUTCOME instead: whenever resolution
selects SQLite, refuse to run unless the target file already exists —
a migration/maintenance script must never CREATE a database. Applies
only to requireDbUrlForEnv (the script entry path); server.ts calls
resolveDbUrl directly and is untouched, so first boot still creates
its DB as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
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