# Nearle Console The new Nearle Daily retail operations console. Built against the existing Fiesta backend — the API is a fixed constraint, not something this repo changes. ## Running it ```bash npm install npm run dev # http://localhost:3100 npm run typecheck # tsc --noEmit npm run build # typecheck + production build ``` > **Port 3100, not 3000.** The old console (`daily_merchant_web`) runs its dev > server on 3000. `strictPort` is on, so if 3100 is taken this fails loudly > rather than silently moving — which is the failure that makes you think your > changes did not land when you are actually looking at a different app. In development Vite proxies `/fiesta` → `https://fiesta.nearle.app`. For a deployed build set `VITE_API_BASE` (see `.env.example`). ## Stack | Layer | Choice | |---|---| | Build | Vite 8 | | Language | **TypeScript 7**, strict, `noUncheckedIndexedAccess`. No `.js`/`.jsx` anywhere. | | UI | React 19 + **@astryxdesign/core** | | Theme | `src/theme/nearle.ts`, compiled to `nearle.css` | | Routing | React Router 7, split per page | | Server state | TanStack Query 5 | | Icons | lucide-react | | Spreadsheets | `xlsx`, loaded on demand | ## What's built All three workspaces. Which one opens is decided by the account, not chosen — see `src/auth/roles.ts`. **Nearle Admin** (`issuperadmin`) — the platform operator: - `/nearle/stores` — every tenant, branch counts, per-tenant performance - `/nearle/stores/:tenantId` — one tenant's branches, orders and revenue - `/nearle/onboard/tenant` — provision a merchant group and its first outlet - `/nearle/catalogue` — the global catalogue, plus both product-import paths - `/nearle/partners` — rider partners, and the riders under each - `/nearle/dispatch` — every partner's live work and shifts - `/nearle/uploads` — spreadsheets sent to the catalogue service **Store Admin** (roleid 1 and 3) — one merchant, every branch: - `/admin/console` — online and counter sales side by side, per branch - `/admin/sales` · `/admin/dispatch` · `/admin/inventory` · `/admin/reports` - `/admin/branches/new` — commission an outlet with its delivery thresholds - `/admin/users` — back-office people and till accounts - `/admin/terminals` · `/admin/uploads` · `/admin/profile` · `/admin/onboarding` **Store user** (everything else) — one branch, scoped to it: - `/store/console` · `/store/products` · `/store/sales` · `/store/dispatch` - `/store/reports` · `/store/customers` · `/store/terminals` · `/store/staff` - `/store/uploads` · `/store/account` · `/store/setup` Two routes are redirects rather than pages, and deliberately: `/nearle/onboard/branch` → `/nearle/stores` (a branch is commissioned from the tenant that will own it), and `/nearle/fleet` → `/nearle/dispatch`. ## Things about the backend that shape this code These are not bugs in this repo. They are the API's behaviour, and each one is worked around deliberately. 1. **No web authentication.** The login endpoints return the user record and no token; only `/v1/pos/*` has middleware. So the session *is* that record, held in `sessionStorage`. `src/auth/session.ts` is the only file that changes when tokens arrive. 2. **Every list call needs a scoping id or it 400s.** The IDOR pass added controller-level guards. Query hooks are therefore `enabled`-gated on the id and the id is part of the query key. 3. **Roles are derived, not asserted.** `issuperadmin` is checked before `roleid`, because the flag is server-derived and a roleid is not. Roleids 7 and 8 are till roles and never reach an admin workspace. 4. **`POST /products/create` takes one product and returns no id.** The controller passes the struct to the service by value, so GORM writes the generated id into a copy that is then discarded. The sheet importer therefore creates, then re-queries by SKU to resolve ids, then batches the location and stock writes. See `importSheetProducts` in `src/api/products.ts`. 5. **The two import paths are not symmetric.** Catalogue import is one idempotent batch call. Sheet import is N creates with no dedupe on SKU, so re-uploading a file duplicates its products — the UI says so before the button. 6. **The global catalogue has a price *range*, not a price**, and no mapping to a tenant's own categories. Category, subcategory, retail price, cost and tax are collected before an import can be enabled. 7. **POS endpoints take one required `locationid`.** There is no tenant-wide counter-sales call, so a multi-branch POS view fans out per branch. ## The 30-second cadence Online orders land when an order is placed; counter sales reach the console on a 30-second refetch; anything still sitting on an offline till has not arrived at all. So **any figure blending the two is eventually consistent**, and the `` component exists to say when a number was last true and how many bills are still stranded. Polling is applied per query in `src/queries/hooks.ts` rather than globally — a provisioning form has no business re-polling. ## Design The visual system is ported from KROW (which is built on Astryx too, so the tokens are role-for-role comparable), with Nearle purple `#662582` as the accent. The ambient canvas in `index.css` — a fixed, viewport-wide horizontal gradient at `z-index: -1` — is the one piece of KROW that carries most of the character. Radius, type scale and control heights are matched to KROW in `src/theme/nearle.ts`, with the reasoning kept in comments. **Open:** the dark-mode accent `#B57FD0` was chosen for 5.41:1 contrast on Astryx's dark surface and still needs brand-owner sign-off before dark mode ships.