From 4ebf523de709c00f0feed861669ba858cbac33a2 Mon Sep 17 00:00:00 2001
From: Methexx
Date: Thu, 14 May 2026 17:48:34 +0530
Subject: [PATCH 1/7] fix: Ci pipleine automate and release flutter package
---
.github/workflows/ci.yml | 48 +
.github/workflows/flutter-release.yml | 58 ++
Documentation/01-PROJECT-OVERVIEW.md | 482 ++++++++++
Documentation/02-BACKEND-DEEP-DIVE.md | 1028 ++++++++++++++++++++
Documentation/03-FRONTEND-WEB-APP.md | 741 ++++++++++++++
Documentation/04-FLUTTER-MOBILE-APP.md | 1225 ++++++++++++++++++++++++
Documentation/05-DEVOPS-DOCKER-CICD.md | 973 +++++++++++++++++++
README.md | 565 ++++++-----
8 files changed, 4859 insertions(+), 261 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 .github/workflows/flutter-release.yml
create mode 100644 Documentation/01-PROJECT-OVERVIEW.md
create mode 100644 Documentation/02-BACKEND-DEEP-DIVE.md
create mode 100644 Documentation/03-FRONTEND-WEB-APP.md
create mode 100644 Documentation/04-FLUTTER-MOBILE-APP.md
create mode 100644 Documentation/05-DEVOPS-DOCKER-CICD.md
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..2fcf782
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,48 @@
+name: CI Tests
+
+on:
+ pull_request:
+
+jobs:
+ backend-tests:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: apps/backend
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: npm
+ cache-dependency-path: package-lock.json
+
+ - name: Install dependencies
+ working-directory: .
+ run: npm ci
+
+ - name: Run unit tests
+ run: npm run test:unit
+
+ - name: Run integration tests
+ run: npm run test:integration
+
+ flutter-tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: subosito/flutter-action@v2
+ with:
+ flutter-version: '3.32.0'
+ channel: stable
+ cache: true
+
+ - name: Get dependencies
+ working-directory: apps/flutter
+ run: flutter pub get
+
+ - name: Run widget tests
+ working-directory: apps/flutter
+ run: flutter test
diff --git a/.github/workflows/flutter-release.yml b/.github/workflows/flutter-release.yml
new file mode 100644
index 0000000..4d82bc9
--- /dev/null
+++ b/.github/workflows/flutter-release.yml
@@ -0,0 +1,58 @@
+name: Flutter Release APK
+
+on:
+ push:
+ branches:
+ - release/development
+
+permissions:
+ contents: write
+
+jobs:
+ build-and-release:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+
+ - uses: subosito/flutter-action@v2
+ with:
+ flutter-version: '3.32.0'
+ channel: stable
+ cache: true
+
+ - name: Get dependencies
+ working-directory: apps/flutter
+ run: flutter pub get
+
+ - name: Build debug APK
+ working-directory: apps/flutter
+ run: flutter build apk --debug --dart-define=API_BASE_URL=${{ secrets.RAILWAY_BACKEND_URL }}
+
+ - name: Get next version tag
+ id: tag
+ run: |
+ LATEST=$(git tag --list 'v*' --sort=-version:refname | head -n1)
+ if [ -z "$LATEST" ]; then
+ NEXT="v1.0.0"
+ else
+ NEXT=$(echo $LATEST | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}')
+ fi
+ echo "tag=$NEXT" >> $GITHUB_OUTPUT
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ steps.tag.outputs.tag }}
+ name: "Release ${{ steps.tag.outputs.tag }}"
+ body: |
+ Debug APK — built from `release/development`
+ Commit: ${{ github.sha }}
+ files: apps/flutter/build/app/outputs/flutter-apk/app-debug.apk
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Documentation/01-PROJECT-OVERVIEW.md b/Documentation/01-PROJECT-OVERVIEW.md
new file mode 100644
index 0000000..7567112
--- /dev/null
+++ b/Documentation/01-PROJECT-OVERVIEW.md
@@ -0,0 +1,482 @@
+# Universe System — Project Overview & Architecture
+
+> **Document 1 of 5** — Complete project overview, monorepo structure, tech stack, features, and high-level architecture.
+
+---
+
+## Table of Contents
+
+1. [What is Universe System?](#what-is-universe-system)
+2. [Monorepo Structure](#monorepo-structure)
+3. [User Roles & Permissions](#user-roles--permissions)
+4. [Complete Feature List](#complete-feature-list)
+5. [Tech Stack Summary](#tech-stack-summary)
+6. [System Architecture Diagram](#system-architecture-diagram)
+7. [Data Flow Across Apps](#data-flow-across-apps)
+8. [Module-by-Module Working Style](#module-by-module-working-style)
+9. [Existing Documentation Map](#existing-documentation-map)
+
+---
+
+## What is Universe System?
+
+**Universe System** (also referred to as *School Connect*) is a **full-stack, multi-platform School Management Information System** that connects four user types — **Administrators, Teachers, Security Officers, and Parents** — through a single backend serving both a web dashboard (Next.js) and a parent-facing mobile app (Flutter).
+
+The platform digitizes the day-to-day life of a school — from gate entry/exit tracking via QR scans, to attendance, term results, complaints, lost & found, push notifications, AI-powered policy chatbot, and a real-time messaging system between teachers and parents.
+
+### Core Capabilities
+
+| Capability | Description |
+|---|---|
+| **Gate Tracking** | QR-based check-in/out at the school gate with real-time parent notifications |
+| **Attendance** | Daily attendance sessions per class with parent visibility |
+| **Results & Grades** | Teachers publish term results; parents view child's grades |
+| **Messaging** | Direct messages between teachers and parents with AI draft suggestion |
+| **Announcements** | School-wide or class-scoped notices |
+| **Complaints** | Parent-filed complaints triaged to admin, then assigned to staff |
+| **Lost & Found** | Parents report lost items; staff post found items |
+| **RAG Chatbot** | AI-powered Q&A over school policy PDFs (Gemini-embedded vectors) |
+| **Push Notifications** | Firebase FCM alerts to parent's phone for gate scans, messages, etc. |
+| **Role-Based Dashboards** | Distinct UIs for Admin / Teacher / Security / Parent |
+
+---
+
+## Monorepo Structure
+
+The repository is an **npm workspace monorepo** with three application workspaces:
+
+```
+Universe-System/ ← Repository root
+│
+├── apps/
+│ ├── backend/ ← Fastify + Prisma REST API (port 5000)
+│ │ ├── src/
+│ │ │ ├── server.ts ← App entry: startup, Redis, Firebase init
+│ │ │ ├── app.ts ← Fastify config: plugins, routes, errors
+│ │ │ ├── config/ ← env, prisma, redis, supabase, firebase
+│ │ │ ├── common/ ← middleware, utils (jwt, hash, otp, email)
+│ │ │ ├── modules/ ← 11 feature modules (auth/users/gate/...)
+│ │ │ └── scripts/seedAdmin.ts ← DB seeding
+│ │ ├── prisma/
+│ │ │ ├── schema.prisma ← 20 models, pgvector + uuid-ossp extensions
+│ │ │ └── migrations/ ← Generated migration history
+│ │ ├── tests/ ← Jest unit + integration tests
+│ │ ├── Dockerfile.dev
+│ │ ├── jest.config.js
+│ │ └── package.json
+│ │
+│ ├── frontend/ ← Next.js 16 + React 19 web dashboard (port 3000)
+│ │ ├── src/
+│ │ │ ├── app/ ← App Router: admin/, teacher/, security/, auth pages
+│ │ │ ├── features/ ← 10 feature modules (auth/messages/gate/rag/...)
+│ │ │ ├── shared/ ← components, hooks, lib
+│ │ │ └── core/ ← config, constants
+│ │ ├── public/ ← Static assets
+│ │ ├── Dockerfile.dev
+│ │ ├── eslint.config.mjs
+│ │ └── package.json
+│ │
+│ └── flutter/ ← Flutter parent mobile app (Android/iOS)
+│ ├── lib/
+│ │ ├── main.dart ← Entry: Firebase, ServiceLocator setup
+│ │ ├── app.dart ← MultiProvider + MaterialApp.router
+│ │ ├── core/ ← api, di, navigation, services, storage
+│ │ ├── features/ ← 15 feature modules (auth/dashboard/results/...)
+│ │ └── shared/ ← themes, reusable widgets
+│ ├── android/ ← Native Android project
+│ ├── ios/ ← Native iOS project
+│ ├── test/ ← Widget tests
+│ └── pubspec.yaml
+│
+├── Doc/ ← Existing detailed reference docs (10 files)
+├── Documentation/ ← THIS folder (5 comprehensive guides)
+│
+├── .github/workflows/ ← CI/CD (ci.yml, flutter-release.yml)
+├── .husky/ ← Git hooks (pre-commit, pre-push, commit-msg)
+├── .vscode/, .claude/, .mcp.json ← IDE & AI configs
+│
+├── docker-compose.yml ← Prod stack (redis, backend, frontend)
+├── docker-compose.dev.yml ← Dev stack with hot reload volumes
+├── package.json ← Root workspace scripts
+├── package-lock.json ← Single root lockfile
+├── commitlint.config.js ← Conventional Commits enforcement
+├── .prettierrc ← Code formatting rules
+├── .eslintignore, .gitignore, .dockerignore
+└── vercel.json ← Next.js Vercel deployment config
+```
+
+### Workspace Linking
+
+`package.json` (root) declares:
+
+```json
+{
+ "workspaces": ["apps/frontend", "apps/backend"]
+}
+```
+
+`apps/flutter` is **not** a Node workspace — it has its own `pubspec.yaml` and is managed independently via the Dart/Flutter toolchain.
+
+---
+
+## User Roles & Permissions
+
+The system supports **5 roles**, with `pending` being a transitional state.
+
+| Role | Description | Sign-up Path | Approval Required |
+|---|---|---|---|
+| **pending** | Account created, awaiting admin approval | Self-registers (teacher/security) | Yes — admin promotes |
+| **admin** | Full system control: user management, students, classes, RAG policies | Seeded or promoted by another admin | — |
+| **teacher** | Mark attendance, post notices, message parents, publish results | Self-registers → admin approves | Yes |
+| **security** | Scan QR at gate, view gate logs, message parents | Self-registers → admin approves | Yes |
+| **parent** | View child's gate/attendance/results, file complaints, chat with teachers | Multi-step OTP + 2FA verification | No — auto-active after OTP |
+
+### Parent Registration is Special (2FA)
+
+Parents go through a **2-factor identity check** to prove they own the child:
+1. Submit email + student ID number → backend validates `parent_email` matches school records
+2. OTP sent via Resend
+3. After OTP verification, the parent must answer profile questions: **grade, class, admission year, gender** — server-side validation against the Student record
+4. On success, parent user + `ParentStudent` link created atomically in a Prisma transaction
+
+---
+
+## Complete Feature List
+
+### Backend Feature Modules (11)
+
+| Module | Path | Purpose |
+|---|---|---|
+| **auth** | `apps/backend/src/modules/auth/` | Registration, OTP, login, password reset, JWT refresh, FCM token, 2FA for parents |
+| **users** | `apps/backend/src/modules/users/` | User profile, admin user management (approve/suspend/delete) |
+| **school** | `apps/backend/src/modules/school/` | Grades, classes, students, student photo upload, overview stats |
+| **gate** | `apps/backend/src/modules/gate/` | QR scan IN/OUT, manual entry, time-series stats, parent's child history, auto-checkout scheduler |
+| **attendance** | `apps/backend/src/modules/attendance/` | Daily attendance sessions, batch submit, calendar dates, summaries |
+| **announcements** | `apps/backend/src/modules/announcements/` | School-wide or class-scoped notices with image upload |
+| **messages** | `apps/backend/src/modules/messages/` | Inbox, threads, contacts, send/read, AI draft suggestion |
+| **complaints** | `apps/backend/src/modules/complaints/` | Parent files → admin triages → staff assignment → resolution |
+| **lost-found** | `apps/backend/src/modules/lost-found/` | Found items (posted by staff), lost reports (filed by parents) |
+| **results** | `apps/backend/src/modules/results/` | Terms, result sets per class/term, modules + student grades, publish/unpublish |
+| **rag** | `apps/backend/src/modules/rag/` | Policy PDF ingestion (chunk + embed), hybrid search, LLM answer generation, eval logs |
+
+### Frontend Web Feature Modules (10)
+
+| Module | Path | Used By |
+|---|---|---|
+| **auth** | `apps/frontend/src/features/auth/` | All roles |
+| **attendance** | `apps/frontend/src/features/attendance/` | Admin, Teacher |
+| **gate** | `apps/frontend/src/features/gate/` | Security, Admin |
+| **messages** | `apps/frontend/src/features/messages/` | All roles |
+| **notices** | `apps/frontend/src/features/notices/` | Admin, Teacher |
+| **complaints** | `apps/frontend/src/features/complaints/` | Admin, Teacher |
+| **results** | `apps/frontend/src/features/results/` | Teacher |
+| **rag** | `apps/frontend/src/features/rag/` | Admin (policy chatbot in overview) |
+| **school** | `apps/frontend/src/features/school/` | Admin |
+
+### Flutter Mobile Feature Modules (15)
+
+| Module | Path | Description |
+|---|---|---|
+| **splash** | `lib/features/splash/` | Splash + welcome onboarding screen |
+| **auth** | `lib/features/auth/` | Login, multi-step registration with OTP, forgot password, biometric login |
+| **dashboard** | `lib/features/dashboard/` | Home grid with action cards, animated bubble effects, announcements carousel, live clock |
+| **profile** | `lib/features/profile/` | Parent + child + teacher info, profile setup |
+| **attendance** | `lib/features/attendance/` | Paginated attendance records for child |
+| **gate** | `lib/features/gate/` | Child's gate IN/OUT history + currently-inside-school status |
+| **results** | `lib/features/results/` | Per-term grades, modules, average score, grade letter (A+, A, B+...) |
+| **messages** | `lib/features/messages/` | Inbox + contacts tab, conversations with teachers |
+| **contact_teacher** | `lib/features/contact_teacher/` | Direct chat with class teacher |
+| **support** | `lib/features/support/` | File complaints/suggestions, view status (pending/assigned/resolved) |
+| **notices** | `lib/features/notices/` | School & class announcements |
+| **notifications** | `lib/features/notifications/` | Notification history |
+| **chatbot** | `lib/features/chatbot/` | RAG-powered policy Q&A |
+| **lost_and_found** | `lib/features/lost_and_found/` | Lost item reporting (stub) |
+| **settings** | `lib/features/settings/` | Theme, logout, preferences |
+
+---
+
+## Tech Stack Summary
+
+### Backend
+
+| Layer | Technology | Version |
+|---|---|---|
+| **Runtime** | Node.js | 20-alpine |
+| **Framework** | Fastify | 5.7.4 |
+| **Language** | TypeScript | 5.9.3 (strict mode, ES2020) |
+| **Database** | PostgreSQL via Supabase | with `pgvector` & `uuid-ossp` |
+| **ORM** | Prisma | 6.16.3 |
+| **Cache** | Redis (optional) | 7-alpine |
+| **Validation** | Zod | 4.3.6 |
+| **Auth** | JWT (jsonwebtoken) + bcryptjs | 9.0.3 / 3.0.3 |
+| **AI** | Google Gemini, Anthropic Claude SDK | 0.24.1 / 0.93.0 |
+| **Email** | Resend | 6.12.2 |
+| **Push** | Firebase Admin | 13.8.0 |
+| **Logging** | Pino + pino-pretty | 10.3.1 / 13.1.3 |
+| **Scheduling** | node-cron | 4.2.1 |
+| **PDF** | pdf-parse | 2.4.5 |
+| **Testing** | Jest + ts-jest + supertest | 29.7.0 / 29.4.9 / 7.2.2 |
+
+### Frontend (Next.js)
+
+| Layer | Technology | Version |
+|---|---|---|
+| **Framework** | Next.js (App Router) | 16.1.6 |
+| **UI Library** | React | 19.2.3 |
+| **Language** | TypeScript | 5.x (strict) |
+| **Styling** | Tailwind CSS | 3.4.19 |
+| **Forms** | react-hook-form + Zod | 7.73.1 / 4.3.6 |
+| **Charts** | Recharts | 3.8.1 |
+| **Animation** | Framer Motion | 12.38.0 |
+| **Icons** | lucide-react | 1.8.0 |
+| **QR** | qrcode + react-qr-code | 1.5.4 / 2.0.18 |
+| **QR Scan** | @zxing/browser + @zxing/library | 0.2.0 / 0.22.0 |
+| **Database SDK** | @supabase/supabase-js | 2.105.1 |
+| **Lint** | ESLint 9 + next/core-web-vitals + next/typescript | — |
+
+### Flutter (Mobile)
+
+| Layer | Technology | Version |
+|---|---|---|
+| **Framework** | Flutter | 3.32.0 |
+| **Language** | Dart | SDK ^3.10.1 |
+| **State** | provider | 6.1.5 |
+| **Routing** | go_router | 16.2.1 |
+| **HTTP** | dio | 5.9.0 |
+| **Storage** | flutter_secure_storage + shared_preferences | 9.2.4 / 2.5.3 |
+| **Biometric** | local_auth | 3.0.1 |
+| **Firebase** | firebase_core + firebase_messaging | 3.0.0 / 15.0.0 |
+| **Notifications** | flutter_local_notifications | 21.0.0 |
+| **Fonts** | google_fonts (Plus Jakarta Sans) | 6.2.1 |
+| **OTP UI** | pinput | 6.0.2 |
+| **Animation** | animations | 2.0.11 |
+
+### DevOps
+
+| Tool | Purpose |
+|---|---|
+| **Docker Compose** | Local stack (Redis + Backend + Frontend) |
+| **GitHub Actions** | CI tests (ci.yml) + Flutter APK release (flutter-release.yml) |
+| **Husky** | Git hooks: pre-commit, pre-push, commit-msg |
+| **commitlint** | Conventional Commits enforcement |
+| **lint-staged** | Run ESLint --fix on staged files |
+| **Prettier** | Code formatting (singleQuote, 2 tab, 100 width) |
+| **Vercel** | Web frontend hosting (vercel.json) |
+| **Railway** | Backend production hosting (referenced in CI secrets) |
+| **Supabase Cloud** | Managed PostgreSQL + Auth + Storage |
+
+---
+
+## System Architecture Diagram
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ USER DEVICES │
+│ │
+│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
+│ │ Admin │ │ Teacher │ │ Security │ │ Parent │ │
+│ │ Browser │ │ Browser │ │ Browser │ │ Phone/App │ │
+│ │ (Next.js) │ │ (Next.js) │ │ (Next.js) │ │ (Flutter) │ │
+│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
+└─────────┼─────────────────┼─────────────────┼────────────────┼──────────┘
+ │ │ │ │
+ │ HTTPS/Cookies │ │ │ HTTPS/JWT
+ │ │ │ │
+ └─────────────────┴─────┬───────────┴────────────────┘
+ │
+ ┌───────────────▼────────────────┐
+ │ Fastify API (5000) │
+ │ ┌──────────────────────────┐ │
+ │ │ helmet + cors + rate-lim │ │
+ │ ├──────────────────────────┤ │
+ │ │ authenticate + rbac │ │
+ │ ├──────────────────────────┤ │
+ │ │ 11 modules: │ │
+ │ │ auth, users, school, │ │
+ │ │ gate, attendance, │ │
+ │ │ announcements, messages, │ │
+ │ │ complaints, lost-found, │ │
+ │ │ results, rag │ │
+ │ └──────┬──────────┬────────┘ │
+ └─────────┼──────────┼───────────┘
+ │ │
+ ┌───────────▼─┐ ┌──▼──────────────┐
+ │ Redis 7 │ │ Prisma Client │
+ │ (cache, │ └──────┬──────────┘
+ │ sessions, │ │
+ │ online) │ ┌───────▼────────────┐
+ └─────────────┘ │ PostgreSQL │
+ │ (Supabase) │
+ │ + pgvector │
+ │ + uuid-ossp │
+ │ 20 models │
+ └────────────────────┘
+
+ ┌────────────────┐ ┌─────────────────┐
+ │ Gemini API │ │ Resend Email │
+ │ (embed + LLM) │ │ (OTP/Approval) │
+ └────────────────┘ └─────────────────┘
+
+ ┌────────────────┐ ┌─────────────────┐
+ │ Firebase FCM │ │ Supabase │
+ │ (push notif) │ │ Storage │
+ └────────────────┘ └─────────────────┘
+```
+
+---
+
+## Data Flow Across Apps
+
+### Example 1: Gate Scan → Parent Notification
+
+```
+1. Security Officer (Frontend /security/dashboard) scans QR code
+2. POST /api/gate/scan { student_id, direction:"IN", method:"qr" }
+3. Fastify: authenticate → rbac(['admin','teacher','security']) → controller
+4. Controller:
+ a. Prisma: insert GateEvent
+ b. Redis: cache invalidate (delCacheByPattern('gate:*'))
+ c. Fetch Student → ParentStudent[] → User.fcm_token
+ d. Firebase Admin: sendMulticastFcmNotification(tokens, "Child checked in", body)
+5. Parent (Flutter app):
+ - FCM listener (FirebaseMessaging.onMessage) fires
+ - flutter_local_notifications shows OS notification
+ - GateViewModel pulls latest events on next refresh
+```
+
+### Example 2: Parent Files Complaint
+
+```
+1. Parent (Flutter /support/submit) types complaint, selects category
+2. POST /api/complaints { category, description, student_id }
+3. Backend: authenticate → controller → Prisma.complaint.create({ status:'pending' })
+4. Admin (Frontend /admin/complaints):
+ - Sees new complaint with status badge
+ - PUT /api/complaints/:id/assign { assigned_to_id: teacherId }
+ - Status updates to 'assigned'
+5. Teacher (Frontend /teacher/complaints):
+ - Sees assigned complaint
+ - PUT /api/complaints/:id/status { status:'resolved', reply_note }
+6. Parent app refreshes — sees resolution + reply note in detail screen
+```
+
+### Example 3: RAG Chatbot Query
+
+```
+1. User (Flutter /chatbot OR Frontend /admin/overview/policies) types question
+2. POST /api/rag/query { question, top_k:5 }
+3. Rate limit: 10/min per IP
+4. Cache check: Redis key based on question hash → if hit, return cached
+5. Pipeline:
+ a. Embed query via Gemini gemini-embedding-2 (768-dim)
+ b. Hybrid search: pgvector cosine similarity + tsvector BM25
+ c. Re-rank top chunks by term frequency
+ d. Build prompt + call Gemini 2.5 Flash Lite
+ e. Cache answer for 1 hour
+ f. Log to EvaluationLog table (query, chunks, answer, confidence)
+6. Return { answer, sources, confidence }
+```
+
+---
+
+## Module-by-Module Working Style
+
+Every backend module follows the same **4-file pattern**:
+
+```
+modules//
+├── .routes.ts ← Fastify route registration
+├── .controller.ts ← Request handlers
+├── .service.ts ← Business logic (sometimes inline in controller for small modules)
+└── .schema.ts ← Zod validation schemas
+```
+
+### Why This Pattern?
+
+| File | Responsibility |
+|---|---|
+| **routes** | URL paths, HTTP method, schema validation in `preHandler`, RBAC guards, rate limits |
+| **controller** | Parse `request`, call service, format response, set status code |
+| **service** | Pure business logic — Prisma queries, external API calls, transformations |
+| **schema** | Zod schemas; types are inferred via `z.infer['body']` |
+
+### Frontend (Next.js) Pattern
+
+```
+src/features//
+├── lib/
+│ └── -api.ts ← fetch wrappers returning { ok, data } | { ok:false, error }
+├── components/ ← Feature-specific React components
+├── context/ ← Optional ContextProvider for shared state
+└── types/ ← TypeScript interfaces (mirror backend)
+```
+
+### Flutter Pattern (MVVM + Repository)
+
+```
+lib/features//
+├── models/ ← Pure data classes with fromJson/toJson
+├── repositories/ ← Abstract + Api implementation (Dio calls)
+├── viewmodels/ ← extends ChangeNotifier; loading/error/data state
+└── views/ ← Widget tree consuming ViewModel via Provider
+```
+
+---
+
+## Existing Documentation Map
+
+The repository already contains an extensive `Doc/` folder with **10 deep-dive references**. Use this guide alongside them:
+
+| File | Purpose |
+|---|---|
+| `Doc/00-master-project-reference.md` | Master reference: roles, terminology, glossary |
+| `Doc/01-system-architecture.md` | Architecture diagrams, deployment topology |
+| `Doc/02-database-schema.md` | Every table, every column, relationships |
+| `Doc/03-api-endpoints.md` | Detailed REST API reference |
+| `Doc/04-backend-structure.md` | Backend folder structure & patterns |
+| `Doc/05-realtime-channels.md` | Realtime + Firebase push channels |
+| `Doc/06-auth-flow.md` | Step-by-step auth flow by role |
+| `Doc/07-data-flow.md` | Feature-by-feature data movement |
+| `Doc/08-flutter-app-structure.md` | Flutter MVVM architecture |
+| `Doc/09-sprint-timeline.md` | Week-by-week development log |
+| `Doc/10-production-readiness.md` | Deployment checklist, security hardening |
+
+The 5 docs in `Documentation/` (this folder) are designed to be **standalone and comprehensive** — giving a reader the whole picture without needing the `Doc/` folder, while `Doc/` provides deeper drill-downs for specific topics.
+
+### Reading Order
+
+| If you want to... | Read |
+|---|---|
+| Understand the whole system | `Documentation/01-PROJECT-OVERVIEW.md` (this file) |
+| Build / modify the backend | `Documentation/02-BACKEND-DEEP-DIVE.md` |
+| Build / modify the web app | `Documentation/03-FRONTEND-WEB-APP.md` |
+| Build / modify the mobile app | `Documentation/04-FLUTTER-MOBILE-APP.md` |
+| Deploy / run locally / CI | `Documentation/05-DEVOPS-DOCKER-CICD.md` |
+
+---
+
+## Quick-Start Commands
+
+```bash
+# Install everything (npm workspaces)
+npm install
+
+# Generate Prisma client
+npm run prisma:generate --prefix apps/backend
+
+# Run dev (without Docker)
+npm run backend:dev # Terminal 1 — http://localhost:5000
+npm run frontend:dev # Terminal 2 — http://localhost:3000
+
+# Run dev (with Docker)
+docker compose -f docker-compose.dev.yml up --build
+
+# Flutter
+cd apps/flutter
+flutter pub get
+flutter run # Connects to http://10.0.2.2:5000 on emulator
+```
+
+---
+
+**Next:** [02-BACKEND-DEEP-DIVE.md](02-BACKEND-DEEP-DIVE.md) — Fastify, Prisma, all API endpoints, auth, RAG pipeline.
diff --git a/Documentation/02-BACKEND-DEEP-DIVE.md b/Documentation/02-BACKEND-DEEP-DIVE.md
new file mode 100644
index 0000000..c2fd3bc
--- /dev/null
+++ b/Documentation/02-BACKEND-DEEP-DIVE.md
@@ -0,0 +1,1028 @@
+# Universe System — Backend Deep Dive
+
+> **Document 2 of 5** — Fastify backend: every API endpoint, database schema, authentication, RAG pipeline, middleware, testing, and packages.
+
+**Location:** `apps/backend/`
+**Port:** `5000`
+**Entry:** `src/server.ts` → `src/app.ts`
+
+---
+
+## Table of Contents
+
+1. [Application Bootstrap](#application-bootstrap)
+2. [Folder Structure (Detailed)](#folder-structure-detailed)
+3. [Every Package & Its Role](#every-package--its-role)
+4. [Database Schema (Prisma)](#database-schema-prisma)
+5. [Authentication & Authorization](#authentication--authorization)
+6. [Middleware Stack](#middleware-stack)
+7. [Complete API Reference](#complete-api-reference)
+8. [The RAG Pipeline (How the Chatbot Works)](#the-rag-pipeline-how-the-chatbot-works)
+9. [Scheduled Jobs](#scheduled-jobs)
+10. [Caching Strategy (Redis)](#caching-strategy-redis)
+11. [External Integrations](#external-integrations)
+12. [Testing Strategy](#testing-strategy)
+13. [Environment Variables](#environment-variables)
+14. [NPM Scripts](#npm-scripts)
+15. [Error Handling & Response Format](#error-handling--response-format)
+
+---
+
+## Application Bootstrap
+
+### `src/server.ts` — Entry Point
+
+```typescript
+// Simplified responsibility flow
+1. Load env (validated by Zod in config/env.ts)
+2. Initialize Redis (graceful: skip if REDIS_URL not set)
+3. Initialize Firebase Admin (FCM)
+4. Build Fastify app via createApp() in app.ts
+5. Start cron schedulers (gate auto-checkout)
+6. app.listen({ port: env.PORT, host: '0.0.0.0' })
+```
+
+### `src/app.ts` — Fastify Configuration
+
+```typescript
+// What it does, in order:
+1. Create Fastify instance with Pino logger
+2. Register @fastify/cookie
+3. Register @fastify/multipart (5 MB per file limit)
+4. Register @fastify/cors (origins: WEB_URL, FLUTTER_ORIGIN, localhost:3000, localhost:3001)
+5. Register @fastify/helmet (CORP: cross-origin, CSP: disabled)
+6. Register @fastify/rate-limit (global: 100 req/min)
+7. Set custom error handler (errorHandler middleware)
+8. Register routes with prefixes:
+ /api/auth, /api/users, /api/school, /api/gate,
+ /api/attendance, /api/announcements, /api/messages,
+ /api/complaints, /api/lost-found, /api/results, /api/rag
+9. Add GET /health → { ok: true }
+```
+
+---
+
+## Folder Structure (Detailed)
+
+```
+apps/backend/
+├── src/
+│ ├── server.ts ← App startup
+│ ├── app.ts ← Fastify configuration
+│ │
+│ ├── config/
+│ │ ├── env.ts ← Zod env schema, fails loud on missing vars
+│ │ ├── prisma.ts ← PrismaClient singleton (dev: global to survive HMR)
+│ │ ├── redis.ts ← Redis client; optional, degrades gracefully
+│ │ ├── supabase.ts ← createClient public + admin (storage uploads)
+│ │ └── firebase.ts ← Firebase Admin init from FIREBASE_SERVICE_ACCOUNT_JSON
+│ │
+│ ├── common/
+│ │ ├── middleware/
+│ │ │ ├── authenticate.ts ← JWT verify, DB lookup with retry, Redis online status
+│ │ │ ├── rbac.ts ← Role guard: rbac(['admin','teacher'])
+│ │ │ └── errorHandler.ts ← Zod → 400, Fastify errors → statusCode, else 500
+│ │ └── utils/
+│ │ ├── jwt.ts ← generateToken({userId,role,email}), verifyToken
+│ │ ├── hash.ts ← bcrypt: hashPassword(10 rounds), comparePassword
+│ │ ├── otp.ts ← generateOTP() → 6-digit string
+│ │ ├── cache.ts ← getCache/setCache/delCache/delCacheByPattern
+│ │ ├── email.ts ← Resend: sendOtpEmail, sendApprovalEmail (3/10min per addr)
+│ │ └── response.ts ← successResponse(msg,data) / errorResponse(msg,err)
+│ │
+│ ├── modules/ ← 11 feature modules (see Module section)
+│ │ ├── auth/
+│ │ ├── users/
+│ │ ├── school/
+│ │ ├── gate/
+│ │ ├── attendance/
+│ │ ├── announcements/
+│ │ ├── messages/
+│ │ ├── complaints/
+│ │ ├── lost-found/
+│ │ ├── results/
+│ │ └── rag/
+│ │ ├── rag.routes.ts
+│ │ ├── rag.schema.ts
+│ │ ├── controllers/
+│ │ │ ├── ingest.controller.ts ← PDF upload → chunk → embed → store
+│ │ │ ├── query.controller.ts ← Full RAG pipeline
+│ │ │ ├── documents.controller.ts ← List/Delete policy PDFs
+│ │ │ └── eval.controller.ts ← Evaluation metrics
+│ │ └── services/
+│ │ ├── chunker.service.ts ← Recursive / character / markdown chunking
+│ │ ├── embedder.service.ts ← Gemini embeddings (768-dim, batch 100)
+│ │ ├── retrieval.service.ts ← Hybrid search + re-rank
+│ │ ├── generation.service.ts ← Gemini 2.5 Flash Lite answer gen
+│ │ └── eval.service.ts ← Log to EvaluationLog
+│ │
+│ └── scripts/
+│ └── seedAdmin.ts ← Bootstrap first admin user
+│
+├── prisma/
+│ ├── schema.prisma ← 20 models, 7 phases
+│ └── migrations/ ← Auto-generated history
+│
+├── tests/
+│ ├── setup.ts ← Jest mocks (Prisma, Redis, Firebase, Email)
+│ ├── unit/
+│ │ ├── hash.test.ts
+│ │ ├── jwt.test.ts
+│ │ ├── otp.test.ts
+│ │ └── schemas.test.ts
+│ └── integration/
+│ ├── auth.test.ts ← supertest against built app
+│ ├── attendance.test.ts
+│ └── gate.test.ts
+│
+├── .env.example ← All required env vars documented
+├── Dockerfile.dev ← Node 20 alpine, hot reload
+├── jest.config.js ← ts-jest preset, 70% coverage threshold
+├── nodemon.json ← Watch src/, exec ts-node src/server.ts
+├── tsconfig.json ← strict, ES2020, CommonJS
+├── tsconfig.build.json ← rootDir: src, outDir: dist
+└── package.json
+```
+
+---
+
+## Every Package & Its Role
+
+### Production Dependencies
+
+| Package | Version | What it does | Where it's used |
+|---|---|---|---|
+| `fastify` | 5.7.4 | Async-first HTTP framework | `app.ts`, all routes |
+| `@fastify/cookie` | 11.0.2 | Parse cookies (auth_token) | `app.ts`, login controller |
+| `@fastify/cors` | 11.2.0 | CORS plugin | `app.ts` |
+| `@fastify/helmet` | 13.0.2 | Security headers (X-Frame-Options, etc.) | `app.ts` |
+| `@fastify/jwt` | 10.0.0 | JWT plugin (alternative API) | Available, our code uses `jsonwebtoken` directly |
+| `@fastify/multipart` | 10.0.0 | File uploads (student photos, PDFs) | school routes, rag routes |
+| `@fastify/rate-limit` | 10.3.0 | Per-route + global limiting | `app.ts` global, auth & rag routes per-route |
+| `@prisma/client` | 6.16.3 | Auto-generated ORM client | Everywhere (`config/prisma.ts`) |
+| `prisma` | 6.16.3 | CLI for migrations/generate | npm scripts |
+| `@supabase/supabase-js` | 2.97.0 | Supabase SDK | `config/supabase.ts`, file storage |
+| `redis` | 4.7.1 | Redis client | `config/redis.ts`, `utils/cache.ts` |
+| `bcryptjs` | 3.0.3 | Password hashing | `utils/hash.ts` |
+| `jsonwebtoken` | 9.0.3 | JWT signing/verifying | `utils/jwt.ts` |
+| `zod` | 4.3.6 | Schema validation + TS inference | Every `*.schema.ts` |
+| `firebase-admin` | 13.8.0 | FCM push notifications, admin SDK | `config/firebase.ts`, gate controller |
+| `resend` | 6.12.2 | Transactional email | `utils/email.ts` |
+| `@google/generative-ai` | 0.24.1 | Gemini embeddings + LLM | rag services |
+| `@anthropic-ai/sdk` | 0.93.0 | Claude API (available, currently unused) | (future) |
+| `axios` | 1.14.0 | External HTTP calls | rag/eval services |
+| `node-cron` | 4.2.1 | Cron scheduler | `gate.scheduler.ts` |
+| `pdf-parse` | 2.4.5 | Extract text from PDFs | `ingest.controller.ts` |
+| `pino` | 10.3.1 | JSON logger (built into Fastify) | Auto-loaded by Fastify |
+| `pino-pretty` | 13.1.3 | Pretty-print logs in dev | dev only |
+| `dotenv` | 16.4.7 | Load `.env` files | Auto-loaded by env.ts |
+
+### Development Dependencies
+
+| Package | Purpose |
+|---|---|
+| `typescript@5.9.3` | Type checker / compiler |
+| `ts-node@10.9.2` | Run TS without compile step (dev) |
+| `ts-node-dev@2.0.0` | TS + watch + restart |
+| `nodemon@3.1.14` | File watcher (used by `npm run dev`) |
+| `jest@29.7.0` | Test runner |
+| `ts-jest@29.4.9` | Jest TypeScript preset |
+| `@types/jest@29.5.14` | Jest types |
+| `@types/node@22.x` | Node.js types |
+| `@types/bcryptjs`, `@types/jsonwebtoken` | Type stubs |
+| `supertest@7.2.2` | HTTP assertions for integration tests |
+| `@types/supertest@7.2.0` | Supertest types |
+| `ndb@1.1.5` | Node debugger UI |
+
+---
+
+## Database Schema (Prisma)
+
+**File:** `apps/backend/prisma/schema.prisma` (~444 lines, 20 models)
+**Database:** PostgreSQL via Supabase
+**Extensions:** `vector` (pgvector for embeddings), `uuid-ossp` (UUID gen)
+
+### Phase 1 — Authentication
+
+```prisma
+model User {
+ id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
+ email String @unique
+ password_hash String
+ role String // pending | teacher | security | admin | parent
+ requested_role String?
+ full_name String?
+ avatar_url String?
+ phone_number String?
+ fcm_token String? // for push notifications
+ user_id_no String? @unique // e.g., T-000001, S-000001, P-000001
+ gender String?
+ is_active Boolean @default(true)
+ is_suspended Boolean @default(false)
+ created_at DateTime @default(now()) @db.Timestamptz
+ updated_at DateTime @updatedAt @db.Timestamptz
+ last_seen DateTime? @db.Timestamptz
+
+ // relations (~13 inverse relations: classes_taught, parent_links, gate_scans, ...)
+ @@index([email])
+ @@index([role])
+ @@index([is_active])
+ @@index([role, is_active])
+}
+
+model OtpVerification {
+ id String @id @default(uuid())
+ email String
+ otp_code String // 6-digit
+ expires_at DateTime @db.Timestamptz
+ is_used Boolean @default(false)
+ attempts Int @default(0) // capped at 3
+ created_at DateTime @default(now()) @db.Timestamptz
+ @@index([email])
+}
+```
+
+### Phase 2 — Core (Profiles, Students, Classes)
+
+```prisma
+model SchoolGrade {
+ id String @id @default(uuid())
+ name String @unique // "Grade 10"
+ created_at DateTime @default(now())
+ classes Class[]
+}
+
+model Class {
+ id String @id @default(uuid())
+ school_grade_id String
+ name String // "10-A"
+ teacher_id String? // FK to User
+ subject String?
+ created_at DateTime @default(now())
+
+ school_grade SchoolGrade @relation(fields: [school_grade_id], references: [id])
+ teacher User? @relation("ClassTeacher", fields: [teacher_id], references: [id])
+ students Student[]
+ announcements Announcement[]
+ attendanceSessions AttendanceSession[]
+ result_sets ResultSet[]
+
+ @@unique([school_grade_id, name])
+ @@index([teacher_id])
+}
+
+model Student {
+ id String @id @default(uuid())
+ full_name String
+ date_of_birth DateTime? @db.Date
+ photo_url String?
+ student_id_no String @unique // shown on ID card
+ qr_code String @unique // payload for QR
+ class_id String?
+ gender String?
+ parent_email String?
+ parent_mobile String?
+ parent_name String?
+ fcm_token String?
+ is_parent_linked Boolean @default(false)
+ is_active Boolean @default(true)
+ // ...
+ class Class? @relation(fields: [class_id], references: [id])
+ parents ParentStudent[]
+ gate_events GateEvent[]
+ attendance_records AttendanceRecord[]
+ // ...
+}
+
+model ParentStudent {
+ id String @id @default(uuid())
+ parent_id String // FK User
+ student_id String @unique // 1 parent <-> 1 student
+ relation String? // "mother", "father", "guardian"
+ verified_via String? // 'school_records' | 'admin_override'
+ linked_at DateTime @default(now())
+}
+```
+
+### Phase 3 — Gate & Attendance
+
+```prisma
+model GateEvent {
+ id String @id @default(uuid())
+ student_id String
+ scanned_by_id String? // null if auto-checkout (cron)
+ direction String // "IN" | "OUT"
+ method String // "qr" | "manual"
+ manual_reason String?
+ timestamp DateTime @default(now()) @db.Timestamptz
+
+ @@index([student_id])
+ @@index([timestamp])
+ @@index([student_id, timestamp])
+}
+
+model AttendanceRecord {
+ id String @id @default(uuid())
+ student_id String
+ date DateTime @db.Date
+ status String // "present" | "absent" | "late" | "excused"
+ marked_by_id String?
+ session_id String?
+ remarks String?
+
+ @@unique([student_id, date])
+}
+
+model AttendanceSession {
+ id String @id @default(uuid())
+ class_id String
+ teacher_id String
+ date DateTime @db.Date
+ records AttendanceRecord[]
+ @@unique([class_id, date])
+}
+```
+
+### Phase 4 — Announcements, Messages, Complaints
+
+```prisma
+model Announcement {
+ id String @id @default(uuid())
+ author_id String
+ title String
+ content String @db.Text
+ image_url String?
+ scope String // "school_wide" | "class"
+ target String // "all" | "parents_only" | "staff_only" |
+ class_id String?
+}
+
+model Message {
+ id String @id @default(uuid())
+ sender_id String
+ receiver_id String
+ student_id String? // contextual: "about which student?"
+ content String @db.Text
+ is_read Boolean @default(false)
+ created_at DateTime @default(now())
+}
+
+model Complaint {
+ id String @id @default(uuid())
+ parent_id String // who filed
+ student_id String?
+ category String // "academic" | "teacher_conduct" | "facility" | "administrative" | "suggestion" | "other"
+ description String @db.Text
+ status String // "pending" | "assigned" | "resolved" | "rejected"
+ assigned_to_id String? // FK User
+ assigned_at DateTime?
+ reply_note String? @db.Text
+ resolved_by_id String?
+ created_at DateTime @default(now())
+ updated_at DateTime @updatedAt
+}
+```
+
+### Phase 5 — Lost & Found
+
+```prisma
+model LostFoundItem {
+ id String @id @default(uuid())
+ posted_by String // staff who found
+ item_name String
+ description String? @db.Text
+ photo_url String?
+ found_at String?
+ found_date DateTime?
+ status String @default("unclaimed") // "unclaimed" | "collected"
+ collected_at DateTime?
+}
+
+model LostFoundReport {
+ id String @id @default(uuid())
+ parent_id String
+ student_id String
+ item_name String
+ description String?
+ photo_url String?
+ date_lost DateTime?
+ status String @default("open") // "open" | "recovered"
+}
+```
+
+### Phase 6 — RAG (Policy Documents + Chatbot)
+
+```prisma
+model PolicyDocument {
+ id String @id @default(uuid())
+ uploaded_by String
+ file_name String
+ display_name String
+ storage_path String // Supabase Storage path
+ file_hash String? @unique // SHA-256, prevents duplicate uploads
+ is_processed Boolean @default(false)
+ chunk_count Int @default(0)
+ chunks DocumentChunk[]
+}
+
+model DocumentChunk {
+ id String @id @default(uuid())
+ document_id String
+ chunk_text String @db.Text
+ embedding Unsupported("vector(768)")? // pgvector
+ chunk_index Int
+ @@index([document_id])
+}
+
+model EvaluationLog {
+ id String @id @default(uuid())
+ query String @db.Text
+ retrieved_chunks Json // [{id, score, text}]
+ generated_answer String @db.Text
+ confidence_score Float?
+ sources Json // [{document, page, snippet}]
+ created_at DateTime @default(now())
+}
+```
+
+### Phase 7 — Results
+
+```prisma
+model Term {
+ id String @id @default(uuid())
+ label String // "Term 1 2026"
+ created_by String
+ result_sets ResultSet[]
+}
+
+model ResultSet {
+ id String @id @default(uuid())
+ class_id String
+ teacher_id String
+ term_id String
+ status String @default("draft") // "draft" | "published"
+ published_at DateTime?
+ modules ResultModule[]
+ grades StudentGrade[]
+ @@unique([class_id, term_id])
+}
+
+model ResultModule {
+ id String @id @default(uuid())
+ result_set_id String
+ name String // "Mathematics", "Science"
+ order_index Int
+ grades StudentGrade[]
+ @@unique([result_set_id, name])
+}
+
+model StudentGrade {
+ id String @id @default(uuid())
+ result_set_id String
+ module_id String
+ student_id String
+ score Float? // 0-100
+ @@unique([result_set_id, module_id, student_id])
+}
+```
+
+---
+
+## Authentication & Authorization
+
+### Strategy
+**JWT (7-day expiry)** + **bcrypt password hashing (10 rounds)** + **Role-Based Access Control (RBAC)**.
+
+### Auth Endpoints (`/api/auth`)
+
+| Method | Path | Rate Limit | Auth | Purpose |
+|---|---|---|---|---|
+| POST | `/register` | 5/min | — | Start registration (sends OTP) |
+| POST | `/verify-otp` | 10/min | — | Verify OTP code |
+| POST | `/complete-registration` | — | — | Parent 2FA + create user |
+| POST | `/login` | 15/min | — | Email + password → JWT |
+| POST | `/resend-otp` | 3/min | — | Resend OTP (60s cooldown) |
+| POST | `/forgot-password` | 5/min | — | Send password-reset OTP |
+| POST | `/reset-password` | 5/min | — | Reset with OTP + new pass |
+| PUT | `/change-password` | — | ✓ | Authenticated password change |
+| PUT | `/link-child` | — | — | Link parent → student (OTP gated) |
+| POST | `/refresh` | — | ✓ | New JWT for existing session |
+| POST | `/logout` | — | ✓ | Clear cookie |
+| POST | `/logout-all` | — | ✓ | Invalidate all sessions |
+| PUT | `/fcm-token` | — | ✓ | Update Firebase token |
+| GET | `/me` | — | ✓ | Current user + classes_taught |
+| GET | `/parent-profile` | — | ✓ Parent | Parent + child + teacher details |
+
+### Registration Flow — Non-Parent (Teacher/Security)
+
+```
+1. POST /api/auth/register
+ { email, password, full_name, phone_number, role:"teacher" }
+ → Generate OTP, store pending registration in Redis + in-memory Map (10 min TTL)
+ → sendOtpEmail(email, otp)
+ → 200 { message: "OTP sent" }
+
+2. POST /api/auth/verify-otp
+ { email, otp_code }
+ → Lookup OtpVerification (not expired, not used, < 3 attempts)
+ → Create User { role:"pending", requested_role:"teacher" }
+ → Mark OTP as used
+ → Generate JWT, set httpOnly cookie + return token
+ → 200 { token, user, requires_profile_setup: false }
+
+3. Admin sees in /admin/overview/pending-requests
+ → PUT /api/users/:id/promote → role:"teacher", is_active:true
+ → sendApprovalEmail(email, fullName, "teacher")
+```
+
+### Registration Flow — Parent (2FA)
+
+```
+1. POST /api/auth/register
+ { email, password, full_name, student_id_no }
+ → Validate student exists, parent_email matches, not already linked
+ → Store pending registration with student snapshot
+ → sendOtpEmail
+ → 200 { message: "OTP sent" }
+
+2. POST /api/auth/verify-otp
+ { email, otp_code }
+ → Mark OTP verified in Redis (DO NOT create user yet)
+ → 200 { student: {...}, requires_profile_setup: true }
+
+3. POST /api/auth/complete-registration ← 2nd factor: identity proof
+ { email, grade, class, admission_year, gender }
+ → Validate answers match school records for the student
+ → BEGIN TRANSACTION
+ INSERT User (role:"parent", user_id_no:"P-000001")
+ INSERT ParentStudent (parent_id, student_id, verified_via:"school_records")
+ → COMMIT
+ → Generate JWT
+ → 200 { token, user }
+```
+
+### JWT Claims
+
+```typescript
+{
+ userId: string; // User.id
+ role: string; // "admin" | "teacher" | "security" | "parent" | "pending"
+ email: string;
+}
+```
+
+**Secret:** `JWT_SECRET` env var (min 32 chars validated by Zod)
+**Expiry:** `7d`
+**Cookie:** `auth_token`, HttpOnly, Secure (prod), SameSite=Lax, maxAge 7d
+
+### Middleware Application
+
+```typescript
+// Example: gate routes
+fastify.post('/scan',
+ {
+ schema: { body: scanSchema },
+ preHandler: [
+ authenticate, // verify JWT, attach user to request
+ rbac(['admin', 'teacher', 'security']) // 403 if role not allowed
+ ]
+ },
+ scanController
+);
+```
+
+---
+
+## Middleware Stack
+
+### 1. `authenticate.ts`
+
+```typescript
+// File: src/common/middleware/authenticate.ts
+async function authenticate(request, reply) {
+ // 1. Extract token: Authorization "Bearer ..." OR cookie auth_token
+ // 2. verifyToken(token) → claims { userId, role, email }
+ // 3. Prisma.user.findUnique({ where:{id:userId} }) with retry (2x, 500ms backoff)
+ // 4. If user is_active=false, is_suspended=true, or role="pending" → 401/403
+ // 5. Set Redis online status: SETEX online: 35 "1"
+ // 6. request.user = { userId, role, email }
+}
+```
+
+**Returns 503** if database is unavailable after retries.
+
+### 2. `rbac.ts`
+
+```typescript
+// File: src/common/middleware/rbac.ts
+function rbac(allowed: string[]) {
+ return async (request, reply) => {
+ if (!allowed.includes(request.user.role)) {
+ return reply.status(403).send(errorResponse('Forbidden'));
+ }
+ };
+}
+```
+
+### 3. `errorHandler.ts`
+
+```typescript
+// Catches:
+ZodError → 400 { success:false, message:"Validation failed", error: issues[] }
+FastifyError → uses error.statusCode + error.message
+Other → 500 (dev includes stack trace)
+```
+
+### 4. Global Plugins (in `app.ts`)
+
+| Plugin | Config |
+|---|---|
+| `@fastify/cookie` | Default |
+| `@fastify/multipart` | `limits: { fileSize: 5_242_880 }` (5 MB) |
+| `@fastify/cors` | `origin: [WEB_URL, FLUTTER_ORIGIN, localhost:3000, localhost:3001]`, `credentials: true` |
+| `@fastify/helmet` | `crossOriginResourcePolicy: { policy: 'cross-origin' }`, `contentSecurityPolicy: false` |
+| `@fastify/rate-limit` | `max: 100`, `timeWindow: '1 minute'` |
+
+---
+
+## Complete API Reference
+
+### Health
+- `GET /health` → `{ ok: true }`
+
+### Users (`/api/users`)
+| Method | Path | Roles |
+|---|---|---|
+| GET | `/me` | All |
+| PUT | `/me` | All — update full_name, avatar_url, phone |
+| DELETE | `/me` | All — self-delete |
+| PUT | `/fcm-token` | All — update push token |
+| GET | `/pending` | admin |
+| GET | `/all` | admin |
+| GET | `/teachers` | admin |
+| PUT | `/:id/promote` | admin — pending → role |
+| PUT | `/:id/suspend` | admin |
+| PUT | `/:id/unsuspend` | admin |
+| DELETE | `/:id` | admin |
+
+### School (`/api/school`)
+| Method | Path | Roles | Purpose |
+|---|---|---|---|
+| POST | `/grades` | admin | Create grade |
+| GET | `/grades` | admin, teacher | List grades |
+| DELETE | `/grades/:id` | admin | Delete grade |
+| POST | `/classes` | admin | Create class |
+| GET | `/classes` | admin | List classes |
+| GET | `/grades-with-classes` | admin | Tree view |
+| PATCH | `/classes/:id` | admin | Edit class |
+| DELETE | `/classes/:id` | admin | Delete class |
+| GET | `/classes/:id/students` | admin, teacher | Students in class |
+| GET | `/students/next-id` | admin | Generate next student ID |
+| POST | `/students/photo` | admin | Upload photo (multipart) |
+| POST | `/students` | admin | Create student |
+| GET | `/students` | admin, teacher | List/filter students |
+| PATCH | `/students/:id` | admin | Edit student |
+| DELETE | `/students/:id` | admin | Delete student |
+| GET | `/overview/stats` | admin, teacher | Dashboard stats |
+| GET | `/overview/recent-activity` | admin | Activity feed |
+
+### Gate (`/api/gate`)
+| Method | Path | Roles | Notes |
+|---|---|---|---|
+| POST | `/scan` | admin, teacher, security | Creates GateEvent + FCM to parent |
+| GET | `/student` | admin, security, teacher | Lookup by student_id_no |
+| GET | `/search` | admin, security, teacher | Autocomplete |
+| GET | `/stats` | admin, security, teacher | "Checked in today" |
+| GET | `/stats/timeseries` | admin | range=today\|week\|30days |
+| GET | `/events` | admin, security, teacher | Recent scans |
+| GET | `/my-child-events` | parent | Child's history + current IN/OUT status |
+
+### Attendance (`/api/attendance`)
+| Method | Path | Roles |
+|---|---|---|
+| POST | `/mark` | admin, teacher |
+| GET | `/` | admin, teacher, security |
+| POST | `/session` | admin, teacher |
+| GET | `/session` | admin, teacher |
+| POST | `/session/submit` | admin, teacher — batch save |
+| GET | `/session/dates` | admin, teacher — for calendar |
+| GET | `/summary` | admin, teacher |
+| GET | `/my-child` | parent |
+
+### Announcements (`/api/announcements`)
+| Method | Path | Roles |
+|---|---|---|
+| POST | `/` | admin, teacher |
+| GET | `/` | All (filtered by scope/target) |
+| DELETE | `/:id` | admin or author |
+
+### Messages (`/api/messages`)
+| Method | Path |
+|---|---|
+| GET | `/inbox` |
+| GET | `/contacts` |
+| GET | `/thread/:userId` |
+| POST | `/send` |
+| PUT | `/:id/read` |
+| POST | `/ai-draft` |
+| DELETE | `/:id` |
+
+### Complaints (`/api/complaints`)
+| Method | Path | Notes |
+|---|---|---|
+| POST | `/` | Parent files |
+| GET | `/my` | Parent's own |
+| GET | `/all` | Admin |
+| GET | `/:id` | Admin / Parent / Assignee |
+| PUT | `/:id/assign` | Admin → staff |
+| PUT | `/:id/status` | Status flow: pending → assigned → resolved |
+
+### Lost & Found (`/api/lost-found`)
+| Method | Path | Notes |
+|---|---|---|
+| POST | `/items` | Staff posts found item |
+| GET | `/items` | Browse unclaimed |
+| PUT | `/items/:id/collected` | Mark collected |
+| DELETE | `/items/:id` | Remove |
+| POST | `/reports` | Parent reports lost |
+| GET | `/reports/my` | Parent's reports |
+| PUT | `/reports/:id/recovered` | Mark recovered |
+| GET | `/reports` | Staff view all |
+
+### Results (`/api/results`)
+| Method | Path | Roles |
+|---|---|---|
+| GET | `/terms` | teacher, admin |
+| POST | `/terms` | teacher, admin |
+| PATCH | `/terms/:termId` | teacher, admin |
+| DELETE | `/terms/:termId` | teacher, admin |
+| GET | `/class/:classId?term_id=...` | teacher, admin — auto-creates if missing |
+| PATCH | `/:resultSetId` | teacher — save modules + grades |
+| POST | `/:resultSetId/publish` | teacher |
+| POST | `/:resultSetId/unpublish` | teacher |
+| GET | `/my-child` | parent |
+
+### RAG (`/api/rag`)
+| Method | Path | Roles | Rate |
+|---|---|---|---|
+| POST | `/documents` | admin | — |
+| GET | `/documents` | admin | — |
+| DELETE | `/documents/:id` | admin | — |
+| POST | `/query` | admin, teacher, parent | 10/min |
+| GET | `/eval/report` | admin | — |
+
+---
+
+## The RAG Pipeline (How the Chatbot Works)
+
+### Ingestion (admin uploads policy PDF)
+
+```
+POST /api/rag/documents (multipart, file=)
+ ├─ Save to Supabase Storage → return storage_path
+ ├─ Compute SHA-256 hash → reject if already exists (file_hash UNIQUE)
+ ├─ INSERT PolicyDocument (is_processed=false)
+ ├─ Background: pdf-parse extract text
+ ├─ chunker.service.ts split into ~500-token chunks (recursive_character)
+ ├─ embedder.service.ts: Gemini "gemini-embedding-2" in batches of 100
+ │ → 3072-dim → TRUNCATE to first 768 dims (pgvector index limit)
+ ├─ INSERT DocumentChunk rows with embedding vector(768)
+ └─ UPDATE PolicyDocument SET is_processed=true, chunk_count=N
+```
+
+### Query (user asks question)
+
+```
+POST /api/rag/query { question, top_k:5 }
+ ├─ Rate limit check (10/min)
+ ├─ Cache lookup: Redis key sha256(question.lower)
+ │ → IF hit: return cached { answer, sources, confidence }
+ ├─ embedder.embedQuery(question) → 768-dim vector
+ ├─ retrieval.service.ts:
+ │ 1. pgvector: SELECT ... ORDER BY embedding <-> $query LIMIT k*2
+ │ 2. tsvector BM25: full-text search on chunk_text
+ │ 3. Merge + dedupe by chunk_id
+ │ 4. Re-rank: boost chunks with high term frequency of question words
+ │ 5. Return top_k (default 5)
+ ├─ generation.service.ts:
+ │ Build prompt:
+ │ "You are a school policy assistant. Use ONLY the context below.
+ │ If the answer is not in context, say 'I don't know'."
+ │ + retrieved chunks
+ │ Call Gemini "gemini-2.5-flash-lite"
+ │ Parse answer + extract source citations
+ ├─ eval.service.ts: INSERT EvaluationLog (query, chunks, answer, confidence)
+ ├─ Cache for 1 hour
+ └─ 200 { answer, sources:[{document, snippet, score}], confidence }
+```
+
+### Why 768 dims, not 3072?
+
+PgVector's HNSW/IVFFlat indexes have a **2000-dim cap**. Gemini's `gemini-embedding-2` returns 3072 dims with **Matryoshka representation learning**, meaning the first N dims contain the most important information. Truncating to 768 preserves ~95% of semantic quality while making vector indexing feasible.
+
+---
+
+## Scheduled Jobs
+
+### Gate Auto-Checkout — `gate.scheduler.ts`
+
+```typescript
+// Runs daily at 18:00 (6 PM) school time
+cron.schedule('0 18 * * *', async () => {
+ // Find all students with last GateEvent direction="IN" today
+ // INSERT GateEvent { student_id, direction:"OUT", method:"manual",
+ // manual_reason:"Auto-checkout at end of day", scanned_by_id:null }
+ // FCM notify parents: "Day ended — please verify your child is home"
+});
+```
+
+---
+
+## Caching Strategy (Redis)
+
+| Key Pattern | TTL | Purpose |
+|---|---|---|
+| `rag:answer:` | 3600s (1 hr) | RAG answer cache |
+| `pending_reg:` | ~600s | Pending registration during OTP window |
+| `online:` | 35s | Refreshed on every authenticated request — used to show "online now" |
+| `gate:stats:` | 60s | Dashboard stats; invalidated on scan |
+
+**Graceful degradation:** If `REDIS_URL` is not set, all `getCache`/`setCache` calls return null/no-op. App keeps working without cache.
+
+---
+
+## External Integrations
+
+| Service | Purpose | File |
+|---|---|---|
+| **Gemini API** | `gemini-embedding-2` (embeddings) + `gemini-2.5-flash-lite` (LLM) | `rag/services/embedder.service.ts`, `generation.service.ts` |
+| **Firebase Admin** | FCM push notifications: `sendFcmNotification`, `sendMulticastFcmNotification` | `config/firebase.ts` |
+| **Resend** | Transactional email; from `noreply@mg.methum.space` | `utils/email.ts` |
+| **Supabase JS** | Storage (student photos, PDFs) via admin client | `config/supabase.ts` |
+| **Anthropic SDK** | Imported, reserved for future use (message AI draft fallback) | — |
+
+---
+
+## Testing Strategy
+
+**Framework:** Jest 29 + ts-jest preset + supertest
+
+### Layout
+
+```
+tests/
+├── setup.ts ← Global jest.mock() for all externals
+├── unit/
+│ ├── hash.test.ts ← bcrypt round-trip, compare wrong password
+│ ├── jwt.test.ts ← sign + verify, expired tokens, tampered
+│ ├── otp.test.ts ← length, randomness
+│ └── schemas.test.ts ← Zod schema validation cases
+└── integration/
+ ├── auth.test.ts ← POST /api/auth/register edge cases
+ ├── attendance.test.ts ← Session create + batch submit
+ └── gate.test.ts ← Scan flow, parent notification mocking
+```
+
+### Mocks (`tests/setup.ts`)
+
+```typescript
+jest.mock('../src/config/prisma', () => ({
+ prisma: {
+ user: { findUnique: jest.fn(), create: jest.fn(), update: jest.fn() },
+ attendanceSession: { upsert: jest.fn() },
+ attendanceRecord: { findMany: jest.fn() },
+ gateLog: { create: jest.fn() }
+ }
+}));
+
+jest.mock('../src/config/firebase', () => ({
+ sendFcmNotification: jest.fn().mockResolvedValue(true),
+ sendMulticastFcmNotification: jest.fn().mockResolvedValue({ successCount: 1 })
+}));
+
+jest.mock('../src/utils/email', () => ({
+ sendOtpEmail: jest.fn().mockResolvedValue(true),
+ sendApprovalEmail: jest.fn().mockResolvedValue(true)
+}));
+```
+
+### Config (`jest.config.js`)
+
+```javascript
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ testMatch: ['**/tests/**/*.test.ts'],
+ setupFiles: ['/tests/setup.ts'],
+ collectCoverageFrom: [
+ 'src/common/utils/**/*.ts',
+ 'src/modules/auth/**/*.ts'
+ ],
+ coverageThreshold: { global: { lines: 70 } }
+};
+```
+
+### Running
+
+```bash
+npm run test # all
+npm run test:unit # tests/unit only
+npm run test:integration # tests/integration only
+npm run test:coverage # generate coverage/ folder
+npm run test:watch # watch mode
+```
+
+---
+
+## Environment Variables
+
+Defined in `.env.example` and validated by Zod in `src/config/env.ts` on startup.
+
+| Variable | Required | Description |
+|---|---|---|
+| `PORT` | No (5000) | HTTP port |
+| `NODE_ENV` | No | development \| production \| test |
+| `DATABASE_URL` | **Yes** | Pooled Postgres connection (PgBouncer) |
+| `DIRECT_URL` | **Yes** | Direct Postgres for Prisma migrations |
+| `REDIS_URL` | No | Redis connection; disables cache if absent |
+| `REDIS_DEFAULT_TTL` | No (120) | Default cache TTL seconds |
+| `JWT_SECRET` | **Yes** (≥32 chars) | JWT signing |
+| `SUPABASE_URL` | **Yes** | Supabase project URL |
+| `SUPABASE_ANON_KEY` | **Yes** | Public key |
+| `SUPABASE_SERVICE_KEY` | **Yes** | Service role (storage admin) |
+| `RESEND_API_KEY` | **Yes** | Email transactional |
+| `RESEND_FROM_EMAIL` | No | Default `noreply@mg.methum.space` |
+| `GEMINI_API_KEY` | **Yes** | Embeddings + LLM |
+| `ANTHROPIC_API_KEY` | No | Future Claude integration |
+| `OPENAI_API_KEY` | No | Optional fallback |
+| `FIREBASE_PROJECT_ID` | **Yes** | FCM |
+| `FIREBASE_SERVICE_ACCOUNT_JSON` | **Yes** | Stringified service account JSON |
+| `WEB_URL` | **Yes** | CORS allow origin |
+| `FLUTTER_ORIGIN` | **Yes** | CORS allow origin |
+
+---
+
+## NPM Scripts
+
+```json
+{
+ "dev": "nodemon",
+ "build": "tsc -p tsconfig.build.json",
+ "start": "node dist/server.js",
+ "lint": "echo 'No linting yet'",
+ "test": "jest",
+ "test:unit": "jest tests/unit",
+ "test:integration": "jest tests/integration",
+ "test:coverage": "jest --coverage",
+ "test:watch": "jest --watch",
+ "prisma:generate": "prisma generate",
+ "prisma:migrate:dev": "prisma migrate dev",
+ "prisma:migrate:deploy": "prisma migrate deploy",
+ "prisma:push": "prisma db push",
+ "prisma:studio": "prisma studio",
+ "postinstall": "prisma generate"
+}
+```
+
+---
+
+## Error Handling & Response Format
+
+### Success Envelope
+
+```json
+{
+ "success": true,
+ "message": "Login successful",
+ "data": { "token": "...", "user": { ... } }
+}
+```
+
+### Error Envelope
+
+```json
+{
+ "success": false,
+ "message": "Invalid credentials",
+ "error": "Specific detail (dev only stack trace)"
+}
+```
+
+### HTTP Status Codes
+
+| Code | Used For |
+|---|---|
+| 200 | OK |
+| 201 | Created |
+| 400 | Validation failure (Zod) |
+| 401 | Missing/invalid token |
+| 403 | Authenticated but role denied (RBAC) |
+| 404 | Not found |
+| 409 | Conflict (duplicate email, etc.) |
+| 429 | Rate limit exceeded |
+| 500 | Internal error |
+| 503 | Database temporarily unreachable |
+
+---
+
+**Next:** [03-FRONTEND-WEB-APP.md](03-FRONTEND-WEB-APP.md) — Next.js 16 admin/teacher/security dashboard.
diff --git a/Documentation/03-FRONTEND-WEB-APP.md b/Documentation/03-FRONTEND-WEB-APP.md
new file mode 100644
index 0000000..b93bf53
--- /dev/null
+++ b/Documentation/03-FRONTEND-WEB-APP.md
@@ -0,0 +1,741 @@
+# Universe System — Frontend Web App Deep Dive
+
+> **Document 3 of 5** — Next.js 16 + React 19 web dashboard for Admin, Teacher, and Security roles.
+
+**Location:** `apps/frontend/`
+**Port:** `3000`
+**Framework:** Next.js 16 App Router
+**Language:** TypeScript (strict)
+**Styling:** Tailwind CSS 3.4 + custom CSS
+
+---
+
+## Table of Contents
+
+1. [What Does the Frontend Do?](#what-does-the-frontend-do)
+2. [Tech Stack & Packages](#tech-stack--packages)
+3. [Folder Structure](#folder-structure)
+4. [Routing — Every Page Mapped](#routing--every-page-mapped)
+5. [Auth & Session Management](#auth--session-management)
+6. [State Management](#state-management)
+7. [API Integration Layer](#api-integration-layer)
+8. [Feature Modules Walkthrough](#feature-modules-walkthrough)
+9. [UI / Component System](#ui--component-system)
+10. [Styling Approach](#styling-approach)
+11. [Build & Deployment](#build--deployment)
+12. [Linting & Quality](#linting--quality)
+13. [Notable Patterns & Conventions](#notable-patterns--conventions)
+
+---
+
+## What Does the Frontend Do?
+
+The Next.js web app is the **staff-facing dashboard** for the Universe System. It is used by three roles:
+
+| Role | What they do |
+|---|---|
+| **Admin** | Approve registrations, manage students/teachers/classes, view system logs, run policy RAG, see complaints, monitor gate activity, send school-wide notices |
+| **Teacher** | Mark attendance, post class notices, message parents (with AI draft), publish term results, manage own classes |
+| **Security** | Scan QR at gate (camera + zxing), view recent gate logs, message admin |
+
+> Parents do **not** use this web app — they use the Flutter mobile app instead.
+
+---
+
+## Tech Stack & Packages
+
+### Core
+
+| Package | Version | What it does |
+|---|---|---|
+| `next` | 16.1.6 | App Router framework (SSR + RSC + Routing) |
+| `react` | 19.2.3 | UI library |
+| `react-dom` | 19.2.3 | DOM bindings |
+| `typescript` | 5.x | Static typing |
+
+### UI & Styling
+
+| Package | Version | What it does |
+|---|---|---|
+| `tailwindcss` | 3.4.19 | Utility-first styling |
+| `postcss` | latest | CSS processor (used by Tailwind) |
+| `clsx` | 2.1.1 | Conditional className strings |
+| `tailwind-merge` | 3.5.0 | Resolve Tailwind conflicts (`merge('p-4', 'p-6') === 'p-6'`) |
+| `framer-motion` | 12.38.0 | Page/component animations |
+| `lucide-react` | 1.8.0 | Open-source SVG icon set |
+
+### Forms & Validation
+
+| Package | Version | What it does |
+|---|---|---|
+| `react-hook-form` | 7.73.1 | Form state, validation, performance |
+| `zod` | 4.3.6 | Runtime + compile-time schemas (matches backend) |
+| `@hookform/resolvers` | 5.2.2 | Bridge Zod → react-hook-form |
+
+### Data Visualization
+
+| Package | Version | What it does |
+|---|---|---|
+| `recharts` | 3.8.1 | Charts (gate trends, attendance summary) |
+
+### QR / Barcode (used by Security dashboard)
+
+| Package | Version | What it does |
+|---|---|---|
+| `qrcode` | 1.5.4 | Render QR codes (for student ID cards) |
+| `react-qr-code` | 2.0.18 | React wrapper for QR rendering |
+| `@zxing/browser` | 0.2.0 | Webcam-based scanner |
+| `@zxing/library` | 0.22.0 | Core barcode library |
+
+### Backend Access
+
+| Package | Version | What it does |
+|---|---|---|
+| `@supabase/supabase-js` | 2.105.1 | Direct Supabase client (used for realtime; primary path goes through backend API) |
+| `redis` | 5.11.0 | Redis client (only used in Edge/route handlers if direct cache access needed) |
+
+### Dev Tooling
+
+| Package | Purpose |
+|---|---|
+| `eslint@9.x` | Linter |
+| `eslint-config-next` | Next.js + React Hooks rules |
+| `@types/react`, `@types/node`, `@types/react-dom` | TypeScript stubs |
+| `tailwindcss/postcss` | PostCSS preset |
+
+---
+
+## Folder Structure
+
+```
+apps/frontend/
+├── public/ ← Static files (favicons, images)
+├── src/
+│ ├── app/ ← Next.js App Router (folder = route segment)
+│ │ ├── layout.tsx ← Root layout: providers (AuthContext),
+│ │ ├── page.tsx ← / → redirect to /login
+│ │ ├── globals.css ← Tailwind directives + CSS variables
+│ │ ├── favicon.ico
+│ │ ├── not-found.tsx ← 404
+│ │ │
+│ │ ├── login/page.tsx ← Public login form
+│ │ ├── register/page.tsx ← Multi-step parent reg (also staff)
+│ │ ├── forgot-password/page.tsx
+│ │ ├── pending/page.tsx ← "Your account is pending admin approval"
+│ │ │
+│ │ ├── admin/
+│ │ │ ├── layout.tsx ← Admin-only wrapper, RBAC guard, sidebar
+│ │ │ ├── overview/
+│ │ │ │ ├── page.tsx ← Dashboard cards (stats, recent activity)
+│ │ │ │ ├── pending-requests/page.tsx ← Approve/reject users
+│ │ │ │ └── policies/page.tsx ← RAG chatbot
+│ │ │ ├── attendance/page.tsx
+│ │ │ ├── calendar/page.tsx
+│ │ │ ├── enrollments/page.tsx
+│ │ │ ├── complaints/page.tsx
+│ │ │ ├── logs/page.tsx
+│ │ │ ├── messages/page.tsx
+│ │ │ ├── notices/page.tsx
+│ │ │ ├── students/page.tsx
+│ │ │ ├── teachers/page.tsx
+│ │ │ └── profile/page.tsx
+│ │ │
+│ │ ├── teacher/
+│ │ │ ├── layout.tsx ← Teacher-only wrapper
+│ │ │ ├── overview/page.tsx
+│ │ │ ├── attendance/page.tsx
+│ │ │ ├── calendar/page.tsx
+│ │ │ ├── classes/page.tsx
+│ │ │ ├── complaints/page.tsx
+│ │ │ ├── lost-and-found/page.tsx
+│ │ │ ├── messages/page.tsx
+│ │ │ ├── notices/page.tsx
+│ │ │ ├── results/page.tsx
+│ │ │ └── profile/page.tsx
+│ │ │
+│ │ └── security/
+│ │ ├── layout.tsx
+│ │ ├── dashboard/page.tsx ← QR scanner + manual entry
+│ │ ├── logs/page.tsx
+│ │ ├── messages/page.tsx
+│ │ ├── overview/page.tsx
+│ │ └── profile/page.tsx
+│ │
+│ ├── core/ ← App-wide config
+│ │ ├── config/
+│ │ └── constants/
+│ │
+│ ├── features/ ← Feature modules
+│ │ ├── auth/
+│ │ │ ├── components/
+│ │ │ │ ├── auth-shell.tsx ← Shared layout (logo, carousel side panel)
+│ │ │ │ ├── login-form.tsx
+│ │ │ │ ├── register-form.tsx
+│ │ │ │ └── forgot-password-form.tsx
+│ │ │ ├── context/AuthContext.tsx ← Global { user, login, logout, loading }
+│ │ │ ├── lib/
+│ │ │ │ ├── auth-api.ts ← fetch wrappers
+│ │ │ │ ├── register-api.ts
+│ │ │ │ ├── register-schemas.ts ← Zod schemas
+│ │ │ │ └── validators.ts
+│ │ │ └── types/auth.ts
+│ │ │
+│ │ ├── attendance/lib/attendance-api.ts
+│ │ ├── gate/lib/gate-api.ts
+│ │ ├── messages/
+│ │ │ ├── components/MessagesContainer.tsx
+│ │ │ ├── context/UnreadMessagesContext.tsx
+│ │ │ └── lib/messages-api.ts
+│ │ ├── notices/lib/notices-api.ts
+│ │ ├── complaints/lib/complaints-api.ts
+│ │ ├── results/lib/results-api.ts
+│ │ ├── rag/lib/rag-api.ts
+│ │ └── school/lib/school-api.ts
+│ │
+│ ├── shared/ ← Cross-feature reusables
+│ │ ├── components/
+│ │ │ ├── auth/ ← AuthButton, AuthInput, GoogleButton, AuthCarousel
+│ │ │ │ └── register/ ← OtpInput, PasswordStrengthBar, StepIndicator, StepOne...
+│ │ │ ├── layout/
+│ │ │ │ ├── DashboardLayout.tsx ← Sidebar + topbar + content
+│ │ │ │ └── Header.tsx
+│ │ │ ├── admin/ ← StudentProfileCard, EditStudentModal, GradesHistory
+│ │ │ ├── ui/ ← Generic primitives (Button, Modal, Input, ...)
+│ │ │ └── AttendanceCalendar.tsx
+│ │ ├── hooks/useCachedFetch.ts
+│ │ └── lib/ ← cn(), formatDate(), etc.
+│ │
+│ └── custom.d.ts ← Module declarations for imports
+│
+├── .env.local ← Local environment (gitignored)
+├── eslint.config.mjs ← Flat config (ESLint 9)
+├── next.config.ts ← Next.js config
+├── tsconfig.json ← strict, paths aliases
+├── postcss.config.mjs
+├── tailwind.config.ts
+├── Dockerfile.dev
+└── package.json
+```
+
+---
+
+## Routing — Every Page Mapped
+
+The App Router uses the file system as routing convention. Each `page.tsx` is a route.
+
+### Public Routes
+
+| Route | Page | Purpose |
+|---|---|---|
+| `/` | redirects → `/login` | — |
+| `/login` | login-form | Email + password → JWT cookie |
+| `/register` | multi-step `RegisterFlow` | Step 1: details → Step 2: OTP → Step 3: pending |
+| `/forgot-password` | forgot-password-form | Send OTP → reset |
+| `/pending` | message page | Shown when `role==="pending"` |
+
+### Admin Routes (`/admin/*`)
+
+| Route | Page Purpose |
+|---|---|
+| `/admin/overview` | Stats dashboard (students, teachers, today's gate count, etc.) |
+| `/admin/overview/pending-requests` | List + promote/reject pending users |
+| `/admin/overview/policies` | RAG chatbot for school policy Q&A |
+| `/admin/attendance` | View attendance across all classes |
+| `/admin/calendar` | School-wide calendar |
+| `/admin/enrollments` | Create/edit student enrollments |
+| `/admin/complaints` | All complaints; assign to staff; view replies |
+| `/admin/logs` | System activity / audit log |
+| `/admin/messages` | Admin inbox |
+| `/admin/notices` | Create/delete school-wide notices |
+| `/admin/students` | Student CRUD with photo upload |
+| `/admin/teachers` | Teacher list, suspend/unsuspend |
+| `/admin/profile` | Personal admin profile |
+
+### Teacher Routes (`/teacher/*`)
+
+| Route | Page Purpose |
+|---|---|
+| `/teacher/overview` | Dashboard: my classes, today's tasks |
+| `/teacher/attendance` | Open class today → mark students → submit session |
+| `/teacher/calendar` | Class calendar |
+| `/teacher/classes` | List of classes I teach |
+| `/teacher/complaints` | Complaints assigned to me |
+| `/teacher/lost-and-found` | View/manage lost items |
+| `/teacher/messages` | Conversations with parents (AI draft helper) |
+| `/teacher/notices` | Post class notices |
+| `/teacher/results` | Term result entry: select term → class → modules → grades → publish |
+| `/teacher/profile` | Teacher profile |
+
+### Security Routes (`/security/*`)
+
+| Route | Page Purpose |
+|---|---|
+| `/security/overview` | Today's stats: checked-in count |
+| `/security/dashboard` | **QR scanner via webcam + manual student lookup** |
+| `/security/logs` | Recent gate events table |
+| `/security/messages` | Direct chat with admin |
+| `/security/profile` | Security profile |
+
+---
+
+## Auth & Session Management
+
+### `AuthContext.tsx` — Global Auth State
+
+```typescript
+// src/features/auth/context/AuthContext.tsx
+type AuthUser = {
+ userId: string;
+ email: string;
+ role: 'admin' | 'teacher' | 'security' | 'parent' | 'pending';
+ full_name?: string;
+ phone_number?: string;
+ avatar_url?: string;
+ gender?: string;
+ last_seen?: string;
+ classes_taught?: Array<{
+ id: string;
+ name: string;
+ school_grade: { id: string; name: string };
+ }>;
+};
+
+type AuthContextValue = {
+ user: AuthUser | null;
+ loading: boolean;
+ login: (email, password) => Promise<{ok:true} | {ok:false, error}>;
+ logout: () => Promise;
+ refresh: () => Promise;
+};
+```
+
+### Auth Flow
+
+```
+1. wraps children with
+2. AuthProvider on mount calls getMe() → /api/auth/me
+ - If 200 → setUser
+ - If 401 → user stays null
+3. Pages that need the user use `useAuth()` hook
+4. Role-specific layouts (admin/layout.tsx) read user and:
+ - If null && !loading → router.push('/login')
+ - If user.role !== 'admin' → router.push(`/${user.role}/overview`)
+```
+
+### Login Flow
+
+```
+1. User submits
+2. POST /api/auth/login (credentials:'include') → backend sets httpOnly cookie
+3. On success → context.refresh() → getMe()
+4. Router push to /{role}/overview
+```
+
+### Cookies vs Tokens
+
+The backend supports **both**:
+- HttpOnly cookie `auth_token` (preferred for browser security — protects against XSS)
+- `Authorization: Bearer ` header (used by Flutter)
+
+Frontend uses **cookies only**; all `fetch()` calls include `credentials: 'include'`.
+
+---
+
+## State Management
+
+| Concern | Approach |
+|---|---|
+| **Auth user** | React Context (`AuthContext`) |
+| **Unread message badge** | React Context (`UnreadMessagesContext`) |
+| **Form state** | `react-hook-form` (local to form) |
+| **Server data** | Custom `useCachedFetch` hook (small built-in cache) |
+| **Page-level state** | `useState` / `useReducer` |
+| **URL state** | `useSearchParams`, `useRouter` (Next.js) |
+
+There is **no Redux, Zustand, or Tanstack Query**. Data fetching is direct `fetch()` calls wrapped in feature-level API libraries.
+
+### `useCachedFetch` (`src/shared/hooks/useCachedFetch.ts`)
+
+A lightweight hook that:
+- Caches responses in-memory by URL key
+- Returns `{ data, error, loading, refresh }`
+- Re-fetches on mount unless cached value exists and isn't stale
+
+---
+
+## API Integration Layer
+
+### The `request` Pattern
+
+Every feature has a `*-api.ts` file with a typed `request()` helper:
+
+```typescript
+// src/features/auth/lib/auth-api.ts (abridged)
+type Response =
+ | { ok: true; data: T }
+ | { ok: false; status: number; error: string; retry_after?: number };
+
+async function request(path: string, init?: RequestInit): Promise> {
+ try {
+ const res = await fetch(`${API_URL}${path}`, {
+ ...init,
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json', ...init?.headers }
+ });
+ const body = await res.json();
+ if (!res.ok) {
+ return {
+ ok: false,
+ status: res.status,
+ error: body.message ?? 'Request failed',
+ retry_after: res.headers.get('retry-after') ? Number(res.headers.get('retry-after')) : undefined
+ };
+ }
+ return { ok: true, data: body.data };
+ } catch (e) {
+ return { ok: false, status: 0, error: 'Network error' };
+ }
+}
+
+export const loginUser = (email, password) =>
+ request<{ token: string; user: AuthUser }>('/api/auth/login', {
+ method: 'POST',
+ body: JSON.stringify({ email, password })
+ });
+
+export const getMe = () =>
+ request('/api/auth/me');
+```
+
+### Base URL
+
+```typescript
+const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:5000';
+```
+
+### Per-feature API libs
+
+| File | Exports |
+|---|---|
+| `features/auth/lib/auth-api.ts` | login, logout, getMe, register, verifyOtp, completeRegistration, forgotPassword, resetPassword |
+| `features/auth/lib/register-api.ts` | startRegistration, resendOtp |
+| `features/attendance/lib/attendance-api.ts` | createSession, submitSession, getSession, getDates, getSummary |
+| `features/gate/lib/gate-api.ts` | scan, search, getEvents, getStats, getTimeseries |
+| `features/messages/lib/messages-api.ts` | getInbox, getContacts, getThread, sendMessage, markRead, aiDraft |
+| `features/notices/lib/notices-api.ts` | createAnnouncement, listAnnouncements, deleteAnnouncement |
+| `features/complaints/lib/complaints-api.ts` | listAll, assignComplaint, updateStatus |
+| `features/results/lib/results-api.ts` | listTerms, createTerm, getOrCreateResultSet, saveResults, publish, unpublish |
+| `features/rag/lib/rag-api.ts` | listDocuments, uploadDocument, deleteDocument, queryRag, getEvalReport |
+| `features/school/lib/school-api.ts` | createGrade, listGrades, createClass, listClasses, createStudent, listStudents, uploadStudentPhoto |
+
+---
+
+## Feature Modules Walkthrough
+
+### `features/auth/` — Login + Multi-step Registration
+
+The registration flow has 3 steps managed by `RegisterFlow.tsx`:
+
+```
+StepOneDetails → StepTwoOtp → StepThreePending
+ (form) (6 boxes) (waiting page)
+```
+
+Components:
+- `OtpInput` — 6 individual input boxes, auto-advance on type, paste support
+- `PasswordStrengthBar` — Live visual indicator using regex rules
+- `StepIndicator` — Animated step dots
+
+### `features/messages/` — Real-time-ish messaging
+
+The `MessagesContainer` is a full inbox UI:
+- Left panel: contact list
+- Right panel: thread view + composer
+- `UnreadMessagesContext` exposes a count badge to sidebar
+- Polling (no WebSockets) — every N seconds the inbox refreshes
+- AI draft button calls `POST /api/messages/ai-draft` and inserts the suggested text
+
+### `features/rag/` — Policy Chatbot
+
+Used in `/admin/overview/policies`:
+- Upload PDF → uploads progress shown via custom progress UI
+- Ask question → calls `POST /api/rag/query`
+- Displays answer with source snippets (clickable to expand)
+
+### `features/gate/` — Security Dashboard QR Scanner
+
+`/security/dashboard` uses **@zxing/browser**:
+```typescript
+import { BrowserMultiFormatReader } from '@zxing/browser';
+
+const reader = new BrowserMultiFormatReader();
+reader.decodeFromConstraints(
+ { video: { facingMode: 'environment' } },
+ videoRef.current,
+ (result) => {
+ if (result) {
+ const qrPayload = result.getText();
+ scanGate(qrPayload, direction);
+ }
+ }
+);
+```
+
+When a scan succeeds:
+1. Decode QR → `qr_code` string
+2. Show student card (photo, name, class)
+3. Security clicks IN or OUT button
+4. `POST /api/gate/scan` → success animation → parent gets FCM push
+
+### `features/school/` — Admin Student & Class Management
+
+`/admin/students`:
+- List with search + pagination
+- Add student modal: form + photo upload (separate endpoint `/students/photo`)
+- `StudentProfileCard` shows details
+- `EditStudentModal` edits in place
+- `GradesHistory` shows result sets across terms
+
+`/admin/teachers`:
+- List, promote pending → teacher, suspend/unsuspend
+
+`/admin/enrollments`:
+- Tree view of `Grade → Classes → Students` from `/grades-with-classes` + drill-down
+
+### `features/results/` — Term Result Entry
+
+`/teacher/results`:
+- Select term (or create new via inline form)
+- Select class (only teacher's classes shown)
+- Backend auto-creates ResultSet (`GET /api/results/class/:classId?term_id=...`)
+- Editable grid: rows = students, columns = modules
+- Add/rename modules dynamically
+- PATCH `/api/results/:resultSetId` saves entire matrix
+- POST `/api/results/:resultSetId/publish` makes visible to parents
+
+---
+
+## UI / Component System
+
+### No Pre-Built Library
+
+The frontend deliberately does **not** use shadcn/ui, MUI, Chakra, or any UI kit. All components are **hand-rolled** to match the school's specific design (rounded cards, soft teal palette, generous whitespace).
+
+### Component Categories
+
+```
+shared/components/
+├── ui/ ← Primitives
+│ ├── Button.tsx ← Primary, secondary, outline, sizes
+│ ├── Input.tsx
+│ ├── Modal.tsx
+│ ├── Card.tsx
+│ ├── Badge.tsx
+│ ├── Avatar.tsx
+│ └── Toast.tsx
+├── auth/ ← Auth-page specifics
+│ ├── auth-button.tsx
+│ ├── auth-input.tsx
+│ ├── auth-checkbox.tsx
+│ ├── auth-carousel.tsx ← Side panel image carousel on login
+│ ├── auth-layout.tsx
+│ ├── google-button.tsx
+│ └── register/
+│ ├── OtpInput.tsx
+│ ├── PasswordStrengthBar.tsx
+│ ├── RegisterFlow.tsx ← orchestrates 3 steps
+│ ├── StepIndicator.tsx
+│ ├── StepOneDetails.tsx
+│ ├── StepTwoOtp.tsx
+│ └── StepThreePending.tsx
+├── layout/
+│ ├── DashboardLayout.tsx ← role-aware sidebar + topbar
+│ └── Header.tsx
+├── admin/
+│ ├── StudentProfileCard.tsx
+│ ├── AddStudentButton.tsx
+│ ├── EditStudentModal.tsx
+│ └── GradesHistory.tsx
+└── AttendanceCalendar.tsx ← Shared between admin & teacher
+```
+
+### Animations
+
+`framer-motion` is used for:
+- Page transitions (fade + slide)
+- Card hover states
+- Modal enter/exit
+- Toast notifications
+
+---
+
+## Styling Approach
+
+### Tailwind Configuration
+
+`tailwind.config.ts` extends:
+- Custom palette: `primary` (teal #1A3A44 mirroring Flutter), `accent`, `surface`, `muted`
+- Font family: `Kumbh Sans` (Google Fonts via `next/font`)
+- Custom shadows: `card`, `soft`
+- Custom border radii
+
+### `globals.css`
+
+```css
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --color-primary: #1A3A44;
+ --color-bg: #F0FBFF;
+ --color-text: #101820;
+ --radius-card: 16px;
+}
+```
+
+### Class Composition Helper
+
+```typescript
+// shared/lib/cn.ts
+import { clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export const cn = (...classes) => twMerge(clsx(...classes));
+```
+
+Used everywhere:
+```tsx
+
- setCompose({ open: true, editId: null })}
- className="inline-flex flex-shrink-0 items-center gap-1.5 rounded-[10px] bg-[#4f46e5] px-3 py-2 text-[12px] font-bold text-white transition-colors hover:bg-[#4338ca]"
- >
-
- Add Event
-
-
-
- {/* Source filter */}
-
- {(["all", "principal", "school", "mine"] as ("all" | EventSource)[]).map((src) => (
- setSourceFilter(src)}
- className={clsx(
- "rounded-full border px-3 py-1 text-[11px] font-bold transition-all",
- sourceFilter === src
- ? "border-[#0f172a] bg-[#0f172a] text-white"
- : "border-[#e2e8f0] bg-white text-[#64748b] hover:border-[#cbd5e1]"
- )}
- >
- {src === "all" ? "All"
- : src === "principal" ? "Principal"
- : src === "school" ? "School"
- : "My Events"}
-
- ))}
@@ -626,7 +335,7 @@ export default function TeacherCalendarPage() {
All clear
- No events for this day. Use + Add Event to schedule one.
+ No events for this day.
) : (
@@ -635,41 +344,13 @@ export default function TeacherCalendarPage() {
setCompose({ open: true, editId: ev.id })}
- onDelete={() => handleDelete(ev.id)}
/>
))}
)}
-
- {/* Panel footer hint */}
-
-
- Principal & School events are read-only. Hover your own events to edit or delete.
-
-
-
- {/* ── Compose modal ──────────────────────────────────────────────────── */}
- {compose.open && (
- setCompose({ open: false, editId: null })}
- onSave={handleSave}
- />
- )}
);
}
diff --git a/apps/frontend/src/features/calendar/lib/calendar-api.ts b/apps/frontend/src/features/calendar/lib/calendar-api.ts
new file mode 100644
index 0000000..74060e7
--- /dev/null
+++ b/apps/frontend/src/features/calendar/lib/calendar-api.ts
@@ -0,0 +1,53 @@
+import { request } from '../../../shared/lib/api-client';
+
+export interface CalendarEvent {
+ id: string;
+ title: string;
+ description: string | null;
+ start_time: string;
+ end_time: string;
+ location: string | null;
+ type: 'event' | 'holiday' | 'exam' | 'meeting';
+ created_by: string;
+ creator?: {
+ full_name: string | null;
+ };
+}
+
+export function getCalendarEvents() {
+ return request('/api/calendar');
+}
+
+export function createCalendarEvent(data: {
+ title: string;
+ description?: string;
+ start_time: string;
+ end_time: string;
+ location?: string;
+ type?: string;
+}) {
+ return request('/api/calendar', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ });
+}
+
+export function updateCalendarEvent(id: string, data: Partial<{
+ title: string;
+ description?: string;
+ start_time: string;
+ end_time: string;
+ location?: string;
+ type?: string;
+}>) {
+ return request(`/api/calendar/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ });
+}
+
+export function deleteCalendarEvent(id: string) {
+ return request<{ success: true }>(`/api/calendar/${id}`, {
+ method: 'DELETE',
+ });
+}
diff --git a/apps/frontend/src/shared/lib/api-client.ts b/apps/frontend/src/shared/lib/api-client.ts
new file mode 100644
index 0000000..c4fbfff
--- /dev/null
+++ b/apps/frontend/src/shared/lib/api-client.ts
@@ -0,0 +1,24 @@
+const BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:5000';
+
+export type ApiOk = { ok: true; data: T };
+export type ApiErr = { ok: false; status: number; error: string };
+export type ApiResult = ApiOk | ApiErr;
+
+export async function request(path: string, options: RequestInit = {}): Promise> {
+ try {
+ const hasBody = options.body !== undefined && options.body !== null;
+ const res = await fetch(`${BASE}${path}`, {
+ credentials: 'include',
+ headers: {
+ ...(hasBody ? { 'Content-Type': 'application/json' } : {}),
+ ...options.headers,
+ },
+ ...options,
+ });
+ const json = await res.json().catch(() => ({}));
+ if (res.ok) return { ok: true, data: (json.data ?? json) as T };
+ return { ok: false, status: res.status, error: json.error ?? json.message ?? 'UNKNOWN' };
+ } catch {
+ return { ok: false, status: 0, error: 'NETWORK_ERROR' };
+ }
+}
From f75dda9a3c3d9fdd69704099c41d89adab4880bd Mon Sep 17 00:00:00 2001
From: Methexx
Date: Fri, 15 May 2026 11:20:31 +0530
Subject: [PATCH 4/7] fix: flutter setting screens new wireups
---
.../src/modules/users/settings.schema.ts | 10 +
.../src/modules/users/users.controller.ts | 40 ++++
.../backend/src/modules/users/users.routes.ts | 2 +
apps/flutter/lib/app.dart | 4 +
.../lib/core/constants/app_routes.dart | 2 +
apps/flutter/lib/core/di/service_locator.dart | 6 +
.../lib/core/navigation/app_router.dart | 15 +-
.../auth/viewmodels/auth_viewmodel.dart | 27 +--
.../settings/models/settings_model.dart | 53 ++++++
.../repositories/settings_repository.dart | 42 +++++
.../viewmodels/settings_viewmodel.dart | 76 ++++++++
.../settings/views/privacy_policy_screen.dart | 80 ++++++++
.../settings/views/settings_screen.dart | 174 ++----------------
.../views/terms_of_service_screen.dart | 80 ++++++++
apps/flutter/lib/main.dart | 1 +
15 files changed, 438 insertions(+), 174 deletions(-)
create mode 100644 apps/backend/src/modules/users/settings.schema.ts
create mode 100644 apps/flutter/lib/features/settings/models/settings_model.dart
create mode 100644 apps/flutter/lib/features/settings/repositories/settings_repository.dart
create mode 100644 apps/flutter/lib/features/settings/viewmodels/settings_viewmodel.dart
create mode 100644 apps/flutter/lib/features/settings/views/privacy_policy_screen.dart
create mode 100644 apps/flutter/lib/features/settings/views/terms_of_service_screen.dart
diff --git a/apps/backend/src/modules/users/settings.schema.ts b/apps/backend/src/modules/users/settings.schema.ts
new file mode 100644
index 0000000..e7dfbe2
--- /dev/null
+++ b/apps/backend/src/modules/users/settings.schema.ts
@@ -0,0 +1,10 @@
+import { z } from 'zod';
+
+export const updateSettingsSchema = z.object({
+ pushNotifications: z.boolean().optional(),
+ attendanceAlerts: z.boolean().optional(),
+ gateAlerts: z.boolean().optional(),
+ resultAlerts: z.boolean().optional(),
+});
+
+export type UpdateSettingsInput = z.infer;
diff --git a/apps/backend/src/modules/users/users.controller.ts b/apps/backend/src/modules/users/users.controller.ts
index a1aef67..8584713 100644
--- a/apps/backend/src/modules/users/users.controller.ts
+++ b/apps/backend/src/modules/users/users.controller.ts
@@ -2,6 +2,7 @@ import { FastifyRequest, FastifyReply } from 'fastify';
import { prisma } from '../../config/prisma';
import { z } from 'zod';
import { UsersService } from './users.service';
+import { updateSettingsSchema } from './settings.schema';
const updateFcmTokenSchema = z.object({
fcm_token: z.string().min(1),
@@ -113,5 +114,44 @@ export const UsersController = {
request.log.error(error);
reply.status(500).send({ success: false, message: 'Failed to update FCM token' });
}
+ },
+
+ async getSettings(request: FastifyRequest, reply: FastifyReply) {
+ const user = (request as any).user;
+ let settings = await (prisma as any).userSettings.findUnique({
+ where: { userId: user.userId },
+ });
+
+ if (!settings) {
+ settings = await (prisma as any).userSettings.create({
+ data: { userId: user.userId },
+ });
+ }
+
+ return reply.status(200).send({ success: true, data: settings });
+ },
+
+ async updateSettings(request: FastifyRequest, reply: FastifyReply) {
+ try {
+ const user = (request as any).user;
+ const body = updateSettingsSchema.parse(request.body);
+
+ const settings = await (prisma as any).userSettings.upsert({
+ where: { userId: user.userId },
+ update: body,
+ create: {
+ ...body,
+ userId: user.userId,
+ },
+ });
+
+ return reply.status(200).send({ success: true, data: settings });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return reply.status(400).send({ success: false, message: 'Invalid input' });
+ }
+ request.log.error(error);
+ reply.status(500).send({ success: false, message: 'Failed to update settings' });
+ }
}
};
diff --git a/apps/backend/src/modules/users/users.routes.ts b/apps/backend/src/modules/users/users.routes.ts
index 0ad157d..b5efa04 100644
--- a/apps/backend/src/modules/users/users.routes.ts
+++ b/apps/backend/src/modules/users/users.routes.ts
@@ -19,6 +19,8 @@ export default async function usersRoutes(fastify: FastifyInstance) {
fastify.delete('/me', UsersController.deleteMe);
fastify.put('/fcm-token', UsersController.updateFcmToken);
+ fastify.get('/settings', UsersController.getSettings);
+ fastify.patch('/settings', UsersController.updateSettings);
// 2. ADMIN-ONLY ROUTES
// We use the `authorize` RBAC middleware in the preHandler array
diff --git a/apps/flutter/lib/app.dart b/apps/flutter/lib/app.dart
index e5e212d..6456e2a 100644
--- a/apps/flutter/lib/app.dart
+++ b/apps/flutter/lib/app.dart
@@ -11,6 +11,7 @@ import 'package:universe_app/features/attendance/viewmodels/attendance_viewmodel
import 'package:universe_app/features/results/viewmodels/results_viewmodel.dart';
import 'package:universe_app/features/support/viewmodels/support_viewmodel.dart';
import 'package:universe_app/features/notices/viewmodels/notices_viewmodel.dart';
+import 'package:universe_app/features/settings/viewmodels/settings_viewmodel.dart';
import 'package:universe_app/shared/themes/app_theme.dart';
class UniverseApp extends StatefulWidget {
@@ -24,6 +25,7 @@ class UniverseApp extends StatefulWidget {
required this.resultsViewModel,
required this.supportViewModel,
required this.noticesViewModel,
+ required this.settingsViewModel,
});
final AuthViewModel authViewModel;
@@ -34,6 +36,7 @@ class UniverseApp extends StatefulWidget {
final ResultsViewModel resultsViewModel;
final SupportViewModel supportViewModel;
final NoticesViewModel noticesViewModel;
+ final SettingsViewModel settingsViewModel;
@override
State createState() => _UniverseAppState();
@@ -72,6 +75,7 @@ class _UniverseAppState extends State with WidgetsBindingObserver {
ChangeNotifierProvider.value(value: widget.resultsViewModel),
ChangeNotifierProvider.value(value: widget.supportViewModel),
ChangeNotifierProvider.value(value: widget.noticesViewModel),
+ ChangeNotifierProvider.value(value: widget.settingsViewModel),
],
child: MaterialApp.router(
title: AppStrings.appName,
diff --git a/apps/flutter/lib/core/constants/app_routes.dart b/apps/flutter/lib/core/constants/app_routes.dart
index 031d09f..2e1be7e 100644
--- a/apps/flutter/lib/core/constants/app_routes.dart
+++ b/apps/flutter/lib/core/constants/app_routes.dart
@@ -20,6 +20,8 @@ class AppRoutes {
static const String settings = '/settings';
static const String notices = '/notices';
static const String notifications = '/notifications';
+ static const String privacyPolicy = '/settings/privacy';
+ static const String termsOfService = '/settings/terms';
// Support sub-routes
static const String submitComplaint = '/support/submit';
diff --git a/apps/flutter/lib/core/di/service_locator.dart b/apps/flutter/lib/core/di/service_locator.dart
index b25b36f..858e3b7 100644
--- a/apps/flutter/lib/core/di/service_locator.dart
+++ b/apps/flutter/lib/core/di/service_locator.dart
@@ -19,6 +19,8 @@ import 'package:universe_app/features/support/repositories/support_repository.da
import 'package:universe_app/features/support/viewmodels/support_viewmodel.dart';
import 'package:universe_app/features/notices/repositories/notices_repository.dart';
import 'package:universe_app/features/notices/viewmodels/notices_viewmodel.dart';
+import 'package:universe_app/features/settings/repositories/settings_repository.dart';
+import 'package:universe_app/features/settings/viewmodels/settings_viewmodel.dart';
class ServiceLocator {
ServiceLocator._();
@@ -44,6 +46,8 @@ class ServiceLocator {
late SupportViewModel supportViewModel;
late NoticesRepository noticesRepository;
late NoticesViewModel noticesViewModel;
+ late SettingsRepository settingsRepository;
+ late SettingsViewModel settingsViewModel;
Future setup() async {
apiClient = ApiClient(baseUrl: ApiConfig.baseUrl);
@@ -87,5 +91,7 @@ class ServiceLocator {
secureStorage: secureStorageService,
);
noticesViewModel = NoticesViewModel(repository: noticesRepository);
+ settingsRepository = SettingsRepository(apiClient.dio, secureStorageService);
+ settingsViewModel = SettingsViewModel(settingsRepository);
}
}
diff --git a/apps/flutter/lib/core/navigation/app_router.dart b/apps/flutter/lib/core/navigation/app_router.dart
index 642a4bd..a4a6765 100644
--- a/apps/flutter/lib/core/navigation/app_router.dart
+++ b/apps/flutter/lib/core/navigation/app_router.dart
@@ -25,6 +25,8 @@ import 'package:universe_app/features/profile/views/profile_screen.dart';
import 'package:universe_app/features/settings/views/settings_screen.dart';
import 'package:universe_app/features/notices/views/notices_screen.dart';
import 'package:universe_app/features/notifications/views/notifications_screen.dart';
+import 'package:universe_app/features/settings/views/privacy_policy_screen.dart';
+import 'package:universe_app/features/settings/views/terms_of_service_screen.dart';
CustomTransitionPage _slideTransition({
required LocalKey key,
@@ -204,12 +206,21 @@ class AppRouter {
key: state.pageKey,
child: const NoticesScreen(),
),
+ ),
+ ),
+ ),
+ GoRoute(
+ path: AppRoutes.privacyPolicy,
+ pageBuilder: (context, state) => _slideTransition(
+ key: state.pageKey,
+ child: const PrivacyPolicyScreen(),
+ ),
),
GoRoute(
- path: AppRoutes.notifications,
+ path: AppRoutes.termsOfService,
pageBuilder: (context, state) => _slideTransition(
key: state.pageKey,
- child: const NotificationsScreen(),
+ child: const TermsOfServiceScreen(),
),
),
],
diff --git a/apps/flutter/lib/features/auth/viewmodels/auth_viewmodel.dart b/apps/flutter/lib/features/auth/viewmodels/auth_viewmodel.dart
index 9b0097c..e5fe380 100644
--- a/apps/flutter/lib/features/auth/viewmodels/auth_viewmodel.dart
+++ b/apps/flutter/lib/features/auth/viewmodels/auth_viewmodel.dart
@@ -64,20 +64,25 @@ class AuthViewModel extends BaseViewModel {
final availableAndEnabled = await isBiometricAvailableAndEnabled();
if (!availableAndEnabled) return false;
- final authenticated = await _biometricService.authenticate(
- reason: 'Please authenticate to log in automatically',
- );
+ try {
+ final authenticated = await _biometricService.authenticate(
+ reason: 'Please authenticate to log in automatically',
+ );
- if (!authenticated) return false;
+ if (!authenticated) return false;
- final creds = await _secureStorage.getBiometricCredentials();
- if (creds == null) return false;
+ final creds = await _secureStorage.getBiometricCredentials();
+ if (creds == null) return false;
- return login(
- email: creds['email']!,
- password: creds['password']!,
- keepMeSignedIn: await _localStorage.getKeepMeSignedIn(),
- );
+ return login(
+ email: creds['email']!,
+ password: creds['password']!,
+ keepMeSignedIn: await _localStorage.getKeepMeSignedIn(),
+ );
+ } catch (e) {
+ setError('Biometric authentication failed or was canceled.');
+ return false;
+ }
}
Future login({
diff --git a/apps/flutter/lib/features/settings/models/settings_model.dart b/apps/flutter/lib/features/settings/models/settings_model.dart
new file mode 100644
index 0000000..2840e69
--- /dev/null
+++ b/apps/flutter/lib/features/settings/models/settings_model.dart
@@ -0,0 +1,53 @@
+class UserSettingsModel {
+ final String id;
+ final String userId;
+ final bool pushNotifications;
+ final bool attendanceAlerts;
+ final bool gateAlerts;
+ final bool resultAlerts;
+
+ UserSettingsModel({
+ required this.id,
+ required this.userId,
+ required this.pushNotifications,
+ required this.attendanceAlerts,
+ required this.gateAlerts,
+ required this.resultAlerts,
+ });
+
+ factory UserSettingsModel.fromJson(Map json) {
+ return UserSettingsModel(
+ id: (json['id'] ?? '').toString(),
+ userId: (json['userId'] ?? json['user_id'] ?? '').toString(),
+ pushNotifications: (json['pushNotifications'] ?? json['push_notifications'] ?? true) == true,
+ attendanceAlerts: (json['attendanceAlerts'] ?? json['attendance_alerts'] ?? true) == true,
+ gateAlerts: (json['gateAlerts'] ?? json['gate_alerts'] ?? true) == true,
+ resultAlerts: (json['resultAlerts'] ?? json['result_alerts'] ?? true) == true,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'pushNotifications': pushNotifications,
+ 'attendanceAlerts': attendanceAlerts,
+ 'gateAlerts': gateAlerts,
+ 'resultAlerts': resultAlerts,
+ };
+ }
+
+ UserSettingsModel copyWith({
+ bool? pushNotifications,
+ bool? attendanceAlerts,
+ bool? gateAlerts,
+ bool? resultAlerts,
+ }) {
+ return UserSettingsModel(
+ id: id,
+ userId: userId,
+ pushNotifications: pushNotifications ?? this.pushNotifications,
+ attendanceAlerts: attendanceAlerts ?? this.attendanceAlerts,
+ gateAlerts: gateAlerts ?? this.gateAlerts,
+ resultAlerts: resultAlerts ?? this.resultAlerts,
+ );
+ }
+}
diff --git a/apps/flutter/lib/features/settings/repositories/settings_repository.dart b/apps/flutter/lib/features/settings/repositories/settings_repository.dart
new file mode 100644
index 0000000..2acb505
--- /dev/null
+++ b/apps/flutter/lib/features/settings/repositories/settings_repository.dart
@@ -0,0 +1,42 @@
+import 'package:dio/dio.dart';
+import 'package:universe_app/core/storage/secure_storage.dart';
+import 'package:universe_app/features/settings/models/settings_model.dart';
+
+class SettingsRepository {
+ final Dio _dio;
+ final SecureStorageService _secureStorage;
+
+ SettingsRepository(this._dio, this._secureStorage);
+
+ Future