# UdyamLinc — UI Three surfaces in one Vite/React app, matching the three institutional tiers. Full project documentation, including the demo path and what is real versus seeded, lives in **`../UdyamLinc-Backend/README.md`**. ```bash npm install npm run dev # http://localhost:5182 ``` The backend is expected at `http://localhost:8082/api/v1/udyam`. Override with `VITE_UDYAM_API_BASE_URL` in `.env.local`. --- ## The three surfaces | Route | Tier | Shell | |---|---|---| | `/nabard` | NABARD — aggregates only | `features/nabard` | | `/officer` | Bank console — one institution's book | `features/officer` | | `/app` | Enterprise PWA — offline, Hindi-first | `features/enterprise` | Both staff tiers share one sign-in form and land in different portals; `homeFor()` picks the destination from the role. That routing is a **convenience only** — the backend re-derives the tier from the signed JWT and refuses the other surface with a `403`, so a tampered session gains nothing. The NABARD portal is deliberately a different chrome from the bank console. An apex user and a branch officer are not two ranks of one job, and a portal that looked identical would invite the assumption that the apex user is a bank user with more rights. --- ## The house pattern Every screen follows the same shape, and new ones should: ```tsx const { data, isError } = useQuery({ queryKey: [...], queryFn: () => get('/path') }) const demo = isError const rows = data ?? demoRows() // in-file fallback from lib/demo/ … {demo && } ``` A screen must render with the backend down — field devices are on patchy 2G — but it must **say so**. The "Demo data" pill means that screen's call failed, not that the whole app is a mock. Demo fallbacks deliberately include failure states: the data-entry fallback contains rejected rows, and the NABARD fallback contains a bank below the suppression threshold. A demo where everything is clean teaches the wrong thing — a reviewer who only ever sees green rows will not know the rejection panel exists. --- ## Screens that carry the argument - **`/officer/rules`** — every trip-wire with its outcome, including the ones that were *skipped* and why. This is the screen that shows the system refusing to fire on stale data. - **`/officer/feeds`** — provenance. Mandi rows carry a source: `Agmarknet`, or an amber **Hand** chip for a price somebody typed. By the time a price reaches a feature the two are indistinguishable, so the distinction has to be visible here. - **`/officer/data-entry`** — upload, preview, commit. Accepted, rejected and overwriting rows stay in three separate groups. - **`/nabard`** — the India choropleth and the zone → state → district drill-down. States with no portfolio are drawn in a neutral grey that is nowhere on the stress ramp, so absence is never read as health. --- ## Checks ```bash npx tsc -b # types npx oxlint src # lint npm run build # production build ``` All three are expected to pass before a commit. --- ## Attribution State boundaries by the [DataMeet India community](http://datameet.org/) ([datameet/maps](https://github.com/datameet/maps)), used under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Simplified for display and **not an authoritative depiction of any boundary**. The credit renders on the map itself, as the licence requires.