mihretue b49de1c1a8 fix(exam): guard question assignment once the paper is locked
The detail page fired assign/draw requests blind and surfaced the raw
backend key to the user — a locked paper showed up as the literal string
"paper_locked_after_registration" in a toast.

Manage Questions is now disabled once any candidate has registered, with a
tooltip and badge explaining why; both the manual assign and the random draw
live behind that modal, so one guard covers the same ground the backend's
assertPaperEditable does. The empty-paper alert now explains the deadlock
instead of telling the user to click a button that will fail, and both
error handlers translate paper_locked_after_registration in case someone
registers while the modal is open.

Registrations come from the query ExamCandidatesPanel already runs on this
page, so RTK Query serves it from cache rather than issuing a second request.
2026-08-24 14:04:24 +00:00
2026-08-18 11:08:34 +03:00
2026-06-12 11:46:25 +03:00
2026-06-08 13:17:29 +03:00
2026-06-15 10:29:28 +00:00
2026-06-15 10:29:28 +00:00
2026-08-21 10:42:38 +03:00
2026-06-11 06:55:40 +00:00
2026-06-02 09:23:01 +03:00
2026-08-21 10:42:38 +03:00
2026-08-21 11:16:22 +00:00
Fix
2026-08-21 11:01:32 +03:00
2026-06-11 12:04:21 +00:00

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 Query createApi instance with prepareHeaders that injects the Bearer token from Redux state or storage.
  • session/resolveTokenFromStorage() reads the auth-token cookie first, falling back to localStorage for legacy pre-migration sessions. resolveSessionContext() merges Redux state token with storage fallback.
  • query-and-mutation/ — Generic useApiQuery / useApiMutation wrappers 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/notifications with .success, .error, .info, .warning helpers.

libs/shared

  • ema-theme — Mantine v8 createTheme() with emaPrimary (blue) and emaSecondary (warm) color tuples, Inter font, and custom shadow scale.

Auth Flow

  1. User submits the login form (LoginForm / LoginPage).
  2. The form calls the login RTK Query mutation (backoffice) or a plain fetch (portal).
  3. On success, loginSuccess action is dispatched → Redux auth slice stores token and user; authStorage.setToken() persists the token to a cookie (both apps call configureAuthStorage(prefix, true)).
  4. baseApi's prepareHeaders reads the token via resolveSessionContext(getState()) and attaches Authorization: Bearer <token> to every RTK Query request.
  5. ProtectedRoute checks authStorage/the token cookie on every navigation — if absent, redirects to /login.
  6. logout action clears Redux state and calls authStorage.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

  1. 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
    
  2. Wire up the API endpoint in <feature-name>/api/<feature>-api.ts using baseApi.injectEndpoints(...).

  3. Add a route in apps/<app>/src/app/router/index.tsx (backoffice) or apps/<app>/src/app/router.tsx (portal).

  4. Add a sidebar entry in AppSidebar.tsx (backoffice only) for the new route.

  5. If the feature needs shared UI, add components to libs/ui/ and export from libs/ui/src/index.ts.

Description
No description provided
Readme 219 MiB
Languages
TypeScript 97.9%
CSS 1.3%
HTML 0.4%
Shell 0.3%