102 lines
4.6 KiB
Markdown
102 lines
4.6 KiB
Markdown
# 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
|
|
|
|
Only the **Nearle Admin** workspace so far:
|
|
|
|
- `/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
|
|
- `/nearle/onboard/branch` — commission an outlet with its delivery thresholds
|
|
- `/nearle/catalogue` — the global catalogue, plus both product-import paths
|
|
|
|
`/admin/*` (Store Admin) and `/store/*` (Store Manager) render a named
|
|
placeholder rather than a 404, so those roles land somewhere that explains
|
|
itself.
|
|
|
|
## 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
|
|
`<Freshness>` 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.
|