Skip to content

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: ThemeProviderAppErrorBoundaryQueryProvider (TanStack Query) → AuthProviderRouterProvider + 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 (monorepo ui-clients/sdk).
  • Staff bundle: import { canteenStaff } from "@smartsapp/sdk/staff" re-exports generated functions such as canteenStaff.listItems, createItem, getSalesReport, etc.
  • Client instances: @smartsapp/sdk/canteen-staff/client.gen (and school) are configured in initSdk(), 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.tsxlogin / 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)

  1. Query key — e.g. src/features/canteen/menu/menu.keys.ts (menuKeys.categories(), menuKeys.itemsLibrary()).
  2. Query hook — e.g. src/features/canteen/menu/menu.queries.ts (useMenuItemsLibraryQueryqueryFn: fetchMenuItems).
  3. API function — e.g. src/features/canteen/menu/menu.api.ts calls canteenStaff.listItems and maps the response. Early-returns empty-state data when env.isEmptyState is true. No module-level let cache state — the React Query cache is the single source of truth.
  4. Page — e.g. src/features/canteen/menu/pages/MenuItemsPage.tsx calls the hook, wraps children in QueryBoundary, passes data into a Content component.
  5. Presentation — e.g. MenuItemsContent.tsx renders 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:

  1. Form or table action calls a function in src/features/canteen/api/<domain>.ts (e.g. createMenuItem, updateMenuCategory).
  2. That function builds a body (sometimes normalizing UI fields → API shape), calls canteenStaff.*, throws on error.
  3. On success, the caller often queryClient.setQueryData(menuKeys.…, …) or invalidateQueries so 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.tsxappRoutes merges 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.tsxpath: "canteen" + CanteenModuleLayout + child routes (menu/items, feeding-list, reports, …). Pages are lazy() loaded.

Layout stack

  1. DashboardLayout — shell: sidebar, header, main outlet.
  2. CanteenModuleLayout — full-width canteen column + prefetch in useEffect + <Outlet /> for nested canteen pages.

So: URLrouterAuthGuardDashboardLayoutCanteenModuleLayoutPageQueryBoundary / ProviderContent 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 / EditMenuItemPageMenuItemUpsertFormcreateMenuItem / updateMenuItem → navigate back → (ideally) query cache updated.


9. Worked example: Canteen dashboard

Layer File
Page pages/CanteenPage.tsx
Query reports/reports.queries.tsuseCanteenDashboardQuery
API api/dashboard.tsfetchCanteenDashboard
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 — renders ReportsContent with 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:

  1. Import paths — Mostly @/... absolute imports now, but a handful of canteen sub-features (feeding-list, feeding-tickets) still reach the shared QueryBoundary via relative ../shared/.... Both resolve to the same file; normalize to absolute when you touch those files.
  2. Route sub-paths hardcoded in features/*/routes.tsx — Top-level paths are centralized in src/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 to routes.ts when you next rename one.
  3. Menu planner god filesuse-menu-planner.ts (~1100 LOC hook), MenuScheduleContent.tsx (~1300 LOC), and PlannerGrid.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.
  4. Empty stateenv.isEmptyState changes 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; useEffect usage is limited to deliberate layout-level prefetch and local UI state.
  • ~~Dual caches (let cache + React Query)~~ — no let cache declarations remain in any features/*/api/ file.
  • ~~Sync getters~~ — the last *Sync() helper (getMenuPlannerInitialSync) was deleted; the planner uses React Query.
  • ~~Type duplication~~ — features/canteen/types/sdk-types.ts is a disciplined re-export barrel ("extend only, never redefine"). Other features import directly from @smartsapp/sdk.

14. Practical checklist for a new screen

  1. Add route under the right feature routes.tsx (and lazy page if desired).
  2. Add or reuse an API wrapper in features/<module>/<domain>.api.ts calling canteenStaff / school, with an env.isEmptyState early-return if the screen needs to work offline.
  3. 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.ts file that invalidates or optimistically updates through the named invalidate* helpers.
  4. Keep the page thin: hook + QueryBoundary + optional context provider.
  5. Put tables/forms in components/ and pass data as props; presentational components must never call the SDK.
  6. On mutation success, rely on queryClient.invalidateQueries(menuKeys.…) or queryClient.setQueryData(menuKeys.…, …) via the mutation hook — not a side-channel cache.

  • docs/AI_CONTEXT.md — repo-wide conventions.
  • ui-clients/sdk/ — generated clients; regenerate when OpenAPI changes.
  • Backend REST paths match the canteen-staff OpenAPI 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.