Visual and information-architecture pass across every screen. No API call, payload, poll, permission rule or workflow mirror changes; forms still render from the live schema and the timeline's merge/grouping is unchanged (only the display order is reversible). Design language - Ground + white cards with one hairline and one radius; sentence-case headings; shared primitives in index.css (.card .pill .btn .avatar .live). - One inline icon set (components/Icon.jsx); no more filled navy/red tiles. - Read-side labels in api/display.js: roles and enum tokens humanized, customer initials/tones, money and date formatters. Nothing sent changes. Shell - 56px header with brand, global lead search (portfolio rows, "/" to focus) and identity chip with a human role label. - Sidebar rows carry icons and a clear active bar; AI roster at the foot, each opening its remit card. Login - Operator copy instead of retail copy; centred sign-in card with show/hide password; demo persona picker that fills the EMAIL only. Overview - "Who holds the work" as four quiet cards that warm only when owed. - Pipeline flow bar with every stage named beneath it; KPI strip in a card. - Action rows with chevron / view-only eye; expiry bands filter the table. Queues - Filter bar (search, product, channel, expiry band, stalled, hide closed) and sortable columns, all client-side on the fetched page. - Avatars, product/channel chips, stage chips for closed leads, tinted stalled rows, count inline with the title. Lead - Avatar + chips in the header; metrics card with the stage rail inside, and a branch node for Referred / Parked / Lost / Declined. - Timeline newest-first by default (toggle), the worker's face as the node, one footer row per entry, AI markdown rendered as bold. - "AI team on this lead" panel with per-agent step counts; PDF tags on docs. Forms & dialogs - Required marked with an asterisk + legend; "Select…" placeholder; dd Mon yyyy hint under date inputs; open activity form in a titled panel; one-line dialog footnotes. Fixes: horizontal overflow at 1024px, boxed "in N days" figure, raw tokens (sme_underwriter, verbal_call, Rsa), findings cut at "1.". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
200 lines
12 KiB
Markdown
200 lines
12 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
npm install
|
|
npm run dev # http://localhost:5175
|
|
npm run build # → dist/
|
|
npm run lint # eslint (flat config, ignores dist/)
|
|
npm run preview
|
|
```
|
|
|
|
There are no tests and no test runner in this project.
|
|
|
|
`npm run lint` currently reports 14 pre-existing errors — 8 `react-hooks/refs`, 3
|
|
`react-hooks/set-state-in-effect`, 3 `react-refresh/only-export-components` (the provider
|
|
and portfolio files export both a component and hooks). A clean run is not the baseline;
|
|
compare against this count rather than against zero.
|
|
|
|
Local dev reads `VITE_ZINO_API_URL` from `.env` (`https://dev.getzino.in`). A deployed
|
|
build does **not** — see "Runtime config" below.
|
|
|
|
## What this is
|
|
|
|
A React + Vite operator console (no state library, no UI framework — plain CSS files
|
|
next to their components) for the Zurich Kotak "Lead to Policy" demo on the Zino
|
|
platform. Leads arrive through three channels — direct, bancassurance, agency — and run
|
|
one workflow state machine to policy issuance. AI employees carry most of it; a person is
|
|
needed at three queues (`Contacted` → upload documents, `Referred to Underwriting` →
|
|
clear or decline, `Payment Pending` → confirm premium) and the customer holds one
|
|
(`Quoted`).
|
|
|
|
The frontend owns **no business logic**. The workflow (org `84`, app `536`, workflow
|
|
`zk_wf_lead`) is the source of truth; this repo is a thin renderer over its API.
|
|
|
|
Workflow config is seeded outside this repo, from `sm2/custom-apps/zurich-kotak/`
|
|
(SQL migrations `02_workflow.sql`, `04_views.sql`, …). Design doc:
|
|
`sm2/app-designs/zurich-kotak/design.html`.
|
|
|
|
## Architecture
|
|
|
|
- `src/api/client.js` — every gateway call lives here (`ZinoClient`). JWT + user in
|
|
`localStorage`, 401 clears the session via an auth-error handler.
|
|
- `src/api/config.js` — the environment identifiers and the hand-maintained mirrors of
|
|
workflow tables (`STAGES`, `ACTIONS`, `ENTRY`).
|
|
- `src/api/provider.jsx` — `ZinoProvider` / `useZino()`: one client instance plus session
|
|
state. Restores an **identity**, never a permission set.
|
|
- `src/api/errors.js` — `describeError()` turns the gateway's five error shapes into a
|
|
sentence plus a `kind` the caller branches on (`stale` re-fetches, `gone` returns to the
|
|
list).
|
|
- `src/layout/Shell.jsx` — 56px header (brand, `GlobalSearch`, `UserMenu`) + the sidebar,
|
|
grouped by `NAV_GROUPS` (who is holding the lead) rather than by pipeline position, with
|
|
queue counts from one tallied list call and the AI roster at its foot.
|
|
- `src/layout/GlobalSearch.jsx` — finds a lead from anywhere. Filters the rows
|
|
`PortfolioProvider` already holds (name, business, ref, mobile, registration) — no request
|
|
of its own. `/` focuses it.
|
|
- `src/api/display.js` — read-side labels only: `roleLabel`, `humanize` (snake_case tokens →
|
|
English; names, refs, emails pass through untouched), `initials`/`toneFor` for customer
|
|
avatars, money and date formatters, `findingOf`/`leadIn`/`mdRuns` for AI prose. Nothing in
|
|
here changes a value that is SENT anywhere.
|
|
- `src/components/Icon.jsx` — the one monoline icon set. Use the NAME, never a path.
|
|
- `src/index.css` — the tokens plus the shared primitives every screen builds from:
|
|
`.card`, `.pill`, `.btn`, `.avatar`, `.live`, `.h-sec`/`.h-sub`. Screen stylesheets style
|
|
content; these style containers. Page ground is `--zk-ground`, bounded regions are white
|
|
cards.
|
|
- `src/screens/Pipeline.jsx` — a queue: one record view filtered server-side on
|
|
`current_state_name`. The filter bar (search, product, channel, expiry band, stalled,
|
|
hide closed) and the column sort narrow the PAGE that came back, client-side; they never
|
|
change the request.
|
|
- `src/screens/Lead.jsx` — one lead: header (avatar, chips, Your move / whose move), a
|
|
metrics card with the `StageRail` inside it, the status strip, Next step, the timeline
|
|
(newest first by default, toggle to read as a story), and a sticky reference rail: the AI
|
|
team with per-agent step counts, then At a glance. The full record is `LeadFileDialog`
|
|
(`GROUPS`, in the order the lead is worked).
|
|
- `src/pages/Login.jsx` — sign-in plus a demo persona picker (the README accounts for org
|
|
84). Picking one fills the EMAIL only and focuses the password box — no credential lives
|
|
in the bundle. What the persona may do is still the workflow's decision.
|
|
- `src/screens/AddLead.jsx` — the INIT doors from `ENTRY`; skips the chooser when only
|
|
one is surfaced.
|
|
- `src/components/ActivityForm.jsx` — renders the **live** activity schema and submits it
|
|
back. `src/components/FileField.jsx` handles `file`/`ocr` fields;
|
|
`src/components/Timeline.jsx` renders the audit trail as a story.
|
|
|
|
Routing: `/stage/:stageUid`, `/lead/:instanceId`, `/add`; unauthenticated renders
|
|
`Login` for every path.
|
|
|
|
## The invariants — break these and it fails in ways tests would not catch
|
|
|
|
**Forms are never defined in this repo.** `POST /view/form-screens` returns labels,
|
|
types, select options and which fields are mandatory; `ActivityForm` renders whatever
|
|
comes back. Adding a field in Studio and redeploying surfaces it here with no frontend
|
|
change. Do not hardcode a form — it would silently drift from the workflow.
|
|
|
|
**Permissions are never enforced here.** Nothing hides a button to stop someone. Every
|
|
decision is the workflow's, server-side, on each submission; the console reports the
|
|
refusal. `/activity` refuses with **403**, `/start` with **400**, both carrying
|
|
`permission denied`. Showing an action the signed-in role cannot perform is deliberate —
|
|
the refusal is the demo.
|
|
|
|
**Runtime config, not build-time.** `VITE_ZINO_API_URL` is read at runtime off
|
|
`window.__RUNTIME_CONFIG__`, from a `config.js` the server writes next to the artifact at
|
|
placement. One artifact is promoted between environments unchanged, so a compiled-in URL
|
|
would point every environment at whichever backend built it. `requireConfigValue` throws
|
|
rather than falling back — a production build with no `config.js` must fail loudly. The
|
|
dev-only `.env` fallback is gated on `import.meta.env.DEV`.
|
|
|
|
**One build serves any mount path.** `vite.config.js` sets `base: './'`; the router
|
|
basename comes from `basePath()`, which reads the `<base href>` the server writes into the
|
|
`<!--BASE_HREF-->` placeholder in `index.html`. Do not reintroduce a build-time base, and
|
|
do not remove that placeholder or the `config.js` script tag. `import.meta.env.BASE_URL`
|
|
is unusable here (it resolves to `./`).
|
|
|
|
**Bundled assets must live in `src/assets/`, not `public/`.** Vite rewrites bundled asset
|
|
URLs to be relative; a `public/` file referenced as `/fonts/…` stays absolute and 404s
|
|
under the `/zurich-kotak/` mount. The favicon is the one exception — it stays in
|
|
`public/brand/` and is referenced relatively.
|
|
|
|
**Activity fields are filtered by product line.** `Collect Documents` serves motor
|
|
and SME from one form — all eleven upload slots plus both lines' risk fields, to every
|
|
lead — and the activity carries no `field_rules` (`[]`), so nothing server-side decides
|
|
visibility. A motor renewal was being asked for a Udyam certificate, a stock statement
|
|
and a GSTIN. `fieldApplies()` in `api/config.js` decides: `motor_*` is motor and `sme_*`
|
|
is SME **by prefix**, so a field added to the workflow tomorrow classifies itself;
|
|
`LINE_FIELDS` names the exceptions that carry no prefix (`gstin`, `udyam_no`), and
|
|
`DOC_SLOTS` maps the upload slots plus the two genuine conditions (`doc_address_proof`
|
|
only when `kyc_outcome === 'refer'`, `doc_financials` only when `sme_sections` includes
|
|
`business_interruption`). `ActivityForm` filters on it for both render and submit.
|
|
|
|
Two things keep this safe. Every slot is `mandatory: false`, so hiding one cannot fail
|
|
schema validation. And when the filter would hide EVERY field, `ActivityForm` renders the
|
|
form whole instead — `Capture Motor Risk` is twelve motor fields and is runnable from
|
|
Contacted whatever the product is, so on an SME lead the filter would otherwise remove
|
|
the activity rather than its noise. `entity_name` is deliberately **not** SME-only: motor
|
|
leads carry a business name too.
|
|
|
|
The prefix rule is inference, not configuration — the durable home is `field_rules` on
|
|
the activity. When those are seeded, read them and delete this.
|
|
|
|
**Nothing is pushed — the console polls.** No socket, no SSE, and most steps are carried
|
|
by an AI employee that takes one to three minutes, so a lead moves while it is on screen.
|
|
The lead detail polls every 12s, queues and the sidebar counts every 30s, all gated on
|
|
`document.visibilityState` and all *quiet* — a refresh must never drop a queue back to a
|
|
skeleton under whoever is reading it.
|
|
|
|
**`STAGES` carries `kind`** — `needs` / `customer` / `auto` / `end` — answering "who is
|
|
holding this lead", plus `need` (what a queue wants done, which is what the sidebar shows)
|
|
and `doing`/`by` (what to say while an AI carries it, instead of an idle screen).
|
|
|
|
**`STAGES` and `ACTIONS` are hand-mirrored** from `workflow.tbl_wf_states` and
|
|
`workflow.tbl_wf_state_allowed_activities`, because the API returns neither the order nor
|
|
the labels the sidebar needs. An activity added to the workflow but not added to `ACTIONS`
|
|
is simply invisible — the platform allows it, the console never offers it, nothing errors.
|
|
This bit once already: `zk-act-drop` (Mark Lost) was missing entirely, so no lead could be
|
|
dropped from the console. It is now appended to every state in `DROPPABLE` rather than
|
|
written into ten entries. Keep all of it in step with the seed SQL.
|
|
|
|
No endpoint exposes state → allowed activities (eight plausible paths probed, all 404) and
|
|
the detail view carries `current_state_id` but no activity list — which is *why* the map is
|
|
hand-maintained. If the platform ever serves it, delete the map.
|
|
|
|
## API shapes that have already bitten
|
|
|
|
- `org_id` on `POST /usr/login` must be a **string**; a number returns
|
|
`cannot unmarshal number into Go struct field LoginRequest.org_id`.
|
|
- `POST /view/recordview` names the view `rv_template_uid` (the GET spelling is `rv_id`),
|
|
and paging/sort/filters live **inside** `search_query`. Top-level gives
|
|
`400 Missing param: rv_template_uid (body)`.
|
|
- Row key varies by source type: workflow views return `data`, rdbms views have returned
|
|
`records` — hence the `data ?? records ?? rows` fallbacks.
|
|
- Audit must use the **app-scoped** path `/app/536/view/audit`. Bare `/view/audit` is not
|
|
an API route; it falls through to the SPA and returns HTML with a 200.
|
|
- `POST /upload` **requires** `workflow_uuid` (taken from config, not from the caller's
|
|
ctx). The field context (`activity_id`, `field_id`, `instance_id`) is how the backend
|
|
resolves allowed types, size limit and `ocr_config` — it ignores anything the client
|
|
claims about them.
|
|
- `/ocr-extract` takes a **reference** to an already-uploaded file, not bytes, and answers
|
|
`{ extracted, raw }`. Files therefore upload on pick, not on submit.
|
|
- `POST /view/recordview` takes the view uid in the **body**. The path form the frontend
|
|
spec documents (`/view/recordview/zk-rv-leads`) returns **404**.
|
|
- The detail view returns `{ config, data }` — `config.fields[].output_label` names every
|
|
one of the 117 fields, so labels come from the server rather than from prettified ids.
|
|
- The `message` on a `/start` or `/activity` response is written per activity ("Documents
|
|
received — capturing the risk") and is surfaced verbatim, not replaced with a toast.
|
|
- On submit, `ActivityForm` sends only fields the activity defines, drops empties (an
|
|
unknown field is fatal to schema validation) and skips `id_gen` — that reference is
|
|
issued server-side.
|
|
- OCR results fill only fields still blank; never overwrite what a person typed with what
|
|
a model read.
|
|
|
|
## Deployment
|
|
|
|
Registered with frontgen by `sm2/custom-apps/zurich-kotak/05_frontgen_project.sql` (slug
|
|
`zurich-kotak`, repo_name `zurich_kotak`, org 84). A push to `main` builds and serves at
|
|
https://preview-dev.getzino.in/zurich-kotak/. The webhook fires only on a **new** push.
|
|
|
|
Demo logins (all password `ZurichKotak@2026`, org `84`) are listed in `README.md`; sign in
|
|
as `sanjay.uw@zurichkotak.example` to land in the one human queue.
|