diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9767af9 --- /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.41.9' + 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..6479308 --- /dev/null +++ b/.github/workflows/flutter-release.yml @@ -0,0 +1,75 @@ +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.41.9' + channel: stable + cache: true + + - name: Inject google-services.json + run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > apps/flutter/android/app/google-services.json + + - name: Decode and write release keystore + run: | + echo '${{ secrets.KEYSTORE_BASE64 }}' | base64 --decode > apps/flutter/android/app/universe-release.jks + cat > apps/flutter/android/key.properties <> $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: | + Release APK — built from `release/development` + Commit: ${{ github.sha }} + files: apps/flutter/build/app/outputs/flutter-apk/app-release.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 + + ))} + + +
+
+ + setSearch(e.target.value)} placeholder="Search items..." className="pl-9 pr-4 py-2.5 rounded-xl border border-gray-200 text-[13px] font-medium outline-none focus:border-[#4f46e5] w-[200px]" /> +
+ +
+ + + + {/* Grid */} +
+ {isLoading ? ( +
+ ) : filtered.length === 0 ? ( +
+ +

No items found

+
+ ) : ( + filtered.map((item) => ( +
+
+ +
+ {item.type === "lost" ? "Lost" : "Found"} +
+
+ {item.status} +
+
+
+

{item.title}

+

{item.description}

+
+
{item.location}
+
{new Date(item.date).toLocaleDateString()}
+
+
+
+

{item.postedBy}

+ {item.postedByRole} +
+ +
+
+
+ )) + )} +
+ + {/* Modal placeholder - similar to teacher modal */} + {showModal && ( +
+
+
+

Post an Item

+ +
+
+ +
+ {(["lost", "found"] as ItemType[]).map((t) => ( + + ))} +
+
+
+ + setForm((p) => ({ ...p, title: e.target.value }))} placeholder="e.g. Black School Bag" className="px-4 py-3 rounded-xl border border-gray-200 text-[14px] outline-none focus:border-[#4f46e5]" /> +
+
+ + setForm((p) => ({ ...p, location: e.target.value }))} placeholder="e.g. Library" className="px-4 py-3 rounded-xl border border-gray-200 text-[14px] outline-none focus:border-[#4f46e5]" /> +
+ +
+
+ )} + + ); +} diff --git a/apps/frontend/src/app/admin/overview/policies/page.tsx b/apps/frontend/src/app/admin/overview/policies/page.tsx index 493b9b3..b1ec37c 100644 --- a/apps/frontend/src/app/admin/overview/policies/page.tsx +++ b/apps/frontend/src/app/admin/overview/policies/page.tsx @@ -12,6 +12,7 @@ import { Search, Trash2, X, + XCircle, } from "lucide-react"; import { createClass, @@ -457,7 +458,7 @@ export default function PoliciesPage() { if (!res.ok) { alert(res.error); return; } // Add optimistic row — will show as processing until page refresh setDocuments((prev) => [ - { id: res.data.id, file_name: file.name, display_name: res.data.display_name, is_processed: false, chunk_count: 0, created_at: new Date().toISOString() }, + { id: res.data.id, file_name: file.name, display_name: res.data.display_name, status: "processing", error_message: null, chunk_count: 0, created_at: new Date().toISOString() }, ...prev, ]); // Poll once after 5s to update processed status @@ -646,16 +647,23 @@ export default function PoliciesPage() {

{doc.file_name}

- {doc.is_processed ? doc.chunk_count : "—"} + {doc.status === "completed" ? doc.chunk_count : "—"} {new Date(doc.created_at).toLocaleDateString()} - {doc.is_processed ? ( + {doc.status === "completed" ? ( Ready + ) : doc.status === "failed" ? ( + + Failed + ) : ( Processing diff --git a/apps/frontend/src/app/admin/profile/page.tsx b/apps/frontend/src/app/admin/profile/page.tsx index 299b203..abd26a2 100644 --- a/apps/frontend/src/app/admin/profile/page.tsx +++ b/apps/frontend/src/app/admin/profile/page.tsx @@ -3,7 +3,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { PageHeader } from '@/shared/components/layout/PageHeader'; import { TextInput } from '@/shared/components/ui/forms/TextInput'; -import { Eye, Edit2, Loader2, Check } from 'lucide-react'; +import { Edit2, Loader2, Check } from 'lucide-react'; import { useAuth } from '@/features/auth/context/AuthContext'; import { updateProfile, changePassword, deleteMe } from '@/features/auth/lib/auth-api'; @@ -26,10 +26,6 @@ export default function ProfilePage() { }); const [isSaving, setIsSaving] = useState(false); - const [preferences, setPreferences] = useState({ - darkMode: false, - emailNotifications: false, - }); // Modals state const [isPasswordModalOpen, setPasswordModalOpen] = useState(false); @@ -71,10 +67,6 @@ export default function ProfilePage() { setFormData(prev => ({ ...prev, [name]: value })); }; - const handleToggle = (key: keyof typeof preferences) => { - setPreferences(prev => ({ ...prev, [key]: !prev[key] })); - }; - const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -205,52 +197,6 @@ export default function ProfilePage() {

{user.email}

- {/* Preferences Card */} -
-

Preferences

-

Customize ur web experience

- -
- {/* Dark Mode Toggle */} -
-
-
- -
-
-

Dark Mode

-

Customize ur web experience

-
-
- -
- - {/* Email Notifications Toggle */} -
-
-
- -
-
-

Email Notifications

-

Customize ur web experience

-
-
- -
-
-
- {/* Right Column */} diff --git a/apps/frontend/src/app/admin/students/page.tsx b/apps/frontend/src/app/admin/students/page.tsx index e5f12b1..bc142c6 100644 --- a/apps/frontend/src/app/admin/students/page.tsx +++ b/apps/frontend/src/app/admin/students/page.tsx @@ -6,11 +6,8 @@ import { PageHeader } from '@/shared/components/layout/PageHeader'; import { TabSelector } from '@/shared/components/ui/TabSelector'; import { FilterBar } from '@/shared/components/ui/FilterBar'; import { DirectoryTable } from '@/shared/components/ui/DirectoryTable'; -import { Calendar as CalendarIcon, CheckCircle2, XCircle } from 'lucide-react'; -import clsx from 'clsx'; import { StudentProfileCard } from './components/StudentProfileCard'; import { AddStudentButton } from './components/AddStudentButton'; -import { GradesHistory } from './components/GradesHistory'; import { EditStudentModal, Student } from './components/EditStudentModal'; import { getStudents, type StudentRecord, updateStudent, deleteStudent as deleteStudentApi } from '@/features/school/lib/school-api'; import { cacheGet, cacheSet } from '@/shared/lib/local-cache'; @@ -48,7 +45,7 @@ function mapApiStudentToStudent(s: StudentRecord): Student { export default function StudentsPage() { const searchParams = useSearchParams(); - const [activeTab, setActiveTab] = useState<'general' | 'attendance' | 'grades'>('general'); + const [activeTab, setActiveTab] = useState<'general'>('general'); const [selectedStudentId, setSelectedStudentId] = useState(''); const [students, setStudents] = useState([]); @@ -65,7 +62,7 @@ export default function StudentsPage() { const cached = cacheGet('students'); if (cached && cached.length > 0) { setStudents(cached); - setSelectedStudentId(cached[0].id); + setSelectedStudentId(prev => prev || cached[0].id); setIsLoading(false); } else { setIsLoading(true); @@ -77,10 +74,10 @@ export default function StudentsPage() { const mapped = res.data.map(mapApiStudentToStudent); setStudents(mapped); cacheSet('students', mapped, 300); - if (mapped.length > 0 && !selectedStudentId) setSelectedStudentId(mapped[0].id); + if (mapped.length > 0) setSelectedStudentId(prev => prev || mapped[0].id); } setIsLoading(false); - }, [selectedStudentId]); + }, []); // eslint-disable-next-line react-hooks/set-state-in-effect useEffect(() => { void fetchData(); }, [fetchData]); @@ -159,12 +156,10 @@ export default function StudentsPage() {

Directory

setActiveTab(id as 'general' | 'attendance' | 'grades')} + onTabChange={(id) => setActiveTab(id as 'general')} /> @@ -248,70 +243,6 @@ export default function StudentsPage() { )} -{activeTab === 'attendance' && ( -
-
- , options: [{label: 'April 2024', value: 'apr'}], value: '', onChange: () => {} }, - { id: 'week', label: 'Week', options: [{label: 'Week 1', value: 'w1'}], value: '', onChange: () => {} }, - { id: 'class', label: 'Class', options: [{label: '10-A', value: '10a'}], value: '', onChange: () => {} } - ]} - /> -
- -
- - - - - {[8,9,10,11,12,13,14,15,16,17,18,19,20,21].map(day => ( - - ))} - - - - {filteredStudents.length > 0 ? filteredStudents.map((student, rowIndex) => { - const isSelected = student.id === selectedStudentId; - return ( - setSelectedStudentId(student.id)} className={clsx("cursor-pointer transition-colors hover:bg-gray-50/50", isSelected && "bg-blue-50/50")}> - - {[8,9,10,11,12,13,14,15,16,17,18,19,20,21].map((day) => ( - - ))} - - )}) : ( - - - - )} - -
Student ID - {String(day).padStart(2, '0')} -
- {student.name} {student.id} - -
- {(day + rowIndex) % 5 === 0 || (day === 10 && rowIndex % 2 !== 0) ? - : - - } -
-
- No logs found matching your criteria. -
-
-
-)} - -{activeTab === 'grades' && ( -
- -
-)} diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index 2b6ca0a..a85043e 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Kumbh_Sans } from "next/font/google"; import "./globals.css"; import { AuthProvider } from "@/features/auth/context/AuthContext"; import { UnreadMessagesProvider } from "@/features/messages/context/UnreadMessagesContext"; +import { Toaster } from "react-hot-toast"; const kumbhSans = Kumbh_Sans({ variable: "--font-kumbh-sans", @@ -27,7 +28,10 @@ export default function RootLayout({ suppressHydrationWarning > - {children} + + {children} + + diff --git a/apps/frontend/src/app/security/profile/page.tsx b/apps/frontend/src/app/security/profile/page.tsx index abd6be3..23a95ed 100644 --- a/apps/frontend/src/app/security/profile/page.tsx +++ b/apps/frontend/src/app/security/profile/page.tsx @@ -3,7 +3,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { PageHeader } from '@/shared/components/layout/PageHeader'; import { TextInput } from '@/shared/components/ui/forms/TextInput'; -import { Eye, Edit2, Loader2, Check, Shield } from 'lucide-react'; +import { Edit2, Loader2, Check, Shield } from 'lucide-react'; import { useAuth } from '@/features/auth/context/AuthContext'; import { updateProfile, changePassword, deleteMe } from '@/features/auth/lib/auth-api'; @@ -26,10 +26,6 @@ export default function SecurityProfilePage() { }); const [isSaving, setIsSaving] = useState(false); - const [preferences, setPreferences] = useState({ - darkMode: false, - emailNotifications: true, - }); // Modals state const [isPasswordModalOpen, setPasswordModalOpen] = useState(false); @@ -71,10 +67,6 @@ export default function SecurityProfilePage() { setFormData(prev => ({ ...prev, [name]: value })); }; - const handleToggle = (key: keyof typeof preferences) => { - setPreferences(prev => ({ ...prev, [key]: !prev[key] })); - }; - const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -209,52 +201,6 @@ export default function SecurityProfilePage() {
- {/* Preferences Card */} -
-

Preferences

-

Customize your dashboard

- -
- {/* Dark Mode Toggle */} -
-
-
- -
-
-

Dark Mode

-

Enable dark theme

-
-
- -
- - {/* Email Notifications Toggle */} -
-
-
- -
-
-

Security Alerts

-

Receive critical updates

-
-
- -
-
-
- {/* Right Column */} diff --git a/apps/frontend/src/app/teacher/calendar/page.tsx b/apps/frontend/src/app/teacher/calendar/page.tsx index 3a129d5..c9c2f9f 100644 --- a/apps/frontend/src/app/teacher/calendar/page.tsx +++ b/apps/frontend/src/app/teacher/calendar/page.tsx @@ -1,21 +1,19 @@ "use client"; -import React, { useMemo, useRef, useState } from "react"; +import React, { useMemo, useState } from "react"; import clsx from "clsx"; import { CheckCircle2, ChevronLeft, ChevronRight, Clock, - Edit2, - Megaphone, - Plus, Shield, - Trash2, - Users, - X, } from "lucide-react"; import { PageHeader } from "@/shared/components/layout/PageHeader"; +import { + getCalendarEvents, + CalendarEvent as ApiEvent +} from '@/features/calendar/lib/calendar-api'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -56,28 +54,9 @@ function parseTimeSort(time: string): number { return h * 60 + min; } -// ─── Mock Data ──────────────────────────────────────────────────────────────── - -const INITIAL_EVENTS: CalEvent[] = [ - // Principal events — read-only for teacher - { id:"p1", title:"Monthly Staff Meeting", date:fmt(2026,4,24), time:"9:00 AM", timeSort:540, source:"principal", category:"meeting", description:"All-staff sync. Agenda: Term 2 prep, attendance policy update, sports day coordination." }, - { id:"p2", title:"School Closure", date:fmt(2026,4,25), time:"All Day", timeSort:0, source:"principal", category:"holiday", description:"School closed — water supply issue. Normal schedule resumes April 26th." }, - { id:"p3", title:"Parent–Teacher Meeting", date:fmt(2026,5,3), time:"2:00 PM", timeSort:840, source:"principal", category:"meeting", description:"Grade 10 & 11 PTM. Attendance mandatory for all class teachers. 2:00 PM – 5:00 PM, Main Hall." }, - // School events — read-only - { id:"s1", title:"Term 2 Exams Begin", date:fmt(2026,5,15), time:"8:00 AM", timeSort:480, source:"school", category:"exam", description:"Term 2 examinations commence. All teachers to submit question papers by May 5th." }, - { id:"s2", title:"Vesak Holiday", date:fmt(2026,5,12), time:"All Day", timeSort:0, source:"school", category:"holiday", description:"Vesak Full Moon Poya Day — school closed." }, - { id:"s3", title:"Annual Sports Day", date:fmt(2026,5,22), time:"8:00 AM", timeSort:480, source:"school", category:"event", description:"Annual Sports Day. Duty teachers report by 7:30 AM. Volunteer sign-up due April 30." }, - { id:"s4", title:"Term 1 Results Published", date:fmt(2026,5,8), time:"10:00 AM", timeSort:600, source:"school", category:"event", description:"Term 1 results published on the parent portal. Notify parents via messaging." }, - // Teacher's own events — editable - { id:"m1", title:"10-A Science Lab Prep", date:fmt(2026,4,26), time:"11:00 AM", timeSort:660, source:"mine", category:"reminder", description:"Prepare materials for ecosystem lab experiment. Check microscopes from equipment room." }, - { id:"m2", title:"10-A Math Revision", date:fmt(2026,4,27), time:"3:00 PM", timeSort:900, source:"mine", category:"meeting", description:"Extra revision for Grade 10-A — Algebra & Calculus. Room 14." }, - { id:"m3", title:"11-B Progress Review", date:fmt(2026,4,29), time:"1:00 PM", timeSort:780, source:"mine", category:"meeting", description:"Internal review of 11-B student progress before the PTM. Prepare summary report." }, - { id:"m4", title:"Grade Submission Deadline",date:fmt(2026,5,5), time:"5:00 PM", timeSort:1020, source:"mine", category:"reminder", description:"Term 1 final grades must be submitted to admin by end of day." }, -]; - // ─── Config ─────────────────────────────────────────────────────────────────── -const CAT: Record = { meeting: { label:"Meeting", barColor:"bg-[#4f46e5]", chipBg:"bg-[#eef2ff]", chipText:"text-[#4f46e5]", dotColor:"bg-[#4f46e5]" }, @@ -87,27 +66,13 @@ const CAT: Record = { - principal: { label:"Principal", bg:"bg-amber-100", text:"text-amber-700", Icon:Shield }, - school: { label:"School", bg:"bg-[#f1f5f9]", text:"text-[#475569]", Icon:Megaphone }, - mine: { label:"My Event", bg:"bg-[#eef2ff]", text:"text-[#4f46e5]", Icon:Users }, -}; - -// ─── EventCard ──────────────────────────────────────────────────────────────── - function EventCard({ event, - onEdit, - onDelete, }: { event: CalEvent; - onEdit: () => void; - onDelete: () => void; }) { const [expanded, setExpanded] = useState(false); - const { barColor, chipBg, chipText, label: catLabel } = CAT[event.category]; - const { label: srcLabel, bg: srcBg, text: srcText, Icon: SrcIcon } = SRC[event.source]; - const canEdit = event.source === "mine"; + const { barColor, chipBg, chipText, label: catLabel } = CAT[event.category] || CAT.event; const longDesc = (event.description?.length ?? 0) > 90; return ( @@ -117,25 +82,6 @@ function EventCard({

{event.title}

- - {canEdit && ( -
- - -
- )}
@@ -146,9 +92,9 @@ function EventCard({ {catLabel} - - - {srcLabel} + + + Admin
@@ -173,144 +119,32 @@ function EventCard({ ); } -// ─── ComposeModal ───────────────────────────────────────────────────────────── - -type ComposeForm = { title: string; time: string; category: EventCategory; description: string }; - -const BLANK_FORM: ComposeForm = { title: "", time: "", category: "meeting", description: "" }; - -function ComposeModal({ - date, - initial, - onClose, - onSave, -}: { - date: string; - initial?: ComposeForm; - onClose: () => void; - onSave: (form: ComposeForm) => void; -}) { - const [form, setForm] = useState(initial ?? BLANK_FORM); - const canSave = form.title.trim().length > 0 && form.time.trim().length > 0; - - const displayDate = new Date(date + "T12:00:00").toLocaleDateString("en-US", { - weekday: "long", month: "long", day: "numeric", - }); - - return ( -
-
-
-
-

- {initial ? "Edit Event" : "Add Event"} -

-

{displayDate}

-
- -
- -
-
- - setForm((p) => ({ ...p, title: e.target.value }))} - placeholder="e.g. 10-A Revision Session" - className="w-full rounded-[10px] border border-[#e2e8f0] px-4 py-2.5 text-[13px] text-[#0f172a] outline-none placeholder:text-[#cbd5e1] focus:border-[#4f46e5]" - /> -
- -
- - setForm((p) => ({ ...p, time: e.target.value }))} - placeholder="e.g. 3:00 PM" - className="w-full rounded-[10px] border border-[#e2e8f0] px-4 py-2.5 text-[13px] text-[#0f172a] outline-none placeholder:text-[#cbd5e1] focus:border-[#4f46e5]" - /> -
- -
- -
- {(["meeting", "reminder", "exam", "event"] as EventCategory[]).map((cat) => { - const { label, chipBg, chipText } = CAT[cat]; - return ( - - ); - })} -
-
- -
- -