mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-01 19:23:28 +00:00
6f4878d300e97a68485044907922d37e14620f05
Four screens were telling the applicant things that were not true, and two back-office actions were writing state nobody should be able to write by hand. The exam form is a stepper. Basic Info and Settings each answer only for their own fields, and a third step reviews the whole thing before anything is sent — "Create Exam" exists on that step alone, so there is no path from an earlier one into the API. Clicking it from Basic Info used to report the Settings fields as missing: a correct error the user could not act on, since that step had not been shown yet. The rules move to ./validation, which is what the per-step check and the final whole-payload check both read, so the two cannot drift apart. Back keeps everything entered — state lives in the component. The portal derives the examination stage from the records rather than from the application status alone. Registering, attendance and the sitting itself all happen on the registration and the attempt, and the portal read none of them — which is how an application still said "Awaiting Exam Date" after the back office had already evaluated the paper. examStageFor() names what the records add up to: eligible to register, registered, attendance confirmed, sitting, under evaluation, passed, failed. Both the applications list and the certificates page use it, and both poll the registrations they read it from, because attendance is recorded by an invigilator while the candidate is watching the screen. EXAM_PAID is no longer a waiting state. Nobody assigns a date: the fee having cleared is exactly what makes the candidate eligible to pick a published sitting, so that row now offers Register rather than a disabled button. Schedule Exam and Record Exam Outcome are gone from the COC queue — actions, modals, RTK endpoints and strings. Their server endpoints are gone too, so this is not a hidden button over a live route.
feat: implement interactive vessel registration reporting page with filters, charts, and data tables
EMA Platform — Nx Monorepo Frontend
A fully scaffolded Nx monorepo housing two Vite + React 19 SPAs (Backoffice and Portal) with shared libraries for API integration, UI components, and theming.
Tech Stack
| Tool | Version |
|---|---|
| React | 19 |
| Nx | 22 |
| Vite | 7 |
| TypeScript | 5.9 |
| Redux Toolkit | 2.11 |
| Mantine | 8.3 |
| React Router | 7 |
| TanStack Query | 5 |
| React Hook Form | 7 |
| Zod | 4 |
| Tailwind CSS | 3.4 |
| Vitest | 4 |
Monorepo Structure
emaui/
├── apps/
│ ├── backoffice/ # Admin/operator SPA — port 4201
│ └── portal/ # End-user SPA — port 4200
└── libs/
├── api/ # RTK Query baseApi, session resolution, generic query/mutation hooks
├── ui/ # Shared Mantine components (ConfirmModal, ApiErrorAlert, notify)
└── shared/ # Mantine theme (emaTheme), design tokens
libs/api
base-api/— RTK QuerycreateApiinstance withprepareHeadersthat injects the Bearer token from Redux state or storage.session/—resolveTokenFromStorage()reads theauth-tokencookie first, falling back tolocalStoragefor legacy pre-migration sessions.resolveSessionContext()merges Redux state token with storage fallback.query-and-mutation/— GenericuseApiQuery/useApiMutationwrappers for one-off API calls without defining a dedicated endpoint file.
libs/ui
ConfirmModal— Reusable Mantine modal for destructive-action confirmation.ApiErrorAlert— Extracts a human-readable message from RTK Query error shapes or Error objects.notify— Thin wrapper around@mantine/notificationswith.success,.error,.info,.warninghelpers.
libs/shared
ema-theme— Mantine v8createTheme()withemaPrimary(blue) andemaSecondary(warm) color tuples, Inter font, and custom shadow scale.
Auth Flow
- User submits the login form (LoginForm / LoginPage).
- The form calls the
loginRTK Query mutation (backoffice) or a plainfetch(portal). - On success,
loginSuccessaction is dispatched → Reduxauthslice storestokenanduser;authStorage.setToken()persists the token to a cookie (both apps callconfigureAuthStorage(prefix, true)). baseApi'sprepareHeadersreads the token viaresolveSessionContext(getState())and attachesAuthorization: Bearer <token>to every RTK Query request.ProtectedRoutechecksauthStorage/the token cookie on every navigation — if absent, redirects to/login.logoutaction clears Redux state and callsauthStorage.clear()to remove all auth cookies.
Local Setup
# 1. Install dependencies
npm install
# 2. Copy environment config
cp .env.example .env
# Edit VITE_BASE_API_URL to point at your running backend
# 3. Start the backoffice (port 4201)
npm run backoffice
# 4. Start the portal (port 4200)
npm run portal
# 5. Or start both in parallel
npm run dev:all
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
VITE_BASE_API_URL |
Yes | http://localhost:3000 |
Base URL for all API requests |
VITE_ENABLE_DEVELOPER_TOOLS |
No | true |
Toggle Redux DevTools |
VITE_PAYMENT_API_URL |
Yes (portal) | — | Base URL for the payment service (portal payment feature). Can be a same-origin path if a backend proxy is later placed in front of it |
VITE_PAYMENT_SERVICE_TOKEN |
Yes (portal) | — | Sent as x-service-token on every payment request. Note: bundled VITE_* values are public in the built app, not secret |
PORTAL_PORT |
No | 4200 |
Docker host port for portal |
BACKOFFICE_PORT |
No | 4201 |
Docker host port for backoffice |
Docker
# Build and run both apps via Docker Compose
cp .env.example .env
docker compose up --build
The Dockerfile uses multi-stage builds with named targets (portal / backoffice). Each stage produces an nginx image serving the built SPA.
Adding a New Feature
-
Create the feature folder under the relevant app:
apps/backoffice/src/app/features/<feature-name>/ ├── types/ # TypeScript interfaces ├── api/ # RTK Query injectEndpoints ├── store/ # Redux slice (if local state needed) ├── hooks/ # Custom hooks wrapping store/api ├── components/ # Presentational React components └── pages/ # Route-level components -
Wire up the API endpoint in
<feature-name>/api/<feature>-api.tsusingbaseApi.injectEndpoints(...). -
Add a route in
apps/<app>/src/app/router/index.tsx(backoffice) orapps/<app>/src/app/router.tsx(portal). -
Add a sidebar entry in
AppSidebar.tsx(backoffice only) for the new route. -
If the feature needs shared UI, add components to
libs/ui/and export fromlibs/ui/src/index.ts.
Description
Languages
TypeScript
96.9%
JavaScript
1.3%
CSS
1.2%
HTML
0.4%
Shell
0.2%