Admin Portal — Frontend data flow (API → screen)¶
This document describes how the Admin Portal web app (ui-clients/frontend-web-apps/apps/admin-portal) connects to APIs and gets data onto the screen. It is a mental map for navigating the codebase; the Canteen area is the most built-out example.
Scope: Admin Portal only (not parent app, not mobile). Other modules (Attendance, Academics, Payments, On My Way) follow the same router + layout + feature folder pattern but may use fewer SDK calls or more placeholders.
1. End-to-end boot sequence¶
| Step | Where | What happens |
|---|---|---|
| 1 | src/main.tsx |
Loads CSS, reads env, runs initMonitoring(), initSdk() (must run before any API call), mounts <App /> under StrictMode. |
| 2 | src/App.tsx |
Wraps the tree: ThemeProvider → AppErrorBoundary → QueryProvider (TanStack Query) → AuthProvider → RouterProvider + Toaster. |
| 3 | src/app/router/router.tsx |
Defines routes: public /auth/*, /onboarding, and / with DashboardLayout + AuthGuard + appRoutes. |
| 4 | Matched route | Lazy-loaded feature layout/page renders; components run hooks (useQuery, context, etc.). |
Order matters: SDK clients get baseUrl and auth from initSdk(); React Query cache is cleared on logout inside AuthProvider.
2. Configuration & “empty state” mode¶
| Piece | File | Role |
|---|---|---|
| Env | src/config/env.ts |
apiUrl (VITE_API_URL), isEmptyState (VITE_EMPTY_STATE=true), legacyAdminUrl, appVersion. |
| API base + Bearer | src/services/sdk/setup.ts |
canteenStaffClient and schoolStaffClient get baseUrl: env.apiUrl and auth: () => getAccessToken(). |
| 401 | Same + AuthProvider |
Response interceptor calls onUnauthorized → logout / redirect. |
Empty state: Many src/features/canteen/api/*.ts functions short-circuit when env.isEmptyState is true and return data from src/features/canteen/data/empty-state.ts (no network). That lets UI work without a backend.
3. Generated SDK (OpenAPI → TypeScript)¶
- Package:
@smartsapp/sdk(monorepoui-clients/sdk). - Staff bundle:
import { canteenStaff } from "@smartsapp/sdk/staff"re-exports generated functions such ascanteenStaff.listItems,createItem,getSalesReport, etc. - Client instances:
@smartsapp/sdk/canteen-staff/client.gen(and school) are configured ininitSdk(), not per-feature.
Feature code should not call fetch directly for Smartsapp APIs; it goes through these clients so URLs, auth, and types stay aligned with OpenAPI.
4. Auth and how the token reaches the API¶
| Concern | Location |
|---|---|
| Token storage | src/services/auth/storage.ts |
| React context | src/services/auth/authContext.tsx — login / logout, clears all React Query cache on logout |
| Route protection | src/app/guards/AuthGuard.tsx — redirects unauthenticated users |
| Handoff / login pages | src/app/pages/AuthLoginPage.tsx, AuthHandoffPage.tsx |
The SDK auth callback reads the same storage initSdk() wired; no duplicate header logic in each feature.
5. How data reaches the UI¶
The app uses TanStack Query (React Query) everywhere for server state. A small amount of useEffect-driven prefetch exists at the layout level as a deliberate warm-up, not as an alternative data layer.
A. TanStack Query (the path for list/detail pages)¶
- Query key — e.g.
src/features/canteen/menu/menu.keys.ts(menuKeys.categories(),menuKeys.itemsLibrary()). - Query hook — e.g.
src/features/canteen/menu/menu.queries.ts(useMenuItemsLibraryQuery→queryFn: fetchMenuItems). - API function — e.g.
src/features/canteen/menu/menu.api.tscallscanteenStaff.listItemsand maps the response. Early-returns empty-state data whenenv.isEmptyStateis true. No module-levellet cachestate — the React Query cache is the single source of truth. - Page — e.g.
src/features/canteen/menu/pages/MenuItemsPage.tsxcalls the hook, wraps children inQueryBoundary, passesdatainto a Content component. - Presentation — e.g.
MenuItemsContent.tsxrenders table/cards only; it does not call the SDK.
Exports: @/features/canteen/menu re-exports keys + hooks so pages import one place.
B. Layout-level prefetch (deliberate warm-up, not an alternative pattern)¶
src/features/canteen/layouts/CanteenModuleLayout.tsx uses useEffect + queryClient.prefetchQuery to warm the caches for filters and dropdowns the moment the user enters the canteen module. This is intentional: it eliminates the loading flash when a nested page mounts and immediately fires the same queries. It is not a signal that you should also use useEffect + useState for data fetching — the prefetch is a one-line React Query call, not a bespoke fetch loop.
C. Historical note: module-level let cache + *Sync() getters¶
An earlier iteration of the canteen feature kept in-memory caches in api/*.ts files and exposed sync getters for dropdowns and the menu planner. That pattern has been removed. A grep of features/*/api/ today finds zero let cache declarations. If you see a reference to this pattern in old PRs or code review comments, it's stale — always use React Query. The last surviving *Sync() helper (getMenuPlannerInitialSync in menu-planner.utils.ts) was tombstone code and has been deleted.
6. Mutations (create / update / delete)¶
Typical flow:
- Form or table action calls a function in
src/features/canteen/api/<domain>.ts(e.g.createMenuItem,updateMenuCategory). - That function builds a body (sometimes normalizing UI fields → API shape), calls
canteenStaff.*, throws onerror. - On success, the caller often
queryClient.setQueryData(menuKeys.…, …)orinvalidateQueriesso lists refresh.
Types: src/features/canteen/types/sdk-types.ts extends generated SDK types with UI-only fields (imageUrl, tags, etc.).
7. Routing: from URL to component¶
Global route table¶
src/app/router/routes.tsx—appRoutesmerges feature route arrays and a default index redirect to Attendance.src/config/routes.ts— path constants for links (e.g.routes.canteen→"/canteen").
Canteen subtree¶
src/features/canteen/routes.tsx—path: "canteen"+CanteenModuleLayout+ child routes (menu/items,feeding-list,reports, …). Pages arelazy()loaded.
Layout stack¶
DashboardLayout— shell: sidebar, header, main outlet.CanteenModuleLayout— full-width canteen column + prefetch inuseEffect+<Outlet />for nested canteen pages.
So: URL → router → AuthGuard → DashboardLayout → CanteenModuleLayout → Page → QueryBoundary / Provider → Content component.
8. Worked example: Menu Items list¶
| Layer | File | Responsibility |
|---|---|---|
| Route | features/canteen/routes.tsx |
path: "menu/items" → MenuItemsPage |
| Page | pages/MenuItemsPage.tsx |
useMenuItemsLibraryQuery(), QueryBoundary, optional MenuItemsProvider |
| Query | menu/menu.queries.ts |
queryKey: menuKeys.itemsLibrary(), queryFn: getMenuItemsLibrary |
| API | api/menu-items.ts |
canteenStaff.listItems, env.isEmptyState branch, withImageAliases, module cache |
| UI | components/sections/MenuItemsContent.tsx |
Table/cards from items prop |
Add/Edit item: Route → AddMenuItemPage / EditMenuItemPage → MenuItemUpsertForm → createMenuItem / updateMenuItem → navigate back → (ideally) query cache updated.
9. Worked example: Canteen dashboard¶
| Layer | File |
|---|---|
| Page | pages/CanteenPage.tsx |
| Query | reports/reports.queries.ts — useCanteenDashboardQuery |
| API | api/dashboard.ts — fetchCanteenDashboard |
| UI | components/sections/CanteenDashboardContent.tsx — KPIs, filters, links |
10. Context and URL-driven state (Feeding List)¶
Some screens combine React Context with search params (e.g. preloaded filters from dashboard links):
- Context:
src/features/canteen/feeding-list/context/FeedingListContext.tsx - Page:
feeding-list/pages/FeedingListPage.tsx+FeedingListContent.tsx
Flow: URL → context initial state → filters → data table (may still use SDK or mocks depending on wiring).
11. Reports module¶
- Page:
reports/pages/ReportsPage.tsx— rendersReportsContentwith tabs for Sales, Orders, Tickets, Sika ID, and Kitchen Prep. - API split: Multiple files under
features/canteen/api/(reports.ts,sales.ts,orders-report.ts, …) each map to different SDK endpoints or empty-state data.
12. Folder map (Canteen-heavy)¶
apps/admin-portal/src/
├── main.tsx, App.tsx
├── config/ # env, route constants
├── app/
│ ├── router/ # createBrowserRouter, appRoutes composition
│ ├── layouts/ # DashboardLayout
│ ├── guards/ # AuthGuard
│ ├── providers/ # QueryProvider, ThemeProvider
│ └── pages/ # Auth, handoff
├── services/
│ ├── sdk/ # initSdk, client config
│ └── auth/ # AuthProvider, storage
├── components/ui/ # shadcn-style primitives
├── shared/ # DataTable, PageHeader, cross-feature UI
└── features/
├── canteen/
│ ├── routes.tsx
│ ├── layouts/ # CanteenModuleLayout
│ ├── pages/ # Thin route targets
│ ├── api/ # SDK wrappers + empty-state + module cache
│ ├── menu/ # Query keys + hooks (menu library)
│ ├── reports/ # Reports queries, context, components
│ ├── feeding-list/, feeding-tickets/, students/, settings/
│ ├── components/ # sections, forms, menu-planner UI
│ ├── context/ # Feature-specific providers
│ ├── data/ # mocks, empty-state shapes
│ └── types/ # Domain + sdk-types extensions
├── attendance/, academics/, payments/, on-my-way/, onboarding/
13. Known rough edges¶
Most of the "things feel all over the place" problems an earlier revision of this doc warned about have been cleaned up. What's left:
- Import paths — Mostly
@/...absolute imports now, but a handful of canteen sub-features (feeding-list,feeding-tickets) still reach the sharedQueryBoundaryvia relative../shared/.... Both resolve to the same file; normalize to absolute when you touch those files. - Route sub-paths hardcoded in
features/*/routes.tsx— Top-level paths are centralized insrc/config/routes.ts, but each feature's nested paths ("menu/items","menu/items/new", etc.) are string literals. Renaming a route silently breaks any<Link to="/canteen/menu/items">elsewhere. Add the sub-paths toroutes.tswhen you next rename one. - Menu planner god files —
use-menu-planner.ts(~1100 LOC hook),MenuScheduleContent.tsx(~1300 LOC), andPlannerGrid.tsx(~900 LOC) mix too many concerns. Stable today; if you're actively extending the planner, split these into smaller hooks and presentational components first. - Empty state —
env.isEmptyStatechanges component behavior without changing code paths. Easy to forget when debugging "why no network call". Branching happens consistently at the api/query seam (not inside components), so at least the blast radius is contained.
Cleaned up (no longer a problem)¶
- ~~Query vs no Query~~ —
features/*/api/is React Query everywhere;useEffectusage is limited to deliberate layout-level prefetch and local UI state. - ~~Dual caches (
let cache+ React Query)~~ — nolet cachedeclarations remain in anyfeatures/*/api/file. - ~~Sync getters~~ — the last
*Sync()helper (getMenuPlannerInitialSync) was deleted; the planner uses React Query. - ~~Type duplication~~ —
features/canteen/types/sdk-types.tsis a disciplined re-export barrel ("extend only, never redefine"). Other features import directly from@smartsapp/sdk.
14. Practical checklist for a new screen¶
- Add route under the right feature
routes.tsx(and lazy page if desired). - Add or reuse an API wrapper in
features/<module>/<domain>.api.tscallingcanteenStaff/school, with anenv.isEmptyStateearly-return if the screen needs to work offline. - Add a query key (e.g.
menu.keys.ts) and a useQuery hook (e.g.menu.queries.ts). For mutations, add a hook to the corresponding*.mutations.tsfile that invalidates or optimistically updates through the namedinvalidate*helpers. - Keep the page thin: hook +
QueryBoundary+ optional context provider. - Put tables/forms in
components/and pass data as props; presentational components must never call the SDK. - On mutation success, rely on
queryClient.invalidateQueries(menuKeys.…)orqueryClient.setQueryData(menuKeys.…, …)via the mutation hook — not a side-channel cache.
15. Related repo docs¶
- docs/AI_CONTEXT.md — repo-wide conventions.
ui-clients/sdk/— generated clients; regenerate when OpenAPI changes.- Backend REST paths match the
canteen-staffOpenAPI spec used to generate the SDK.
Last updated to reflect the admin-portal structure as of the doc’s authoring; adjust paths if files move.