Praveen commited on
Commit
ae46d30
·
0 Parent(s):

Initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. civic-platform/.env.example +6 -0
  2. civic-platform/.gitignore +7 -0
  3. civic-platform/README.md +119 -0
  4. civic-platform/ai/README.md +15 -0
  5. civic-platform/ai/schemas/civic-image-record.example.json +11 -0
  6. civic-platform/ai/schemas/complaint-text-record.example.json +12 -0
  7. civic-platform/ai/training/README.md +73 -0
  8. civic-platform/ai/training/civic-image-labels.md +19 -0
  9. civic-platform/app/api/session/login/route.ts +77 -0
  10. civic-platform/app/api/session/logout/route.ts +10 -0
  11. civic-platform/app/api/session/register/route.ts +83 -0
  12. civic-platform/app/complaints/[id]/page.tsx +264 -0
  13. civic-platform/app/complaints/page.tsx +217 -0
  14. civic-platform/app/dashboard/analytics/page.tsx +184 -0
  15. civic-platform/app/dashboard/complaints/[id]/page.tsx +383 -0
  16. civic-platform/app/dashboard/complaints/page.tsx +113 -0
  17. civic-platform/app/dashboard/page.tsx +257 -0
  18. civic-platform/app/emergency/page.tsx +176 -0
  19. civic-platform/app/globals.css +61 -0
  20. civic-platform/app/layout.tsx +19 -0
  21. civic-platform/app/login/page.tsx +40 -0
  22. civic-platform/app/map/page.tsx +59 -0
  23. civic-platform/app/notifications/page.tsx +35 -0
  24. civic-platform/app/page.tsx +15 -0
  25. civic-platform/app/report/location/page.tsx +170 -0
  26. civic-platform/app/report/page.tsx +27 -0
  27. civic-platform/app/report/review/page.tsx +165 -0
  28. civic-platform/app/report/success/page.tsx +173 -0
  29. civic-platform/app/tasks/page.tsx +35 -0
  30. civic-platform/app/template.tsx +16 -0
  31. civic-platform/backend/.env.example +12 -0
  32. civic-platform/backend/README.md +38 -0
  33. civic-platform/backend/backend-dev.log +5 -0
  34. civic-platform/backend/backend-run.log +2 -0
  35. civic-platform/backend/backend.crash.log +0 -0
  36. civic-platform/backend/backend.detached.log +1 -0
  37. civic-platform/backend/backend.dev.log +0 -0
  38. civic-platform/backend/package-lock.json +1817 -0
  39. civic-platform/backend/package.json +30 -0
  40. civic-platform/backend/server.err.log +0 -0
  41. civic-platform/backend/server.out.log +0 -0
  42. civic-platform/backend/sql/001_initial_schema.sql +246 -0
  43. civic-platform/backend/sql/002_seed_core_data.sql +133 -0
  44. civic-platform/backend/sql/003_performance_indexes.sql +32 -0
  45. civic-platform/backend/sql/README.md +22 -0
  46. civic-platform/backend/src/app.ts +48 -0
  47. civic-platform/backend/src/config/env.ts +28 -0
  48. civic-platform/backend/src/db/pool.ts +11 -0
  49. civic-platform/backend/src/lib/audit.ts +29 -0
  50. civic-platform/backend/src/lib/cache.ts +37 -0
civic-platform/.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ NEXT_PUBLIC_API_BASE_URL=http://localhost:4000/api
2
+
3
+ # Optional. If set, the public map uses Mapbox streets tiles.
4
+ # Leave blank to use free OpenStreetMap tiles.
5
+ MAPBOX_TOKEN=
6
+ NEXT_PUBLIC_MAPBOX_TOKEN=
civic-platform/.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ .next/
3
+ dist/
4
+ .env
5
+ .env.local
6
+ backend/.env
7
+ backend/dist/
civic-platform/README.md ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Civic Platform
2
+
3
+ A mobile-first civic issue reporting and resolution platform for day-to-day public problems and emergency incidents.
4
+
5
+ ## Vision
6
+
7
+ Citizens report issues with photo, video, voice, text, and geo-location. The system classifies the complaint, assigns priority, routes it to the correct government department, and tracks the full lifecycle from submission to closure.
8
+
9
+ ## Core goals
10
+
11
+ - Make complaint reporting simple and fast
12
+ - Support day-to-day civic domains and emergency domains
13
+ - Route complaints to the correct department with priority
14
+ - Provide transparent status tracking to citizens
15
+ - Give officials a fast dashboard for triage, assignment, and monitoring
16
+ - Keep the system scalable, low-latency, and future-ready
17
+
18
+ ## Main domains
19
+
20
+ 1. Roads and Transportation
21
+ 2. Sanitation and Waste Management
22
+ 3. Water Supply, Sewerage, and Drainage
23
+ 4. Street Lighting and Electrical Infrastructure
24
+ 5. Public Infrastructure and Amenities
25
+ 6. Environment and Public Health
26
+
27
+ ## Emergency domains
28
+
29
+ 1. Fire Emergencies
30
+ 2. Flood and Water Disaster
31
+ 3. Structural and Infrastructure Hazard
32
+ 4. Electrical Hazard
33
+ 5. Disaster and Rescue
34
+
35
+ ## Complaint lifecycle
36
+
37
+ 1. Draft
38
+ 2. Submitted
39
+ 3. Validated
40
+ 4. Classified
41
+ 5. Prioritized
42
+ 6. Assigned
43
+ 7. Accepted
44
+ 8. In Progress
45
+ 9. Resolved
46
+ 10. Citizen Verified
47
+ 11. Closed
48
+
49
+ Additional states:
50
+
51
+ - Escalated
52
+ - Reopened
53
+ - Duplicate
54
+ - Rejected
55
+ - On Hold
56
+
57
+ ## Proposed stack
58
+
59
+ ### Frontend
60
+
61
+ - Next.js
62
+ - TypeScript
63
+ - Tailwind CSS
64
+ - shadcn/ui
65
+ - Framer Motion
66
+ - Leaflet with OpenStreetMap
67
+
68
+ ### Platform
69
+
70
+ - Supabase Auth
71
+ - Supabase Postgres
72
+ - PostGIS
73
+ - Supabase Storage
74
+ - Supabase Realtime
75
+ - Supabase Edge Functions
76
+
77
+ ### AI
78
+
79
+ - Whisper for speech-to-text
80
+ - DistilBERT or similar lightweight text classifier
81
+ - YOLOv8n or MobileNet for image classification
82
+
83
+ ## App areas
84
+
85
+ - Citizen portal
86
+ - Department dashboard
87
+ - Field officer workspace
88
+ - Super admin configuration
89
+ - Analytics and SLA dashboard
90
+
91
+ ## Folder plan
92
+
93
+ - `app/` Next.js App Router pages
94
+ - `components/` shared UI sections
95
+ - `lib/` config, types, and helpers
96
+ - `data/` static seed content used by the frontend
97
+ - `docs/` architecture, lifecycle, and page planning
98
+
99
+ ## Planning docs
100
+
101
+ - `docs/PROJECT_BLUEPRINT.md`
102
+ - `docs/FRONTEND_PLAN.md`
103
+ - `docs/PAGE_MAP.md`
104
+ - `docs/ROLES_AND_PERMISSIONS.md`
105
+ - `docs/COMPLAINT_LIFECYCLE.md`
106
+ - `docs/DOMAIN_ROUTING_MAP.md`
107
+ - `docs/PRIORITY_SCORING.md`
108
+ - `docs/DATABASE_SCHEMA.md`
109
+ - `docs/BACKEND_STRUCTURE.md`
110
+ - `docs/LOCAL_DATABASE_SETUP.md`
111
+
112
+ ## Next build steps
113
+
114
+ 1. Finalize data model and routing rules
115
+ 2. Build the design system and page structure
116
+ 3. Implement citizen report flow
117
+ 4. Add admin dashboard and complaint lifecycle updates
118
+ 5. Connect Supabase auth, database, storage, and realtime
119
+ 6. Add AI-assisted classification after the MVP workflow is stable
civic-platform/ai/README.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CivicPulse AI Workspace
2
+
3
+ This folder is reserved for model training assets, dataset schemas, experiments, and evaluation results.
4
+
5
+ Recommended subfolders:
6
+
7
+ - `datasets/text`
8
+ - `datasets/vision`
9
+ - `datasets/audio`
10
+ - `evals`
11
+ - `notebooks`
12
+ - `schemas`
13
+
14
+ The live product currently uses an assistive heuristic AI layer in the application code. This workspace is where model-backed training and evaluation can be added next.
15
+
civic-platform/ai/schemas/civic-image-record.example.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image": "path-or-huggingface-image-object",
3
+ "label": "Roads and Transportation",
4
+ "metadata": {
5
+ "complaintCode": "CP-2026-000001",
6
+ "source": "citizen_upload",
7
+ "city": "Hyderabad",
8
+ "latitude": 17.385,
9
+ "longitude": 78.4867
10
+ }
11
+ }
civic-platform/ai/schemas/complaint-text-record.example.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Streetlight not working near school gate",
3
+ "description": "Two streetlights are off near the main gate and the road is dark at night.",
4
+ "address_line": "School Road",
5
+ "landmark": "Near Government High School",
6
+ "city_name": "Sample City",
7
+ "state_name": "Sample State",
8
+ "domain": "Street Lighting and Electrical Infrastructure",
9
+ "sub_problem": "Broken streetlight",
10
+ "priority": "P2",
11
+ "is_emergency": false
12
+ }
civic-platform/ai/training/README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CivicPulse Vision Training Plan
2
+
3
+ The current app has a safe baseline for image evidence:
4
+
5
+ - It extracts GPS metadata from JPEG photos when available.
6
+ - It uses address search, GPS, or map pinning when photo GPS metadata is missing.
7
+ - It passes lightweight visual signals into the AI assistant when filenames contain civic issue hints.
8
+
9
+ For accurate image classification, train a real model on labeled civic images. A perfect model is not realistic, but a good production model is achievable with enough labeled examples and evaluation.
10
+
11
+ ## Recommended Labels
12
+
13
+ Use the same project domains as labels:
14
+
15
+ - Roads and Transportation
16
+ - Sanitation and Waste Management
17
+ - Water Supply, Sewerage, and Drainage
18
+ - Street Lighting and Electrical Infrastructure
19
+ - Public Infrastructure and Amenities
20
+ - Environment and Public Health
21
+ - Fire Emergencies
22
+ - Flood and Water Disaster
23
+ - Structural and Infrastructure Hazard
24
+ - Electrical Hazard
25
+ - Disaster and Rescue
26
+
27
+ ## Dataset Format
28
+
29
+ For Hugging Face image classification, publish a dataset with:
30
+
31
+ - `image`: image column
32
+ - `label`: one of the labels above
33
+
34
+ Example record:
35
+
36
+ [civic-image-record.example.json](/C:/Users/Praveen/Documents/New%20project/civic-platform/ai/schemas/civic-image-record.example.json)
37
+
38
+ ## Training Recommendation
39
+
40
+ Start with image classification:
41
+
42
+ - Model: `timm/mobilenetv3_small_100.lamb_in1k` for fast iteration
43
+ - Upgrade: `timm/resnet50.a1_in1k` or `timm/vit_base_patch16_dinov3.lvd1689m` for better accuracy
44
+ - Target metric: validation accuracy and per-class recall
45
+ - Minimum data target: 300 to 500 images per label for a useful first model
46
+ - Better data target: 1,000+ images per label with different lighting, angles, weather, and phones
47
+
48
+ For localization such as detecting a pothole box inside the image, move to object detection later with D-FINE or RT-DETR.
49
+
50
+ ## Deployment Path
51
+
52
+ 1. Collect and label civic images from real reports.
53
+ 2. Upload the dataset to Hugging Face Hub.
54
+ 3. Validate the dataset format before training.
55
+ 4. Fine-tune an image classifier using Hugging Face Jobs.
56
+ 5. Push the trained model to the Hub.
57
+ 6. Add a backend inference endpoint that calls the trained model and returns:
58
+ - predicted domain
59
+ - confidence
60
+ - top 3 alternatives
61
+ - model version
62
+ 7. Keep the operator override flow because image models can be wrong.
63
+
64
+ ## Important Note
65
+
66
+ Do not rely only on image classification. The best production result should combine:
67
+
68
+ - image prediction
69
+ - text/voice description
70
+ - issue domain chosen by the citizen
71
+ - geo-location and ward
72
+ - nearby duplicate complaints
73
+ - operator override
civic-platform/ai/training/civic-image-labels.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CivicPulse Image Labels
2
+
3
+ Use these labels for the first image classification dataset.
4
+
5
+ | Label | Example visual cases |
6
+ |---|---|
7
+ | Roads and Transportation | potholes, broken roads, road blockage, damaged footpath, damaged divider |
8
+ | Sanitation and Waste Management | garbage piles, overflowing bins, illegal dumping, dirty streets |
9
+ | Water Supply, Sewerage, and Drainage | water leakage, sewage overflow, clogged drains, waterlogging |
10
+ | Street Lighting and Electrical Infrastructure | broken streetlights, damaged poles, exposed wires, transformer issues |
11
+ | Public Infrastructure and Amenities | open manholes, broken bus stops, damaged public toilets, park damage |
12
+ | Environment and Public Health | fallen trees, stagnant water, smoke, dead animals, mosquito breeding areas |
13
+ | Fire Emergencies | visible fire, heavy smoke, burning debris |
14
+ | Flood and Water Disaster | floodwater, severe rain overflow, storm damage |
15
+ | Structural and Infrastructure Hazard | wall cracks, building collapse signs, bridge damage |
16
+ | Electrical Hazard | live wires, transformer blast, major short circuit evidence |
17
+ | Disaster and Rescue | landslide, storm collapse, road-blocking tree fall, rescue hazard |
18
+
19
+ Labeling rule: if one image contains multiple issues, choose the most urgent visible issue and store secondary labels in metadata for future multi-label training.
civic-platform/app/api/session/login/route.ts ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cookies } from "next/headers";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+ import bcrypt from "bcryptjs";
4
+ import pool from "@/lib/db";
5
+ import { encodeSession, SESSION_COOKIE_NAME, type SessionUser } from "@/lib/session";
6
+
7
+ type UserRow = SessionUser & { passwordHash: string | null };
8
+
9
+ export async function POST(request: NextRequest) {
10
+ try {
11
+ const body = (await request.json()) as { email?: string; password?: string };
12
+ const email = body.email?.trim().toLowerCase() ?? "";
13
+ const password = body.password ?? "";
14
+
15
+ if (!email) {
16
+ return NextResponse.json({ error: "Email is required" }, { status: 400 });
17
+ }
18
+ if (!password) {
19
+ return NextResponse.json({ error: "Password is required" }, { status: 400 });
20
+ }
21
+
22
+ // Query users table joined with roles
23
+ const result = await pool.query<UserRow>(
24
+ `SELECT
25
+ u.id,
26
+ u.full_name AS "fullName",
27
+ u.email,
28
+ r.name AS role,
29
+ u.department_id AS "departmentId",
30
+ u.ward_id AS "wardId",
31
+ u.password_hash AS "passwordHash"
32
+ FROM users u
33
+ INNER JOIN roles r ON r.id = u.role_id
34
+ WHERE lower(u.email) = $1
35
+ AND u.is_active = TRUE
36
+ LIMIT 1`,
37
+ [email],
38
+ );
39
+
40
+ const user = result.rows[0] ?? null;
41
+
42
+ if (!user) {
43
+ return NextResponse.json({ error: "Invalid email or password" }, { status: 401 });
44
+ }
45
+
46
+ // Support both real bcrypt hashes AND the dev-placeholder-hash used in seed data
47
+ let passwordValid = false;
48
+ if (user.passwordHash === "dev-placeholder-hash" && password === "civicpulse123") {
49
+ passwordValid = true;
50
+ } else if (user.passwordHash) {
51
+ passwordValid = await bcrypt.compare(password, user.passwordHash);
52
+ }
53
+
54
+ if (!passwordValid) {
55
+ return NextResponse.json({ error: "Invalid email or password" }, { status: 401 });
56
+ }
57
+
58
+ const { passwordHash: _ph, ...sessionUser } = user;
59
+ const cookieStore = await cookies();
60
+
61
+ cookieStore.set(SESSION_COOKIE_NAME, encodeSession(sessionUser), {
62
+ httpOnly: true,
63
+ sameSite: "lax",
64
+ path: "/",
65
+ secure: process.env.NODE_ENV === "production",
66
+ maxAge: 60 * 60 * 12,
67
+ });
68
+
69
+ return NextResponse.json({ data: sessionUser });
70
+ } catch (error) {
71
+ console.error("[login] Error:", error);
72
+ return NextResponse.json(
73
+ { error: error instanceof Error ? error.message : "Login failed" },
74
+ { status: 500 },
75
+ );
76
+ }
77
+ }
civic-platform/app/api/session/logout/route.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cookies } from "next/headers";
2
+ import { NextResponse } from "next/server";
3
+ import { SESSION_COOKIE_NAME } from "@/lib/session";
4
+
5
+ export async function POST() {
6
+ const cookieStore = await cookies();
7
+ cookieStore.delete(SESSION_COOKIE_NAME);
8
+
9
+ return NextResponse.json({ data: { ok: true } });
10
+ }
civic-platform/app/api/session/register/route.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cookies } from "next/headers";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+ import bcrypt from "bcryptjs";
4
+ import pool from "@/lib/db";
5
+ import { encodeSession, SESSION_COOKIE_NAME, type SessionUser } from "@/lib/session";
6
+
7
+ type UserRow = SessionUser & { passwordHash: string | null };
8
+
9
+ export async function POST(request: NextRequest) {
10
+ try {
11
+ const body = (await request.json()) as {
12
+ fullName?: string;
13
+ email?: string;
14
+ phone?: string;
15
+ password?: string;
16
+ };
17
+
18
+ const fullName = body.fullName?.trim() ?? "";
19
+ const email = body.email?.trim().toLowerCase() ?? "";
20
+ const phone = body.phone?.trim() || null;
21
+ const password = body.password ?? "";
22
+
23
+ // Validation
24
+ if (!fullName) {
25
+ return NextResponse.json({ error: "Full name is required" }, { status: 400 });
26
+ }
27
+ if (!email) {
28
+ return NextResponse.json({ error: "Email is required" }, { status: 400 });
29
+ }
30
+ if (!password || password.length < 8) {
31
+ return NextResponse.json({ error: "Password must be at least 8 characters" }, { status: 400 });
32
+ }
33
+
34
+ // Check if email already exists
35
+ const existing = await pool.query("SELECT id FROM users WHERE lower(email) = $1", [email]);
36
+ if (existing.rows.length > 0) {
37
+ return NextResponse.json({ error: "An account already exists for this email" }, { status: 409 });
38
+ }
39
+
40
+ // Hash password
41
+ const passwordHash = await bcrypt.hash(password, 12);
42
+
43
+ // Look up citizen role ID
44
+ const roleResult = await pool.query("SELECT id FROM roles WHERE name = 'citizen' LIMIT 1");
45
+ const roleId = roleResult.rows[0]?.id;
46
+ if (!roleId) {
47
+ return NextResponse.json({ error: "Citizen role is not configured in the database" }, { status: 500 });
48
+ }
49
+
50
+ // Insert new user
51
+ const insertResult = await pool.query<UserRow>(
52
+ `INSERT INTO users (full_name, email, phone, password_hash, role_id)
53
+ VALUES ($1, $2, $3, $4, $5)
54
+ RETURNING
55
+ id,
56
+ full_name AS "fullName",
57
+ email,
58
+ 'citizen' AS role,
59
+ department_id AS "departmentId",
60
+ ward_id AS "wardId",
61
+ password_hash AS "passwordHash"`,
62
+ [fullName, email, phone, passwordHash, roleId],
63
+ );
64
+
65
+ const user = insertResult.rows[0];
66
+ const { passwordHash: _ph, ...sessionUser } = user;
67
+
68
+ const cookieStore = await cookies();
69
+ cookieStore.set(SESSION_COOKIE_NAME, encodeSession(sessionUser), {
70
+ httpOnly: true,
71
+ sameSite: "lax",
72
+ path: "/",
73
+ secure: process.env.NODE_ENV === "production",
74
+ maxAge: 60 * 60 * 12,
75
+ });
76
+
77
+ return NextResponse.json({ data: sessionUser }, { status: 201 });
78
+ } catch (error) {
79
+ console.error("[register] Error:", error);
80
+ const message = error instanceof Error ? error.message : "Registration failed";
81
+ return NextResponse.json({ error: message }, { status: 500 });
82
+ }
83
+ }
civic-platform/app/complaints/[id]/page.tsx ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import { AlertTriangle, Clock3, MapPinned, ShieldCheck, Waves } from "lucide-react";
3
+ import { AppShell } from "@/components/app-shell";
4
+ import { CitizenFollowUpPanel } from "@/components/citizen-follow-up-panel";
5
+ import { ComplaintMediaGallery } from "@/components/complaint-media-gallery";
6
+ import { SectionHeader } from "@/components/section-header";
7
+ import { requireRole } from "@/lib/auth";
8
+ import {
9
+ getComplaintDetail,
10
+ getComplaintFeedback,
11
+ getComplaintHistory,
12
+ getComplaintMedia,
13
+ type ComplaintFeedbackItem,
14
+ type ComplaintMediaItem,
15
+ type ComplaintStatusHistoryItem,
16
+ } from "@/lib/api";
17
+
18
+ type ComplaintDetailPageProps = {
19
+ params: Promise<{
20
+ id: string;
21
+ }>;
22
+ };
23
+
24
+ function formatStatus(status: string) {
25
+ return status
26
+ .split("_")
27
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
28
+ .join(" ");
29
+ }
30
+
31
+ function formatPriority(priority: string) {
32
+ switch (priority) {
33
+ case "P1":
34
+ return "P1 Critical";
35
+ case "P2":
36
+ return "P2 High";
37
+ case "P3":
38
+ return "P3 Medium";
39
+ default:
40
+ return "P4 Low";
41
+ }
42
+ }
43
+
44
+ function formatDateTime(value: string) {
45
+ return new Date(value).toLocaleString();
46
+ }
47
+
48
+ function timelineTone(status: string) {
49
+ switch (status) {
50
+ case "submitted":
51
+ return "bg-civic-primary";
52
+ case "validated":
53
+ return "bg-civic-secondary";
54
+ case "classified":
55
+ case "prioritized":
56
+ return "bg-amber-500";
57
+ case "assigned":
58
+ return "bg-slate-800";
59
+ case "in_progress":
60
+ return "bg-emerald-600";
61
+ case "resolved":
62
+ case "closed":
63
+ return "bg-emerald-700";
64
+ case "reopened":
65
+ case "escalated":
66
+ return "bg-civic-danger";
67
+ default:
68
+ return "bg-slate-300";
69
+ }
70
+ }
71
+
72
+ export default async function ComplaintDetailPage({ params }: ComplaintDetailPageProps) {
73
+ await requireRole(["citizen"]);
74
+ const { id } = await params;
75
+
76
+ let complaintStatus = "In Progress";
77
+ let complaintPriority = "P2 High";
78
+ let complaintLocation = "Market Road, Ward 12";
79
+ let complaintDomain = "Water Supply, Sewerage, and Drainage";
80
+ let complaintDescription = "Overflow affecting entry and traffic movement near the market.";
81
+ let complaintCode = "CP-2026-00124";
82
+ let complaintDepartment = "Water and Sewerage";
83
+ let media: ComplaintMediaItem[] = [];
84
+ let feedback: ComplaintFeedbackItem[] = [];
85
+ let rawStatus = "in_progress";
86
+ let citizenId = "00000000-0000-0000-0000-000000000001";
87
+ let timeline: Array<{ title: string; time: string; note: string; tone: string }> = [
88
+ {
89
+ title: "Submitted",
90
+ time: "Pending",
91
+ note: "Complaint created by citizen with image, description, and geo-tagged location.",
92
+ tone: "bg-civic-primary",
93
+ },
94
+ ];
95
+
96
+ try {
97
+ const [complaint, history, mediaItems, feedbackItems] = await Promise.all([
98
+ getComplaintDetail(id),
99
+ getComplaintHistory(id),
100
+ getComplaintMedia(id),
101
+ getComplaintFeedback(id),
102
+ ]);
103
+
104
+ rawStatus = complaint.status;
105
+ complaintStatus = formatStatus(complaint.status);
106
+ complaintPriority = formatPriority(complaint.priorityLevel);
107
+ complaintLocation = complaint.addressLine ?? complaint.landmark ?? "Location pending";
108
+ complaintDomain = complaint.domainName ?? "Unclassified";
109
+ complaintDescription = complaint.description ?? complaintDescription;
110
+ complaintCode = complaint.complaintCode;
111
+ complaintDepartment = complaint.departmentName ?? complaintDepartment;
112
+ citizenId = complaint.citizenId;
113
+ media = mediaItems;
114
+ feedback = feedbackItems;
115
+
116
+ if (history.length > 0) {
117
+ timeline = history.map((entry: ComplaintStatusHistoryItem) => ({
118
+ title: formatStatus(entry.newStatus),
119
+ time: formatDateTime(entry.createdAt),
120
+ note: entry.changeReason ?? `Complaint moved to ${formatStatus(entry.newStatus)}.`,
121
+ tone: timelineTone(entry.newStatus),
122
+ }));
123
+ }
124
+ } catch {
125
+ // Keep fallback values for design flow when backend is unavailable.
126
+ }
127
+
128
+ return (
129
+ <AppShell>
130
+ <section className="grid gap-6 rounded-[2rem] bg-civic-primary px-6 py-8 text-white shadow-civic lg:grid-cols-[1.08fr_0.92fr] lg:px-8">
131
+ <div className="space-y-4">
132
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-white/75">Complaint detail</p>
133
+ <h1 className="text-4xl font-semibold tracking-tight">Track your complaint in one place.</h1>
134
+ <p className="max-w-2xl text-sm leading-7 text-white/80">
135
+ Check the current status, department ownership, attached evidence, and any updates from the city team.
136
+ </p>
137
+ </div>
138
+
139
+ <div className="grid gap-4 sm:grid-cols-2">
140
+ {[
141
+ { label: "Complaint ID", value: complaintCode },
142
+ { label: "Current status", value: complaintStatus },
143
+ { label: "Priority", value: complaintPriority },
144
+ { label: "Assigned department", value: complaintDepartment },
145
+ ].map((card) => (
146
+ <div key={card.label} className="rounded-3xl border border-white/15 bg-white/10 p-5 backdrop-blur">
147
+ <p className="text-sm font-semibold uppercase tracking-[0.16em] text-white/75">{card.label}</p>
148
+ <p className="mt-3 text-xl font-semibold text-white">{card.value}</p>
149
+ </div>
150
+ ))}
151
+ </div>
152
+ </section>
153
+
154
+ <section className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
155
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
156
+ <div className="flex flex-wrap items-start justify-between gap-4">
157
+ <div>
158
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Timeline</p>
159
+ <h2 className="mt-2 text-2xl font-semibold text-civic-text">Status history</h2>
160
+ </div>
161
+ <div className="rounded-full bg-amber-50 px-4 py-2 text-sm font-semibold text-amber-800">
162
+ Current state: {complaintStatus}
163
+ </div>
164
+ </div>
165
+
166
+ <div className="mt-8 space-y-6">
167
+ {timeline.map((entry, index) => (
168
+ <div key={`${entry.title}-${entry.time}-${index}`} className="grid gap-4 md:grid-cols-[auto_1fr] md:gap-5">
169
+ <div className="flex items-start gap-4 md:flex-col md:items-center">
170
+ <div className={["mt-1 h-4 w-4 rounded-full", entry.tone].join(" ")} />
171
+ {index !== timeline.length - 1 ? <div className="hidden h-16 w-px bg-slate-200 md:block" /> : null}
172
+ </div>
173
+ <div className="rounded-3xl bg-slate-50 p-5">
174
+ <div className="flex flex-wrap items-center justify-between gap-3">
175
+ <h3 className="text-lg font-semibold text-civic-text">{entry.title}</h3>
176
+ <div className="inline-flex items-center gap-2 rounded-full bg-white px-3 py-2 text-xs font-semibold text-slate-700">
177
+ <Clock3 className="h-4 w-4" />
178
+ {entry.time}
179
+ </div>
180
+ </div>
181
+ <p className="mt-3 text-sm leading-6 text-civic-muted">{entry.note}</p>
182
+ </div>
183
+ </div>
184
+ ))}
185
+ </div>
186
+ </section>
187
+
188
+ <div className="space-y-6">
189
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
190
+ <SectionHeader eyebrow="Location" icon={MapPinned} title="Complaint context" />
191
+
192
+ <div className="mt-5 space-y-3">
193
+ {[complaintLocation, `Domain: ${complaintDomain}`, complaintDescription].map((item) => (
194
+ <div key={item} className="rounded-2xl bg-slate-50 px-4 py-3 text-sm leading-6 text-civic-text">
195
+ {item}
196
+ </div>
197
+ ))}
198
+ </div>
199
+ </section>
200
+
201
+ <ComplaintMediaGallery
202
+ items={media}
203
+ title="Photos, video, and audio"
204
+ emptyLabel="No evidence has been added to this complaint yet."
205
+ />
206
+
207
+ <CitizenFollowUpPanel complaintId={id} citizenId={citizenId} status={rawStatus} feedback={feedback} />
208
+ </div>
209
+ </section>
210
+
211
+ <section className="grid gap-6 lg:grid-cols-[0.95fr_1.05fr]">
212
+ <section className="rounded-[2rem] border border-emerald-200 bg-emerald-50 p-6 shadow-sm">
213
+ <div className="flex items-start gap-3">
214
+ <ShieldCheck className="mt-0.5 h-5 w-5 text-emerald-700" />
215
+ <div>
216
+ <h2 className="text-lg font-semibold text-emerald-950">What you can see here</h2>
217
+ <p className="mt-2 text-sm leading-6 text-emerald-900/85">
218
+ This page shows your complaint progress and public-safe updates without exposing internal staff notes.
219
+ </p>
220
+ </div>
221
+ </div>
222
+ </section>
223
+
224
+ <section className="rounded-[2rem] border border-amber-200 bg-amber-50 p-6 shadow-sm">
225
+ <div className="flex items-start gap-3">
226
+ <AlertTriangle className="mt-0.5 h-5 w-5 text-amber-700" />
227
+ <div>
228
+ <h2 className="text-lg font-semibold text-amber-950">Need more help?</h2>
229
+ <p className="mt-2 text-sm leading-6 text-amber-900/85">
230
+ If the issue is marked resolved but still exists, use the reopen option below so the team can review it again.
231
+ </p>
232
+ </div>
233
+ </div>
234
+ </section>
235
+ </section>
236
+
237
+ <section className="flex flex-wrap items-center justify-between gap-4 rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
238
+ <div className="space-y-2">
239
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Quick actions</p>
240
+ <h2 className="text-2xl font-semibold text-civic-text">Keep track of this report or return to your list.</h2>
241
+ <p className="max-w-2xl text-sm leading-6 text-civic-muted">
242
+ You can go back to all complaints, stay on this page for updates, or report another issue if needed.
243
+ </p>
244
+ </div>
245
+
246
+ <div className="flex flex-wrap gap-3">
247
+ <Link
248
+ className="inline-flex rounded-full border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-civic-primary hover:text-civic-primary"
249
+ href="/complaints"
250
+ >
251
+ Back to complaints
252
+ </Link>
253
+ <Link
254
+ className="inline-flex items-center gap-2 rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5"
255
+ href="/report"
256
+ >
257
+ Report another issue
258
+ <Waves className="h-4 w-4" />
259
+ </Link>
260
+ </div>
261
+ </section>
262
+ </AppShell>
263
+ );
264
+ }
civic-platform/app/complaints/page.tsx ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import { Clock3, MapPinned, Search } from "lucide-react";
3
+ import { AppShell } from "@/components/app-shell";
4
+ import { PriorityBadge } from "@/components/priority-badge";
5
+ import { StatusBadge } from "@/components/status-badge";
6
+ import { getCitizenComplaints } from "@/lib/api";
7
+ import { requireRole } from "@/lib/auth";
8
+
9
+ const complaintStats = [
10
+ { label: "Active", value: "04" },
11
+ { label: "Resolved", value: "09" },
12
+ { label: "Reopened", value: "01" },
13
+ ];
14
+
15
+ type ComplaintCard = {
16
+ id: string;
17
+ complaintCode?: string;
18
+ title: string;
19
+ domain: string;
20
+ status: string;
21
+ priority: string;
22
+ location: string;
23
+ updatedAt: string;
24
+ };
25
+
26
+ const fallbackComplaints: ComplaintCard[] = [
27
+ {
28
+ id: "CP-2026-00124",
29
+ complaintCode: "CP-2026-00124",
30
+ title: "Sewage overflow near market entrance",
31
+ domain: "Water Supply, Sewerage, and Drainage",
32
+ status: "In Progress",
33
+ priority: "P2 High",
34
+ location: "Market Road, Ward 12",
35
+ updatedAt: "Updated 1 hour ago",
36
+ },
37
+ {
38
+ id: "CP-2026-00108",
39
+ complaintCode: "CP-2026-00108",
40
+ title: "Streetlight not working on school road",
41
+ domain: "Street Lighting and Electrical Infrastructure",
42
+ status: "Assigned",
43
+ priority: "P3 Medium",
44
+ location: "School Street, Ward 8",
45
+ updatedAt: "Updated today",
46
+ },
47
+ {
48
+ id: "CP-2026-00097",
49
+ complaintCode: "CP-2026-00097",
50
+ title: "Overflowing garbage bin near bus stop",
51
+ domain: "Sanitation and Waste Management",
52
+ status: "Resolved",
53
+ priority: "P3 Medium",
54
+ location: "Central Bus Stop, Ward 3",
55
+ updatedAt: "Resolved yesterday",
56
+ },
57
+ {
58
+ id: "CP-2026-00091",
59
+ complaintCode: "CP-2026-00091",
60
+ title: "Open manhole beside community hall",
61
+ domain: "Public Infrastructure and Amenities",
62
+ status: "Reopened",
63
+ priority: "P1 Critical",
64
+ location: "Community Hall Lane, Ward 5",
65
+ updatedAt: "Reopened 3 hours ago",
66
+ },
67
+ ];
68
+
69
+ const filters = ["All", "Active", "Resolved", "Reopened", "Emergency"];
70
+
71
+ function formatStatus(status: string) {
72
+ return status
73
+ .split("_")
74
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
75
+ .join(" ");
76
+ }
77
+
78
+ function formatPriority(priority: string) {
79
+ switch (priority) {
80
+ case "P1":
81
+ return "P1 Critical";
82
+ case "P2":
83
+ return "P2 High";
84
+ case "P3":
85
+ return "P3 Medium";
86
+ default:
87
+ return "P4 Low";
88
+ }
89
+ }
90
+
91
+ export default async function ComplaintsPage() {
92
+ const user = await requireRole(["citizen"]);
93
+ let complaints: ComplaintCard[] = fallbackComplaints;
94
+
95
+ try {
96
+ const apiComplaints = await getCitizenComplaints(user.id);
97
+
98
+ if (apiComplaints.length > 0) {
99
+ complaints = apiComplaints.map((complaint) => ({
100
+ id: complaint.id,
101
+ title: complaint.title,
102
+ domain: complaint.domainName ?? "Unclassified",
103
+ status: formatStatus(complaint.status),
104
+ priority: formatPriority(complaint.priorityLevel),
105
+ location: complaint.departmentName ?? "Department pending",
106
+ updatedAt: new Date(complaint.submittedAt).toLocaleString(),
107
+ complaintCode: complaint.complaintCode,
108
+ }));
109
+ }
110
+ } catch {
111
+ complaints = fallbackComplaints;
112
+ }
113
+
114
+ return (
115
+ <AppShell>
116
+ <section className="relative overflow-hidden rounded-[2.5rem] bg-gradient-to-br from-[#0F172A] via-civic-primary to-[#1E3A8A] px-8 py-12 text-white shadow-civic lg:px-12 lg:py-16">
117
+ <div className="absolute -left-32 -top-32 h-96 w-96 rounded-full bg-white/10 blur-[100px]" />
118
+
119
+ <div className="relative flex flex-wrap items-center justify-between gap-6">
120
+ <div className="space-y-4">
121
+ <h1 className="text-4xl font-extrabold tracking-tight sm:text-5xl">My Reports</h1>
122
+ <p className="text-lg text-slate-300">Track the status of the issues you've reported in your neighborhood.</p>
123
+ </div>
124
+ <Link
125
+ className="group inline-flex items-center gap-2 rounded-full bg-white px-7 py-4 text-base font-bold text-civic-primary shadow-xl transition-all hover:-translate-y-1 hover:shadow-2xl"
126
+ href="/report"
127
+ >
128
+ Report a new issue
129
+ </Link>
130
+ </div>
131
+
132
+ <div className="relative mt-10 grid gap-5 sm:grid-cols-3">
133
+ {complaintStats.map((stat) => (
134
+ <div key={stat.label} className="rounded-[2rem] border border-white/15 bg-white/10 p-6 backdrop-blur-xl">
135
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-white/60">{stat.label}</p>
136
+ <p className="mt-4 text-4xl font-extrabold text-white">{stat.value}</p>
137
+ </div>
138
+ ))}
139
+ </div>
140
+ </section>
141
+
142
+ <section className="space-y-6">
143
+ <div className="rounded-[2rem] border border-slate-200 bg-white p-5 shadow-sm">
144
+ <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
145
+ <div className="flex flex-wrap gap-2">
146
+ {filters.map((filter) => (
147
+ <button
148
+ key={filter}
149
+ type="button"
150
+ className={[
151
+ "rounded-full px-4 py-2 text-sm font-semibold transition",
152
+ filter === "All"
153
+ ? "bg-civic-primary text-white"
154
+ : "bg-slate-100 text-slate-700 hover:bg-slate-200",
155
+ ].join(" ")}
156
+ >
157
+ {filter}
158
+ </button>
159
+ ))}
160
+ </div>
161
+
162
+ <div className="flex items-center gap-3 rounded-full border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-civic-muted lg:min-w-80">
163
+ <Search className="h-4 w-4" />
164
+ Search by complaint ID or area
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <section className="rounded-[2.5rem] border border-slate-200 bg-white/70 p-8 shadow-glass backdrop-blur-xl">
170
+ <h2 className="text-2xl font-extrabold text-slate-900">Recent Activity</h2>
171
+
172
+ <div className="mt-8 space-y-5">
173
+ {complaints.map((complaint) => (
174
+ <article
175
+ key={complaint.id}
176
+ className="group relative overflow-hidden rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm transition-all hover:-translate-y-1 hover:border-slate-300 hover:shadow-xl"
177
+ >
178
+ <div className="flex flex-wrap items-start justify-between gap-4">
179
+ <div className="space-y-3">
180
+ <div className="flex flex-wrap items-center gap-3">
181
+ <span className="rounded-full bg-slate-100 px-3.5 py-1 text-xs font-bold text-slate-700">
182
+ {complaint.complaintCode ?? complaint.id}
183
+ </span>
184
+ <StatusBadge status={complaint.status} />
185
+ <PriorityBadge priority={complaint.priority} />
186
+ </div>
187
+ <h3 className="text-2xl font-bold text-slate-900">{complaint.title}</h3>
188
+ <p className="text-sm font-medium text-slate-500">{complaint.domain}</p>
189
+ </div>
190
+
191
+ <div className="flex items-center gap-2 rounded-full border border-slate-100 bg-slate-50 px-4 py-2 text-xs font-bold text-slate-500">
192
+ <Clock3 className="h-4 w-4" />
193
+ {complaint.updatedAt}
194
+ </div>
195
+ </div>
196
+
197
+ <div className="mt-6 flex items-center gap-2 text-sm font-medium text-slate-500">
198
+ <MapPinned className="h-4 w-4" />
199
+ {complaint.location}
200
+ </div>
201
+
202
+ <div className="mt-6 flex flex-wrap gap-3">
203
+ <Link
204
+ className="inline-flex items-center gap-2 rounded-full bg-slate-900 px-6 py-3 text-sm font-bold text-white shadow-md transition hover:bg-slate-800 hover:shadow-lg"
205
+ href={`/complaints/${complaint.id}`}
206
+ >
207
+ View Details
208
+ </Link>
209
+ </div>
210
+ </article>
211
+ ))}
212
+ </div>
213
+ </section>
214
+ </section>
215
+ </AppShell>
216
+ );
217
+ }
civic-platform/app/dashboard/analytics/page.tsx ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import { AppShell } from "@/components/app-shell";
3
+ import { EmptyState } from "@/components/empty-state";
4
+ import { LiveSyncBadge } from "@/components/live-sync-badge";
5
+ import { requireRole } from "@/lib/auth";
6
+ import { getAnalyticsExportUrl, getAnalyticsSummary, type AnalyticsSummary } from "@/lib/api";
7
+ import { ArrowDownToLine, BarChart3, Clock3, Siren, TrendingUp } from "lucide-react";
8
+
9
+ const fallbackAnalytics: AnalyticsSummary = {
10
+ trends: [],
11
+ domainBreakdown: [],
12
+ departmentPerformance: [],
13
+ kpis: {
14
+ emergencyCount: 0,
15
+ reopenedCount: 0,
16
+ repeatComplaintRiskCount: 0,
17
+ slaBreaches: 0,
18
+ },
19
+ };
20
+
21
+ export default async function DashboardAnalyticsPage() {
22
+ await requireRole(["department_operator", "municipal_admin"]);
23
+
24
+ let analytics = fallbackAnalytics;
25
+
26
+ try {
27
+ analytics = await getAnalyticsSummary();
28
+ } catch {
29
+ analytics = fallbackAnalytics;
30
+ }
31
+
32
+ const maxTrend = Math.max(
33
+ 1,
34
+ ...analytics.trends.flatMap((item) => [item.reportedCount, item.resolvedCount]),
35
+ );
36
+
37
+ return (
38
+ <AppShell>
39
+ <section className="rounded-[2rem] bg-gradient-to-br from-slate-950 via-civic-primary to-[#0b6a8f] px-6 py-8 text-white shadow-civic lg:px-8">
40
+ <div className="flex flex-wrap items-center justify-between gap-4">
41
+ <div className="space-y-3">
42
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-white/70">Analytics</p>
43
+ <h1 className="text-4xl font-semibold tracking-tight">Track performance, trends, and launch readiness.</h1>
44
+ <p className="max-w-2xl text-sm leading-7 text-white/80">
45
+ Use this view to monitor complaint trends, department throughput, SLA risk, and repeat civic failures.
46
+ </p>
47
+ </div>
48
+ <div className="flex flex-wrap items-center gap-3">
49
+ <LiveSyncBadge label="Analytics live refresh" />
50
+ <a
51
+ className="inline-flex items-center gap-2 rounded-full bg-white px-5 py-3 text-sm font-semibold text-civic-primary transition hover:-translate-y-0.5"
52
+ href={getAnalyticsExportUrl()}
53
+ target="_blank"
54
+ rel="noreferrer"
55
+ >
56
+ <ArrowDownToLine className="h-4 w-4" />
57
+ Export CSV
58
+ </a>
59
+ </div>
60
+ </div>
61
+ </section>
62
+
63
+ <section className="grid gap-6 lg:grid-cols-4">
64
+ {[
65
+ { label: "Emergency", value: analytics.kpis.emergencyCount, icon: Siren },
66
+ { label: "Reopened", value: analytics.kpis.reopenedCount, icon: TrendingUp },
67
+ { label: "Repeat risk", value: analytics.kpis.repeatComplaintRiskCount, icon: BarChart3 },
68
+ { label: "SLA breaches", value: analytics.kpis.slaBreaches, icon: Clock3 },
69
+ ].map((item) => {
70
+ const Icon = item.icon;
71
+ return (
72
+ <div key={item.label} className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
73
+ <div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-civic-secondary/15 text-civic-secondary">
74
+ <Icon className="h-5 w-5" />
75
+ </div>
76
+ <p className="mt-5 text-sm font-semibold uppercase tracking-[0.16em] text-civic-muted">{item.label}</p>
77
+ <p className="mt-2 text-3xl font-semibold text-civic-text">{item.value}</p>
78
+ </div>
79
+ );
80
+ })}
81
+ </section>
82
+
83
+ <section className="grid gap-6 xl:grid-cols-[1.15fr_0.85fr]">
84
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
85
+ <div>
86
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Trends</p>
87
+ <h2 className="mt-2 text-2xl font-semibold text-civic-text">Reported vs resolved over the last 7 days</h2>
88
+ </div>
89
+ <div className="mt-6 space-y-4">
90
+ {analytics.trends.length === 0 ? (
91
+ <EmptyState icon={TrendingUp} title="No trend data yet" description="Trend data will appear once complaints start moving through the system." />
92
+ ) : (
93
+ analytics.trends.map((item) => (
94
+ <div key={item.day} className="space-y-2">
95
+ <div className="flex items-center justify-between gap-4 text-sm">
96
+ <span className="font-semibold text-civic-text">{item.day}</span>
97
+ <span className="text-civic-muted">
98
+ reported {item.reportedCount} · resolved {item.resolvedCount}
99
+ </span>
100
+ </div>
101
+ <div className="grid gap-2">
102
+ <div className="h-3 overflow-hidden rounded-full bg-slate-100">
103
+ <div
104
+ className="h-full rounded-full bg-civic-primary"
105
+ style={{ width: `${(item.reportedCount / maxTrend) * 100}%` }}
106
+ />
107
+ </div>
108
+ <div className="h-3 overflow-hidden rounded-full bg-slate-100">
109
+ <div
110
+ className="h-full rounded-full bg-civic-success"
111
+ style={{ width: `${(item.resolvedCount / maxTrend) * 100}%` }}
112
+ />
113
+ </div>
114
+ </div>
115
+ </div>
116
+ ))
117
+ )}
118
+ </div>
119
+ </section>
120
+
121
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
122
+ <div>
123
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Domains</p>
124
+ <h2 className="mt-2 text-2xl font-semibold text-civic-text">Complaint share by category</h2>
125
+ </div>
126
+ <div className="mt-6 space-y-3">
127
+ {analytics.domainBreakdown.length === 0 ? (
128
+ <EmptyState icon={BarChart3} title="No domain data yet" description="Domain distribution appears once complaints are recorded." />
129
+ ) : (
130
+ analytics.domainBreakdown.map((item) => (
131
+ <div key={item.domainName} className="rounded-3xl bg-slate-50 p-4">
132
+ <div className="flex items-center justify-between gap-4">
133
+ <span className="text-sm font-semibold text-civic-text">{item.domainName}</span>
134
+ <span className="rounded-full bg-white px-3 py-1 text-xs font-semibold text-slate-700">
135
+ {item.complaintCount}
136
+ </span>
137
+ </div>
138
+ </div>
139
+ ))
140
+ )}
141
+ </div>
142
+ </section>
143
+ </section>
144
+
145
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
146
+ <div className="flex flex-wrap items-center justify-between gap-4">
147
+ <div>
148
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Departments</p>
149
+ <h2 className="mt-2 text-2xl font-semibold text-civic-text">Performance and response time</h2>
150
+ </div>
151
+ <Link
152
+ href="/dashboard"
153
+ className="rounded-full border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-civic-primary hover:text-civic-primary"
154
+ >
155
+ Back to overview
156
+ </Link>
157
+ </div>
158
+
159
+ <div className="mt-6 overflow-x-auto">
160
+ <table className="min-w-full text-left text-sm">
161
+ <thead>
162
+ <tr className="border-b border-slate-200 text-civic-muted">
163
+ <th className="px-3 py-3 font-semibold">Department</th>
164
+ <th className="px-3 py-3 font-semibold">Pending</th>
165
+ <th className="px-3 py-3 font-semibold">Resolved</th>
166
+ <th className="px-3 py-3 font-semibold">Avg resolution (hrs)</th>
167
+ </tr>
168
+ </thead>
169
+ <tbody>
170
+ {analytics.departmentPerformance.map((item) => (
171
+ <tr key={item.departmentName} className="border-b border-slate-100">
172
+ <td className="px-3 py-4 font-semibold text-civic-text">{item.departmentName}</td>
173
+ <td className="px-3 py-4 text-civic-muted">{item.pendingCount}</td>
174
+ <td className="px-3 py-4 text-civic-muted">{item.resolvedCount}</td>
175
+ <td className="px-3 py-4 text-civic-muted">{item.averageResolutionHours.toFixed(2)}</td>
176
+ </tr>
177
+ ))}
178
+ </tbody>
179
+ </table>
180
+ </div>
181
+ </section>
182
+ </AppShell>
183
+ );
184
+ }
civic-platform/app/dashboard/complaints/[id]/page.tsx ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import {
3
+ ArrowRight,
4
+ CheckCircle2,
5
+ ClipboardList,
6
+ Clock3,
7
+ MessageSquareText,
8
+ ShieldAlert,
9
+ UserCog,
10
+ } from "lucide-react";
11
+ import { AdminAssignAction } from "@/components/admin-assign-action";
12
+ import { AdminAssignmentHistory } from "@/components/admin-assignment-history";
13
+ import { AdminNotesPanel } from "@/components/admin-notes-panel";
14
+ import { AppShell } from "@/components/app-shell";
15
+ import { AdminStatusActions } from "@/components/admin-status-actions";
16
+ import { ComplaintAiPanel } from "@/components/complaint-ai-panel";
17
+ import { ComplaintMediaGallery } from "@/components/complaint-media-gallery";
18
+ import { ResolutionProofUploader } from "@/components/resolution-proof-uploader";
19
+ import { SectionHeader } from "@/components/section-header";
20
+ import { requireRole } from "@/lib/auth";
21
+ import {
22
+ getComplaintDetail,
23
+ getComplaintAssignments,
24
+ getComplaintAiInsights,
25
+ getComplaintHistory,
26
+ getComplaintMedia,
27
+ getComplaintNotes,
28
+ type ComplaintAiInsights,
29
+ type ComplaintAssignmentItem,
30
+ type ComplaintMediaItem,
31
+ type ComplaintNoteItem,
32
+ type ComplaintStatusHistoryItem,
33
+ } from "@/lib/api";
34
+
35
+ const internalActions = [
36
+ "Validate complaint",
37
+ "Adjust domain or sub-problem if needed",
38
+ "Assign responsible department",
39
+ "Assign field officer later",
40
+ "Change lifecycle state",
41
+ "Escalate if public danger is immediate",
42
+ ];
43
+
44
+ type DashboardComplaintDetailPageProps = {
45
+ params: Promise<{
46
+ id: string;
47
+ }>;
48
+ };
49
+
50
+ function formatStatus(status: string) {
51
+ return status
52
+ .split("_")
53
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
54
+ .join(" ");
55
+ }
56
+
57
+ function formatPriority(priority: string) {
58
+ switch (priority) {
59
+ case "P1":
60
+ return "P1 Critical";
61
+ case "P2":
62
+ return "P2 High";
63
+ case "P3":
64
+ return "P3 Medium";
65
+ default:
66
+ return "P4 Low";
67
+ }
68
+ }
69
+
70
+ function formatDateTime(value: string) {
71
+ return new Date(value).toLocaleString();
72
+ }
73
+
74
+ function getPriorityRationale(priority: string, isEmergency: boolean) {
75
+ if (isEmergency || priority === "P1 Critical") {
76
+ return "Emergency or public-danger signals require immediate routing and shortest possible response time.";
77
+ }
78
+
79
+ if (priority === "P2 High") {
80
+ return "High-impact infrastructure or safety risk needs fast ownership before it grows into a wider area failure.";
81
+ }
82
+
83
+ if (priority === "P3 Medium") {
84
+ return "Standard operational issue with visible citizen impact. Keep it moving through the active queue.";
85
+ }
86
+
87
+ return "Lower-impact maintenance issue that can stay in the planned service backlog unless conditions worsen.";
88
+ }
89
+
90
+ function getRoutingConfidence(domain: string, assignments: ComplaintAssignmentItem[], department: string) {
91
+ if (assignments.length > 0) {
92
+ return `Routing has already been acted on. The latest ownership sits with ${assignments[0]?.departmentName ?? department}.`;
93
+ }
94
+
95
+ if (domain === "Fire Emergencies" || domain === "Flood and Water Disaster" || domain === "Disaster and Rescue") {
96
+ return `Emergency domain strongly matches ${department} and should stay on the fastest escalation path.`;
97
+ }
98
+
99
+ return `Current domain and complaint context point to ${department} as the primary queue owner.`;
100
+ }
101
+
102
+ function getOperationalState(status: string, assignments: ComplaintAssignmentItem[]) {
103
+ if (status === "Resolved" || status === "Closed") {
104
+ return "Work appears completed. Review proof, citizen follow-up, and any reopen risk.";
105
+ }
106
+
107
+ if (status === "Reopened" || status === "Escalated") {
108
+ return "Complaint needs renewed attention. Recheck ownership, on-ground risk, and SLA exposure.";
109
+ }
110
+
111
+ if (assignments.length > 0) {
112
+ return "Complaint is in active departmental handling. Track acceptance and field progress closely.";
113
+ }
114
+
115
+ return "Complaint still needs clear ownership. Assignment is the next critical operator action.";
116
+ }
117
+
118
+ function timelineTone(status: string) {
119
+ switch (status) {
120
+ case "submitted":
121
+ return "bg-civic-primary";
122
+ case "validated":
123
+ return "bg-civic-secondary";
124
+ case "classified":
125
+ case "prioritized":
126
+ return "bg-amber-500";
127
+ case "assigned":
128
+ return "bg-slate-800";
129
+ case "in_progress":
130
+ return "bg-emerald-600";
131
+ case "resolved":
132
+ case "closed":
133
+ return "bg-emerald-700";
134
+ case "reopened":
135
+ case "escalated":
136
+ return "bg-civic-danger";
137
+ default:
138
+ return "bg-slate-300";
139
+ }
140
+ }
141
+
142
+ export default async function DashboardComplaintDetailPage({ params }: DashboardComplaintDetailPageProps) {
143
+ const user = await requireRole(["department_operator", "municipal_admin"]);
144
+ const { id } = await params;
145
+
146
+ let complaintCode = "CP-2026-00141";
147
+ let complaintStatusValue = "submitted";
148
+ let complaintStatus = "Submitted";
149
+ let complaintPriority = "P1 Critical";
150
+ let complaintDomain = "Public Infrastructure and Amenities";
151
+ let complaintTitle = "Open manhole on high-traffic road";
152
+ let complaintLocation = "Temple Junction, Ward 4";
153
+ let complaintDescription = "High-risk public hazard affecting traffic near the junction.";
154
+ let complaintDepartment = "Municipal Maintenance";
155
+ let isEmergency = true;
156
+ let media: ComplaintMediaItem[] = [];
157
+ let assignments: ComplaintAssignmentItem[] = [];
158
+ let notes: ComplaintNoteItem[] = [];
159
+ let aiInsights: ComplaintAiInsights | null = null;
160
+ let internalTimeline: Array<{ title: string; time: string; note: string; tone: string }> = [
161
+ {
162
+ title: "Submitted",
163
+ time: "Pending",
164
+ note: "Complaint created by citizen with image and geo-tag.",
165
+ tone: "bg-civic-primary",
166
+ },
167
+ ];
168
+
169
+ try {
170
+ const [complaint, assignmentItems, history, mediaItems, noteItems, aiData] = await Promise.all([
171
+ getComplaintDetail(id),
172
+ getComplaintAssignments(id),
173
+ getComplaintHistory(id),
174
+ getComplaintMedia(id),
175
+ getComplaintNotes(id),
176
+ getComplaintAiInsights(id),
177
+ ]);
178
+
179
+ complaintCode = complaint.complaintCode;
180
+ complaintStatusValue = complaint.status;
181
+ complaintStatus = formatStatus(complaint.status);
182
+ complaintPriority = formatPriority(complaint.priorityLevel);
183
+ complaintDomain = complaint.domainName ?? complaintDomain;
184
+ complaintTitle = complaint.title;
185
+ complaintLocation = complaint.addressLine ?? complaint.landmark ?? complaintLocation;
186
+ complaintDescription = complaint.description ?? complaintDescription;
187
+ complaintDepartment = complaint.departmentName ?? complaintDepartment;
188
+ isEmergency = complaint.isEmergency;
189
+ assignments = assignmentItems;
190
+ media = mediaItems;
191
+ notes = noteItems;
192
+ aiInsights = aiData;
193
+
194
+ if (history.length > 0) {
195
+ internalTimeline = history.map((entry: ComplaintStatusHistoryItem) => ({
196
+ title: formatStatus(entry.newStatus),
197
+ time: formatDateTime(entry.createdAt),
198
+ note: entry.changeReason ?? `Complaint moved to ${formatStatus(entry.newStatus)}.`,
199
+ tone: timelineTone(entry.newStatus),
200
+ }));
201
+ }
202
+ } catch {
203
+ // Keep fallback values when backend is unavailable.
204
+ }
205
+
206
+ const complaintMeta = [
207
+ { label: "Complaint ID", value: complaintCode },
208
+ { label: "Current status", value: complaintStatus },
209
+ { label: "Priority", value: complaintPriority },
210
+ { label: "Domain", value: complaintDomain },
211
+ ];
212
+
213
+ const complaintContext = [
214
+ `Issue: ${complaintTitle}`,
215
+ `Location: ${complaintLocation}`,
216
+ `Likely department: ${complaintDepartment}`,
217
+ complaintDescription,
218
+ ];
219
+
220
+ const routingDecisionItems = [
221
+ `Current queue owner: ${assignments[0]?.departmentName ?? complaintDepartment}`,
222
+ `Operational mode: ${isEmergency ? "Emergency escalation path" : "Standard civic workflow"}`,
223
+ `Priority rationale: ${getPriorityRationale(complaintPriority, isEmergency)}`,
224
+ `Routing confidence: ${getRoutingConfidence(complaintDomain, assignments, complaintDepartment)}`,
225
+ `Current action state: ${getOperationalState(complaintStatus, assignments)}`,
226
+ ];
227
+
228
+ return (
229
+ <AppShell>
230
+ <section className="grid gap-6 rounded-[2rem] bg-civic-primary px-6 py-8 text-white shadow-civic lg:grid-cols-[1.08fr_0.92fr] lg:px-8">
231
+ <div className="space-y-4">
232
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-white/75">Internal complaint view</p>
233
+ <h1 className="text-4xl font-semibold tracking-tight">Manage one complaint from triage to resolution.</h1>
234
+ <p className="max-w-2xl text-sm leading-7 text-white/80">
235
+ Review context, route the work, assign the right team, and move the complaint through the next action.
236
+ </p>
237
+ </div>
238
+
239
+ <div className="grid gap-4 sm:grid-cols-2">
240
+ {complaintMeta.map((item) => (
241
+ <div key={item.label} className="rounded-3xl border border-white/15 bg-white/10 p-5 backdrop-blur">
242
+ <p className="text-sm font-semibold uppercase tracking-[0.16em] text-white/75">{item.label}</p>
243
+ <p className="mt-3 text-xl font-semibold text-white">{item.value}</p>
244
+ </div>
245
+ ))}
246
+ </div>
247
+ </section>
248
+
249
+ <section className="grid gap-6 lg:grid-cols-[1.12fr_0.88fr]">
250
+ <div className="space-y-6">
251
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
252
+ <SectionHeader eyebrow="Complaint context" icon={ClipboardList} title="Citizen submission details" />
253
+
254
+ <div className="mt-5 space-y-3">
255
+ {complaintContext.map((item) => (
256
+ <div key={item} className="rounded-2xl bg-slate-50 px-4 py-3 text-sm leading-6 text-civic-text">
257
+ {item}
258
+ </div>
259
+ ))}
260
+ </div>
261
+ </section>
262
+
263
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
264
+ <SectionHeader eyebrow="Lifecycle" icon={Clock3} iconClassName="bg-civic-primary text-white" title="Internal status timeline" />
265
+
266
+ <div className="mt-6 space-y-5">
267
+ {internalTimeline.map((entry, index) => (
268
+ <div key={`${entry.title}-${entry.time}-${index}`} className="grid gap-4 md:grid-cols-[auto_1fr] md:gap-5">
269
+ <div className="flex items-start gap-4 md:flex-col md:items-center">
270
+ <div className={["mt-1 h-4 w-4 rounded-full", entry.tone].join(" ")} />
271
+ {index !== internalTimeline.length - 1 ? <div className="hidden h-16 w-px bg-slate-200 md:block" /> : null}
272
+ </div>
273
+ <div className="rounded-3xl bg-slate-50 p-5">
274
+ <div className="flex flex-wrap items-center justify-between gap-3">
275
+ <h3 className="text-lg font-semibold text-civic-text">{entry.title}</h3>
276
+ <div className="inline-flex items-center gap-2 rounded-full bg-white px-3 py-2 text-xs font-semibold text-slate-700">
277
+ <Clock3 className="h-4 w-4" />
278
+ {entry.time}
279
+ </div>
280
+ </div>
281
+ <p className="mt-3 text-sm leading-6 text-civic-muted">{entry.note}</p>
282
+ </div>
283
+ </div>
284
+ ))}
285
+ </div>
286
+ </section>
287
+
288
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
289
+ <SectionHeader eyebrow="Internal notes" icon={MessageSquareText} iconClassName="bg-amber-100 text-amber-700" title="Operator-only information" />
290
+
291
+ <div className="mt-5">
292
+ <AdminNotesPanel complaintId={id} initialNotes={notes} operatorId={user.id} />
293
+ </div>
294
+ </section>
295
+
296
+ <AdminAssignmentHistory assignments={assignments} />
297
+ </div>
298
+
299
+ <div className="space-y-6">
300
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
301
+ <SectionHeader eyebrow="Priority and routing" icon={ShieldAlert} iconClassName="bg-red-100 text-civic-danger" title="Department decision block" />
302
+
303
+ <div className="mt-5 space-y-3">
304
+ {routingDecisionItems.map((item) => (
305
+ <div key={item} className="rounded-2xl bg-slate-50 px-4 py-3 text-sm leading-6 text-civic-text">
306
+ {item}
307
+ </div>
308
+ ))}
309
+ </div>
310
+
311
+ <div className="mt-5">
312
+ <AdminAssignAction complaintId={id} suggestedDepartment={complaintDepartment} operatorId={user.id} />
313
+ </div>
314
+ </section>
315
+
316
+ <ComplaintAiPanel insights={aiInsights} />
317
+
318
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
319
+ <SectionHeader eyebrow="Actions" icon={UserCog} title="Operator workflow controls" />
320
+
321
+ <div className="mt-5 space-y-3">
322
+ {internalActions.map((action) => (
323
+ <div key={action} className="rounded-2xl bg-slate-50 px-4 py-3 text-sm leading-6 text-civic-text">
324
+ {action}
325
+ </div>
326
+ ))}
327
+ </div>
328
+
329
+ <div className="mt-5">
330
+ <AdminStatusActions complaintId={id} currentStatus={complaintStatusValue} operatorId={user.id} />
331
+ </div>
332
+ </section>
333
+
334
+ <ComplaintMediaGallery
335
+ items={media}
336
+ title="Complaint evidence"
337
+ emptyLabel="No evidence has been uploaded for this complaint yet."
338
+ />
339
+
340
+ <ResolutionProofUploader complaintId={id} uploadedBy={user.id} label="Attach final proof from operations" />
341
+
342
+ <section className="rounded-[2rem] border border-emerald-200 bg-emerald-50 p-6 shadow-sm">
343
+ <div className="flex items-start gap-3">
344
+ <CheckCircle2 className="mt-0.5 h-5 w-5 text-emerald-700" />
345
+ <div>
346
+ <h2 className="text-lg font-semibold text-emerald-950">Internal-only context</h2>
347
+ <p className="mt-2 text-sm leading-6 text-emerald-900/85">
348
+ Notes, routing context, and assignment controls stay visible only to internal teams.
349
+ </p>
350
+ </div>
351
+ </div>
352
+ </section>
353
+ </div>
354
+ </section>
355
+
356
+ <section className="flex flex-wrap items-center justify-between gap-4 rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
357
+ <div className="space-y-2">
358
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Queue navigation</p>
359
+ <h2 className="text-2xl font-semibold text-civic-text">Jump back to the queue or move into emergency operations.</h2>
360
+ <p className="max-w-2xl text-sm leading-6 text-civic-muted">
361
+ Use the queue to continue triage work, or switch to emergency reporting when a fast-response situation needs immediate handling.
362
+ </p>
363
+ </div>
364
+
365
+ <div className="flex flex-wrap gap-3">
366
+ <Link
367
+ className="inline-flex rounded-full border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-civic-primary hover:text-civic-primary"
368
+ href="/dashboard/complaints"
369
+ >
370
+ Back to queue
371
+ </Link>
372
+ <Link
373
+ className="inline-flex items-center gap-2 rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5"
374
+ href="/emergency"
375
+ >
376
+ Open emergency reporting
377
+ <ArrowRight className="h-4 w-4" />
378
+ </Link>
379
+ </div>
380
+ </section>
381
+ </AppShell>
382
+ );
383
+ }
civic-platform/app/dashboard/complaints/page.tsx ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AppShell } from "@/components/app-shell";
2
+ import {
3
+ DashboardQueueClient,
4
+ type QueueComplaintCard,
5
+ } from "@/components/dashboard-queue-client";
6
+ import { getAllComplaints } from "@/lib/api";
7
+ import { requireRole } from "@/lib/auth";
8
+
9
+ const filterGroups = [
10
+ {
11
+ title: "Status" as const,
12
+ options: ["All", "Submitted", "Assigned", "In Progress", "Resolved", "Reopened"],
13
+ },
14
+ {
15
+ title: "Priority" as const,
16
+ options: ["All", "P1 Critical", "P2 High", "P3 Medium", "P4 Low"],
17
+ },
18
+ {
19
+ title: "Domain" as const,
20
+ options: ["All", "Roads", "Sanitation", "Water", "Electrical", "Infrastructure", "Environment"],
21
+ },
22
+ ];
23
+
24
+ const fallbackComplaints: QueueComplaintCard[] = [
25
+ {
26
+ id: "CP-2026-00141",
27
+ complaintCode: "CP-2026-00141",
28
+ title: "Open manhole on high-traffic road",
29
+ domain: "Public Infrastructure and Amenities",
30
+ status: "Submitted",
31
+ priority: "P1 Critical",
32
+ department: "Municipal Maintenance",
33
+ location: "Temple Junction, Ward 4",
34
+ updatedAt: "5 minutes ago",
35
+ submittedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
36
+ },
37
+ {
38
+ id: "CP-2026-00139",
39
+ complaintCode: "CP-2026-00139",
40
+ title: "Live wire hanging near bus stop",
41
+ domain: "Electrical Hazard",
42
+ status: "Assigned",
43
+ priority: "P1 Critical",
44
+ department: "Electrical Safety Response",
45
+ location: "Bus Stand Road, Ward 9",
46
+ updatedAt: "12 minutes ago",
47
+ submittedAt: new Date(Date.now() - 12 * 60 * 1000).toISOString(),
48
+ },
49
+ {
50
+ id: "CP-2026-00124",
51
+ complaintCode: "CP-2026-00124",
52
+ title: "Sewage overflow near market entrance",
53
+ domain: "Water Supply, Sewerage, and Drainage",
54
+ status: "In Progress",
55
+ priority: "P2 High",
56
+ department: "Water and Sewerage",
57
+ location: "Market Road, Ward 12",
58
+ updatedAt: "1 hour ago",
59
+ submittedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
60
+ },
61
+ ];
62
+
63
+ function formatStatus(status: string) {
64
+ return status
65
+ .split("_")
66
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
67
+ .join(" ");
68
+ }
69
+
70
+ function formatPriority(priority: string) {
71
+ switch (priority) {
72
+ case "P1":
73
+ return "P1 Critical";
74
+ case "P2":
75
+ return "P2 High";
76
+ case "P3":
77
+ return "P3 Medium";
78
+ default:
79
+ return "P4 Low";
80
+ }
81
+ }
82
+
83
+ export default async function DashboardComplaintsPage() {
84
+ const user = await requireRole(["department_operator", "municipal_admin"]);
85
+ let complaints: QueueComplaintCard[] = fallbackComplaints;
86
+
87
+ try {
88
+ const apiComplaints = await getAllComplaints();
89
+
90
+ if (apiComplaints.length > 0) {
91
+ complaints = apiComplaints.map((complaint) => ({
92
+ id: complaint.id,
93
+ complaintCode: complaint.complaintCode,
94
+ title: complaint.title,
95
+ domain: complaint.domainName ?? "Unclassified",
96
+ status: formatStatus(complaint.status),
97
+ priority: formatPriority(complaint.priorityLevel),
98
+ department: complaint.departmentName ?? "Department pending",
99
+ location: complaint.addressLine ?? complaint.landmark ?? "Location pending",
100
+ updatedAt: new Date(complaint.submittedAt).toLocaleString(),
101
+ submittedAt: complaint.submittedAt,
102
+ }));
103
+ }
104
+ } catch {
105
+ complaints = fallbackComplaints;
106
+ }
107
+
108
+ return (
109
+ <AppShell>
110
+ <DashboardQueueClient complaints={complaints} filterGroups={filterGroups} operatorId={user.id} />
111
+ </AppShell>
112
+ );
113
+ }
civic-platform/app/dashboard/page.tsx ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import {
3
+ ArrowRight,
4
+ Building2,
5
+ Clock3,
6
+ MapPinned,
7
+ Siren,
8
+ TimerReset,
9
+ } from "lucide-react";
10
+ import { AppShell } from "@/components/app-shell";
11
+ import { EmptyState } from "@/components/empty-state";
12
+ import { LiveSyncBadge } from "@/components/live-sync-badge";
13
+ import { PriorityBadge } from "@/components/priority-badge";
14
+ import { requireRole } from "@/lib/auth";
15
+ import { getDashboardSummary, type DashboardSummary } from "@/lib/api";
16
+
17
+ const focusItems = [
18
+ "Urgent complaints",
19
+ "Unaccepted assignments",
20
+ "SLA breaches",
21
+ "Pending field updates",
22
+ ];
23
+
24
+ export default async function DashboardPage() {
25
+ await requireRole(["department_operator", "municipal_admin"]);
26
+
27
+ const fallbackSummary: DashboardSummary = {
28
+ overview: {
29
+ openComplaints: 184,
30
+ criticalComplaints: 7,
31
+ slaRisk: 19,
32
+ resolvedToday: 42,
33
+ },
34
+ urgentQueue: [
35
+ {
36
+ id: "sample-urgent-1",
37
+ complaintCode: "CP-2026-00141",
38
+ title: "Open manhole on high-traffic road",
39
+ domainName: "Public Infrastructure and Amenities",
40
+ priorityLevel: "P1",
41
+ location: "Temple Junction, Ward 4",
42
+ submittedAt: new Date().toISOString(),
43
+ },
44
+ {
45
+ id: "sample-urgent-2",
46
+ complaintCode: "CP-2026-00139",
47
+ title: "Live wire hanging near bus stop",
48
+ domainName: "Electrical Hazard",
49
+ priorityLevel: "P1",
50
+ location: "Bus Stand Road, Ward 9",
51
+ submittedAt: new Date().toISOString(),
52
+ },
53
+ ],
54
+ departmentWorkload: [
55
+ { departmentName: "Roads and Public Works", pendingCount: 31 },
56
+ { departmentName: "Sanitation and Solid Waste", pendingCount: 28 },
57
+ { departmentName: "Water and Sewerage", pendingCount: 44 },
58
+ { departmentName: "Electrical and Street Lighting", pendingCount: 22 },
59
+ ],
60
+ };
61
+
62
+ let summary: DashboardSummary = fallbackSummary;
63
+
64
+ try {
65
+ summary = await getDashboardSummary();
66
+ } catch {
67
+ summary = fallbackSummary;
68
+ }
69
+
70
+ const overviewStats = [
71
+ { label: "Open complaints", value: String(summary.overview.openComplaints), tone: "text-civic-text" },
72
+ { label: "Critical", value: String(summary.overview.criticalComplaints), tone: "text-civic-danger" },
73
+ { label: "SLA risk", value: String(summary.overview.slaRisk), tone: "text-amber-700" },
74
+ { label: "Resolved today", value: String(summary.overview.resolvedToday), tone: "text-emerald-700" },
75
+ ];
76
+
77
+ return (
78
+ <AppShell>
79
+ <section className="relative overflow-hidden rounded-[2.5rem] bg-gradient-to-br from-[#09090b] via-civic-primary to-[#1e3a8a] px-8 py-10 text-white shadow-civic lg:px-10 lg:py-12">
80
+ <div className="absolute -left-32 -top-32 h-96 w-96 rounded-full bg-white/5 blur-[100px]" />
81
+ <div className="relative flex flex-wrap items-end justify-between gap-6">
82
+ <div className="space-y-4">
83
+ <p className="text-sm font-bold uppercase tracking-[0.25em] text-white/70">Operations</p>
84
+ <h1 className="text-5xl font-extrabold tracking-tight">Command Center</h1>
85
+ </div>
86
+ <div className="flex flex-wrap items-center gap-4">
87
+ <LiveSyncBadge label="Live Refresh" />
88
+ <Link
89
+ className="group inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/5 px-6 py-3.5 text-sm font-semibold text-white backdrop-blur-md transition hover:border-white/40 hover:bg-white/10"
90
+ href="/dashboard/analytics"
91
+ >
92
+ Analytics
93
+ <ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
94
+ </Link>
95
+ <Link
96
+ className="group inline-flex items-center gap-2 rounded-full bg-white px-6 py-3.5 text-sm font-bold text-civic-primary shadow-xl transition hover:-translate-y-0.5 hover:shadow-2xl"
97
+ href="/dashboard/complaints"
98
+ >
99
+ Open Queue
100
+ <ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
101
+ </Link>
102
+ </div>
103
+ </div>
104
+
105
+ <div className="relative mt-8 grid gap-5 sm:grid-cols-2 xl:grid-cols-4">
106
+ {overviewStats.map((stat) => (
107
+ <div key={stat.label} className="group rounded-[2rem] border border-white/15 bg-white/10 p-6 backdrop-blur-xl transition hover:bg-white/15">
108
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-white/60">{stat.label}</p>
109
+ <p className={["mt-4 text-4xl font-extrabold tracking-tight", stat.tone.replace("text-civic-text", "text-white").replace("text-civic-danger", "text-rose-400").replace("text-amber-700", "text-amber-400").replace("text-emerald-700", "text-emerald-400")].join(" ")}>{stat.value}</p>
110
+ </div>
111
+ ))}
112
+ </div>
113
+ </section>
114
+
115
+ <section className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
116
+ <section className="rounded-[2.5rem] border border-slate-200 bg-white/80 p-8 shadow-glass backdrop-blur-3xl transition hover:shadow-glass-hover">
117
+ <div className="flex flex-wrap items-center justify-between gap-4">
118
+ <div>
119
+ <p className="text-sm font-bold uppercase tracking-[0.25em] text-civic-secondary">Urgent queue</p>
120
+ <h2 className="mt-2 text-3xl font-extrabold text-slate-900">Priority Incidents</h2>
121
+ </div>
122
+ <div className="inline-flex animate-pulse items-center gap-2 rounded-full bg-rose-100 px-4 py-2 text-sm font-bold text-rose-700">
123
+ <Siren className="h-4 w-4" />
124
+ Live Feed
125
+ </div>
126
+ </div>
127
+
128
+ <div className="mt-8 space-y-5">
129
+ {summary.urgentQueue.length === 0 ? (
130
+ <EmptyState
131
+ description="Critical complaints will appear here as soon as they enter the queue."
132
+ icon={Siren}
133
+ title="No urgent complaints right now"
134
+ />
135
+ ) : (
136
+ summary.urgentQueue.map((item) => (
137
+ <article key={item.id} className="group relative overflow-hidden rounded-[2rem] border-2 border-rose-100 bg-gradient-to-br from-rose-50 to-white p-6 shadow-sm transition-all hover:-translate-y-1 hover:border-rose-200 hover:shadow-xl">
138
+ <div className="absolute right-0 top-0 h-32 w-32 -translate-y-8 translate-x-8 rounded-full bg-rose-200/50 blur-3xl transition group-hover:bg-rose-300/50" />
139
+ <div className="relative flex flex-wrap items-start justify-between gap-4">
140
+ <div className="space-y-3">
141
+ <div className="flex flex-wrap items-center gap-3">
142
+ <span className="rounded-full bg-white px-3.5 py-1 text-xs font-bold text-slate-700 shadow-sm">
143
+ {item.complaintCode}
144
+ </span>
145
+ <PriorityBadge priority={`${item.priorityLevel} ${item.priorityLevel === "P1" ? "Critical" : item.priorityLevel === "P2" ? "High" : item.priorityLevel === "P3" ? "Medium" : "Low"}`} />
146
+ </div>
147
+ <h3 className="text-2xl font-bold text-slate-900">{item.title}</h3>
148
+ <p className="text-sm font-medium text-slate-500">{item.domainName ?? "Unclassified"}</p>
149
+ </div>
150
+
151
+ <div className="rounded-full bg-white/80 px-4 py-2 text-xs font-bold text-rose-700 shadow-sm backdrop-blur-md">
152
+ {new Date(item.submittedAt).toLocaleString()}
153
+ </div>
154
+ </div>
155
+
156
+ <div className="relative mt-5 flex flex-wrap items-center gap-4 text-sm font-medium text-slate-600">
157
+ <div className="inline-flex items-center gap-2">
158
+ <MapPinned className="h-4 w-4 text-slate-400" />
159
+ {item.location ?? "Location pending"}
160
+ </div>
161
+ </div>
162
+
163
+ <div className="relative mt-6">
164
+ <Link
165
+ className="group/btn inline-flex items-center gap-2 rounded-full bg-slate-900 px-6 py-3 text-sm font-bold text-white shadow-md transition hover:bg-slate-800 hover:shadow-lg"
166
+ href={`/dashboard/complaints/${item.id}`}
167
+ >
168
+ Open Incident
169
+ <ArrowRight className="h-4 w-4 transition-transform group-hover/btn:translate-x-1" />
170
+ </Link>
171
+ </div>
172
+ </article>
173
+ ))
174
+ )}
175
+ </div>
176
+ </section>
177
+
178
+ <div className="space-y-6">
179
+ <section className="rounded-[2.5rem] border border-slate-200 bg-white/80 p-8 shadow-glass backdrop-blur-3xl transition hover:shadow-glass-hover">
180
+ <div className="flex items-center gap-4">
181
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-civic-secondary/15 text-civic-secondary shadow-inner">
182
+ <Building2 className="h-6 w-6" />
183
+ </div>
184
+ <div>
185
+ <p className="text-sm font-bold uppercase tracking-[0.2em] text-civic-secondary">Departments</p>
186
+ <h2 className="text-2xl font-extrabold text-slate-900">Current Workload</h2>
187
+ </div>
188
+ </div>
189
+
190
+ <div className="mt-8 space-y-4">
191
+ {summary.departmentWorkload.length === 0 ? (
192
+ <EmptyState
193
+ description="Department workload will appear here once complaints are assigned into active queues."
194
+ icon={Building2}
195
+ title="No department workload yet"
196
+ />
197
+ ) : (
198
+ summary.departmentWorkload.map((item) => (
199
+ <div key={item.departmentName} className="group rounded-3xl border border-slate-100 bg-white p-5 shadow-sm transition hover:-translate-y-1 hover:border-slate-200 hover:shadow-md">
200
+ <div className="flex items-center justify-between gap-4">
201
+ <h3 className="text-lg font-bold text-slate-800">{item.departmentName}</h3>
202
+ <span className="rounded-full bg-slate-100 px-4 py-1.5 text-xs font-bold text-slate-700 transition group-hover:bg-civic-secondary group-hover:text-white">
203
+ {item.pendingCount} pending
204
+ </span>
205
+ </div>
206
+ </div>
207
+ ))
208
+ )}
209
+ </div>
210
+ </section>
211
+
212
+ <section className="rounded-[2.5rem] border border-slate-200 bg-white/80 p-8 shadow-glass backdrop-blur-3xl transition hover:shadow-glass-hover">
213
+ <div className="flex items-center gap-4">
214
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-amber-100 text-amber-600 shadow-inner">
215
+ <TimerReset className="h-6 w-6" />
216
+ </div>
217
+ <div>
218
+ <p className="text-sm font-bold uppercase tracking-[0.2em] text-civic-secondary">Focus</p>
219
+ <h2 className="text-2xl font-extrabold text-slate-900">Immediate Attention</h2>
220
+ </div>
221
+ </div>
222
+
223
+ <div className="mt-8 grid gap-4 sm:grid-cols-2">
224
+ {focusItems.map((item) => (
225
+ <div key={item} className="rounded-2xl border border-slate-100 bg-white px-5 py-4 text-sm font-bold text-slate-700 shadow-sm transition hover:border-slate-200 hover:shadow-md">
226
+ {item}
227
+ </div>
228
+ ))}
229
+ </div>
230
+ </section>
231
+
232
+ <section className="overflow-hidden rounded-[2.5rem] bg-gradient-to-br from-civic-primary to-blue-900 p-8 text-white shadow-xl transition hover:shadow-2xl">
233
+ <div className="flex items-center gap-4">
234
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/20 shadow-inner backdrop-blur-md">
235
+ <Clock3 className="h-6 w-6 text-white" />
236
+ </div>
237
+ <div>
238
+ <p className="text-sm font-bold uppercase tracking-[0.2em] text-white/70">Queue</p>
239
+ <h2 className="text-2xl font-extrabold">Continue Operations</h2>
240
+ </div>
241
+ </div>
242
+
243
+ <div className="mt-8">
244
+ <Link
245
+ className="group inline-flex items-center gap-3 rounded-full bg-white px-7 py-4 text-sm font-bold text-civic-primary shadow-lg transition hover:-translate-y-1 hover:shadow-xl"
246
+ href="/dashboard/complaints"
247
+ >
248
+ Go to Complaint Queue
249
+ <ArrowRight className="h-5 w-5 transition-transform group-hover:translate-x-1" />
250
+ </Link>
251
+ </div>
252
+ </section>
253
+ </div>
254
+ </section>
255
+ </AppShell>
256
+ );
257
+ }
civic-platform/app/emergency/page.tsx ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import { AlertTriangle, Flame, RadioTower, ShieldAlert, Siren, Waves, Zap } from "lucide-react";
3
+ import { AppShell } from "@/components/app-shell";
4
+
5
+ const emergencyTypes = [
6
+ {
7
+ title: "Fire",
8
+ summary: "Fire outbreak, smoke, electrical fire, or public flame hazard.",
9
+ icon: Flame,
10
+ },
11
+ {
12
+ title: "Flood",
13
+ summary: "Flooding, dangerous water accumulation, or rapid overflow.",
14
+ icon: Waves,
15
+ },
16
+ {
17
+ title: "Electrical Hazard",
18
+ summary: "Live wire, transformer blast, or severe public electrical risk.",
19
+ icon: Zap,
20
+ },
21
+ {
22
+ title: "Rescue / Disaster",
23
+ summary: "Collapse, storm damage, trapped people, or multi-agency incident.",
24
+ icon: RadioTower,
25
+ },
26
+ ];
27
+
28
+ const emergencyRules = [
29
+ "Emergency reports should take fewer steps than standard complaints.",
30
+ "Location must be captured first or confirmed manually.",
31
+ "Priority should default to P1 unless the complaint is invalid.",
32
+ "The system should route directly to the emergency queue.",
33
+ ];
34
+
35
+ const minimalFields = [
36
+ "Emergency type",
37
+ "Photo or short video",
38
+ "Current location or exact pin",
39
+ "Short description of danger",
40
+ "Optional contact number",
41
+ ];
42
+
43
+ export default function EmergencyPage() {
44
+ return (
45
+ <AppShell>
46
+ <section className="grid gap-6 rounded-[2rem] bg-civic-danger px-6 py-8 text-white shadow-civic lg:grid-cols-[1.08fr_0.92fr] lg:px-8">
47
+ <div className="space-y-4">
48
+ <div className="inline-flex items-center gap-2 rounded-full bg-white/10 px-4 py-2 text-sm font-medium text-white/90">
49
+ <Siren className="h-4 w-4" />
50
+ Emergency reporting flow
51
+ </div>
52
+ <h1 className="text-4xl font-semibold tracking-tight">Use the fastest possible flow for urgent public danger.</h1>
53
+ <p className="max-w-2xl text-sm leading-7 text-white/85">
54
+ Report fire, flood, electrical danger, or rescue situations with the fewest possible steps so the response team can act quickly.
55
+ </p>
56
+ </div>
57
+
58
+ <div className="rounded-[1.75rem] border border-white/15 bg-white/10 p-5 backdrop-blur">
59
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-white/75">Default response behavior</p>
60
+ <div className="mt-4 space-y-3">
61
+ {[
62
+ "Priority set to P1 Critical",
63
+ "Emergency queue routing",
64
+ "Immediate operator visibility",
65
+ "Stronger notification path",
66
+ ].map((item) => (
67
+ <div key={item} className="rounded-2xl bg-white/10 px-4 py-3 text-sm font-medium text-white">
68
+ {item}
69
+ </div>
70
+ ))}
71
+ </div>
72
+ </div>
73
+ </section>
74
+
75
+ <section className="grid gap-6 lg:grid-cols-[1.02fr_0.98fr]">
76
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
77
+ <div className="flex flex-wrap items-start justify-between gap-4">
78
+ <div>
79
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Emergency type</p>
80
+ <h2 className="mt-2 text-2xl font-semibold text-civic-text">Choose the urgent incident category</h2>
81
+ </div>
82
+ <div className="rounded-full bg-red-50 px-4 py-2 text-sm font-semibold text-red-700">
83
+ High priority flow
84
+ </div>
85
+ </div>
86
+
87
+ <div className="mt-6 grid gap-4 md:grid-cols-2">
88
+ {emergencyTypes.map(({ title, summary, icon: Icon }) => (
89
+ <button
90
+ key={title}
91
+ type="button"
92
+ className="group rounded-3xl border border-red-100 bg-red-50/50 p-5 text-left transition duration-200 hover:-translate-y-1 hover:border-civic-danger hover:bg-white hover:shadow-civic"
93
+ >
94
+ <div className="flex items-start justify-between gap-4">
95
+ <div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-white text-civic-danger">
96
+ <Icon className="h-6 w-6" />
97
+ </div>
98
+ <span className="rounded-full bg-white px-3 py-1 text-xs font-semibold text-civic-danger">Select</span>
99
+ </div>
100
+ <h3 className="mt-4 text-lg font-semibold text-civic-text">{title}</h3>
101
+ <p className="mt-2 text-sm leading-6 text-civic-muted">{summary}</p>
102
+ </button>
103
+ ))}
104
+ </div>
105
+ </section>
106
+
107
+ <div className="space-y-6">
108
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
109
+ <div className="flex items-center gap-3">
110
+ <div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-civic-danger/10 text-civic-danger">
111
+ <ShieldAlert className="h-5 w-5" />
112
+ </div>
113
+ <div>
114
+ <p className="text-sm font-semibold uppercase tracking-[0.16em] text-civic-secondary">Form scope</p>
115
+ <h2 className="text-xl font-semibold text-civic-text">Minimal fields for emergency submission</h2>
116
+ </div>
117
+ </div>
118
+
119
+ <div className="mt-5 space-y-3">
120
+ {minimalFields.map((field) => (
121
+ <div key={field} className="rounded-2xl bg-slate-50 px-4 py-3 text-sm font-medium text-civic-text">
122
+ {field}
123
+ </div>
124
+ ))}
125
+ </div>
126
+ </section>
127
+
128
+ <section className="rounded-[2rem] border border-amber-200 bg-amber-50 p-6 shadow-sm">
129
+ <div className="flex items-start gap-3">
130
+ <AlertTriangle className="mt-0.5 h-5 w-5 text-amber-700" />
131
+ <div>
132
+ <h2 className="text-lg font-semibold text-amber-950">Emergency reporting principles</h2>
133
+ <p className="mt-2 text-sm leading-6 text-amber-900/85">
134
+ This flow stays shorter than normal reporting and focuses only on what responders need first.
135
+ </p>
136
+ </div>
137
+ </div>
138
+
139
+ <div className="mt-5 space-y-3">
140
+ {emergencyRules.map((rule) => (
141
+ <div key={rule} className="rounded-2xl bg-white/75 px-4 py-3 text-sm leading-6 text-amber-950">
142
+ {rule}
143
+ </div>
144
+ ))}
145
+ </div>
146
+ </section>
147
+ </div>
148
+ </section>
149
+
150
+ <section className="flex flex-wrap items-center justify-between gap-4 rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
151
+ <div>
152
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Switch reporting modes</p>
153
+ <h2 className="mt-2 text-2xl font-semibold text-civic-text">Return to the normal report flow or operations dashboard.</h2>
154
+ <p className="mt-2 max-w-2xl text-sm leading-6 text-civic-muted">
155
+ Use the standard reporting flow for non-emergency complaints, or return to the dashboard to monitor active work.
156
+ </p>
157
+ </div>
158
+
159
+ <div className="flex flex-wrap gap-3">
160
+ <Link
161
+ className="inline-flex rounded-full border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-civic-primary hover:text-civic-primary"
162
+ href="/report"
163
+ >
164
+ Back to normal report flow
165
+ </Link>
166
+ <Link
167
+ className="inline-flex rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5"
168
+ href="/dashboard"
169
+ >
170
+ Return to dashboard
171
+ </Link>
172
+ </div>
173
+ </section>
174
+ </AppShell>
175
+ );
176
+ }
civic-platform/app/globals.css ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
5
+ @import "mapbox-gl/dist/mapbox-gl.css";
6
+
7
+ :root {
8
+ color-scheme: light;
9
+ }
10
+
11
+ html {
12
+ scroll-behavior: smooth;
13
+ }
14
+
15
+ body {
16
+ margin: 0;
17
+ min-height: 100vh;
18
+ background:
19
+ radial-gradient(circle at top left, rgba(20, 184, 166, 0.08), transparent 45%),
20
+ radial-gradient(circle at bottom right, rgba(15, 76, 129, 0.08), transparent 40%),
21
+ linear-gradient(to bottom, #f8fafc, #f1f5f9);
22
+ background-attachment: fixed;
23
+ color: #0f172a;
24
+ font-family: 'Inter', Arial, Helvetica, sans-serif;
25
+ letter-spacing: -0.015em;
26
+ overflow-x: hidden;
27
+ -webkit-font-smoothing: antialiased;
28
+ }
29
+
30
+ /* Premium Scrollbar */
31
+ ::-webkit-scrollbar {
32
+ width: 8px;
33
+ height: 8px;
34
+ }
35
+
36
+ ::-webkit-scrollbar-track {
37
+ background: transparent;
38
+ }
39
+
40
+ ::-webkit-scrollbar-thumb {
41
+ background: rgba(100, 116, 139, 0.3);
42
+ border-radius: 10px;
43
+ }
44
+
45
+ ::-webkit-scrollbar-thumb:hover {
46
+ background: rgba(100, 116, 139, 0.5);
47
+ }
48
+
49
+ * {
50
+ box-sizing: border-box;
51
+ }
52
+
53
+ a {
54
+ color: inherit;
55
+ text-decoration: none;
56
+ }
57
+
58
+ a {
59
+ color: inherit;
60
+ text-decoration: none;
61
+ }
civic-platform/app/layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Civic Platform",
6
+ description: "Crowdsourced civic issue reporting and resolution system",
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: Readonly<{
12
+ children: React.ReactNode;
13
+ }>) {
14
+ return (
15
+ <html lang="en">
16
+ <body suppressHydrationWarning>{children}</body>
17
+ </html>
18
+ );
19
+ }
civic-platform/app/login/page.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { redirect } from "next/navigation";
2
+ import { ShieldCheck } from "lucide-react";
3
+ import { AppShell } from "@/components/app-shell";
4
+ import { LoginForm } from "@/components/login-form";
5
+ import { getCurrentUser } from "@/lib/auth";
6
+
7
+ export default async function LoginPage() {
8
+ const user = await getCurrentUser();
9
+
10
+ if (user) {
11
+ redirect(user.role === "citizen" ? "/complaints" : user.role === "field_officer" ? "/tasks" : "/dashboard");
12
+ }
13
+
14
+ return (
15
+ <AppShell>
16
+ <div className="mx-auto flex min-h-[75vh] w-full max-w-[420px] flex-col justify-center py-10 px-4 sm:px-0">
17
+ <div className="mb-8 flex flex-col items-center text-center">
18
+ <div className="mb-6 flex h-16 w-16 items-center justify-center rounded-[1.5rem] bg-gradient-to-br from-civic-primary to-[#0b5d87] text-white shadow-xl shadow-civic-primary/20">
19
+ <ShieldCheck className="h-8 w-8" />
20
+ </div>
21
+ <h1 className="text-3xl font-extrabold tracking-tight text-slate-900">
22
+ Welcome back
23
+ </h1>
24
+ <p className="mt-2 text-base text-slate-500">
25
+ Sign in or create an account to participate
26
+ </p>
27
+ </div>
28
+
29
+ <div className="relative w-full">
30
+ {/* Subtle background glow */}
31
+ <div className="absolute -inset-1 rounded-[2.5rem] bg-gradient-to-br from-civic-primary/15 via-transparent to-civic-secondary/10 blur-xl"></div>
32
+
33
+ <div className="relative rounded-[2.5rem] border border-white/60 bg-white/70 p-6 shadow-2xl shadow-sky-900/5 backdrop-blur-2xl sm:p-8">
34
+ <LoginForm />
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </AppShell>
39
+ );
40
+ }
civic-platform/app/map/page.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AppShell } from "@/components/app-shell";
2
+ import { IssueMapBoard } from "@/components/issue-map-board";
3
+ import { getCurrentUser } from "@/lib/auth";
4
+ import {
5
+ getMapComplaints,
6
+ getPublicHotspots,
7
+ type ComplaintHotspotItem,
8
+ type ComplaintMapItem,
9
+ } from "@/lib/api";
10
+
11
+ const fallbackComplaints: ComplaintMapItem[] = [];
12
+ const fallbackHotspots: ComplaintHotspotItem[] = [];
13
+
14
+ export default async function MapPage() {
15
+ const user = await getCurrentUser();
16
+ const mapboxToken = process.env.MAPBOX_TOKEN ?? process.env.NEXT_PUBLIC_MAPBOX_TOKEN;
17
+ const isOperationsView =
18
+ user?.role === "department_operator" || user?.role === "municipal_admin" || user?.role === "field_officer";
19
+
20
+ let complaints = fallbackComplaints;
21
+ let hotspots = fallbackHotspots;
22
+
23
+ try {
24
+ [complaints, hotspots] = await Promise.all([
25
+ getMapComplaints({ publicOnly: !isOperationsView }),
26
+ getPublicHotspots(),
27
+ ]);
28
+ } catch {
29
+ complaints = fallbackComplaints;
30
+ hotspots = fallbackHotspots;
31
+ }
32
+
33
+ return (
34
+ <AppShell>
35
+ <section className="space-y-3">
36
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">
37
+ {isOperationsView ? "Operations map" : "Public map"}
38
+ </p>
39
+ <h1 className="text-4xl font-semibold tracking-tight text-civic-text">
40
+ {isOperationsView
41
+ ? "Monitor live civic issue activity with city-wide visibility."
42
+ : "Explore active civic issues and hotspots across the city."}
43
+ </h1>
44
+ <p className="max-w-3xl text-sm leading-7 text-civic-muted">
45
+ {isOperationsView
46
+ ? "Use the live operations map to spot workload clusters, emergency incidents, and active complaint zones."
47
+ : "Residents can browse public-safe complaint activity, see active clusters, and understand where issues are already being addressed."}
48
+ </p>
49
+ </section>
50
+
51
+ <IssueMapBoard
52
+ complaints={complaints}
53
+ hotspots={hotspots}
54
+ mapboxToken={mapboxToken}
55
+ mode={isOperationsView ? "operations" : "public"}
56
+ />
57
+ </AppShell>
58
+ );
59
+ }
civic-platform/app/notifications/page.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AppShell } from "@/components/app-shell";
2
+ import { LiveSyncBadge } from "@/components/live-sync-badge";
3
+ import { NotificationCenter } from "@/components/notification-center";
4
+ import { requireUser } from "@/lib/auth";
5
+ import { getNotifications, type ComplaintNotificationItem } from "@/lib/api";
6
+
7
+ const fallbackNotifications: ComplaintNotificationItem[] = [];
8
+
9
+ export default async function NotificationsPage() {
10
+ const user = await requireUser();
11
+ let notifications = fallbackNotifications;
12
+
13
+ try {
14
+ notifications = await getNotifications(user.id);
15
+ } catch {
16
+ notifications = fallbackNotifications;
17
+ }
18
+
19
+ return (
20
+ <AppShell>
21
+ <section className="space-y-3">
22
+ <div className="flex flex-wrap items-center justify-between gap-3">
23
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Notifications</p>
24
+ <LiveSyncBadge label="Inbox live refresh" />
25
+ </div>
26
+ <h1 className="text-4xl font-semibold tracking-tight text-civic-text">See the latest updates on your work and complaints.</h1>
27
+ <p className="max-w-3xl text-sm leading-7 text-civic-muted">
28
+ Assignment, field progress, reopening, and citizen verification updates appear here.
29
+ </p>
30
+ </section>
31
+
32
+ <NotificationCenter notifications={notifications} userId={user.id} />
33
+ </AppShell>
34
+ );
35
+ }
civic-platform/app/page.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DomainGrid } from "@/components/domain-grid";
2
+ import { Hero } from "@/components/hero";
3
+ import { AppShell } from "@/components/app-shell";
4
+
5
+ export default function Home() {
6
+ return (
7
+ <AppShell>
8
+ <Hero />
9
+
10
+ <section id="domains" className="py-6">
11
+ <DomainGrid />
12
+ </section>
13
+ </AppShell>
14
+ );
15
+ }
civic-platform/app/report/location/page.tsx ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import { Crosshair, MapPinned, Navigation, ShieldAlert, Sparkles } from "lucide-react";
3
+ import { AppShell } from "@/components/app-shell";
4
+ import { FlowSteps } from "@/components/flow-steps";
5
+
6
+ const steps = ["Choose domain", "Add evidence", "Confirm location", "Review", "Submit"];
7
+
8
+ const locationChecks = [
9
+ "Use current GPS for the quickest routing path.",
10
+ "If GPS is weak, move the map pin to the exact road, lane, or landmark.",
11
+ "Mention a nearby landmark so field staff can find the problem faster.",
12
+ "Emergency incidents should be pinned as precisely as possible.",
13
+ ];
14
+
15
+ const locationFields = [
16
+ "Latitude and longitude",
17
+ "Resolved address",
18
+ "Ward or zone",
19
+ "Nearby landmark",
20
+ "Road or locality name",
21
+ "Manual pin adjustment status",
22
+ ];
23
+
24
+ export default function ReportLocationPage() {
25
+ return (
26
+ <AppShell>
27
+ <section className="space-y-6">
28
+ <div className="space-y-3">
29
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Report flow</p>
30
+ <h1 className="text-4xl font-semibold tracking-tight text-civic-text">Confirm the issue location</h1>
31
+ <p className="max-w-3xl text-sm leading-7 text-civic-muted">
32
+ This step helps the system assign the correct ward, identify the responsible department, and improve
33
+ routing accuracy for field teams.
34
+ </p>
35
+ </div>
36
+
37
+ <FlowSteps currentStep={3} steps={steps} />
38
+ </section>
39
+
40
+ <section className="grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
41
+ <div className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
42
+ <div className="flex flex-wrap items-start justify-between gap-4">
43
+ <div className="space-y-2">
44
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Step 3</p>
45
+ <h2 className="text-2xl font-semibold text-civic-text">Map and address confirmation</h2>
46
+ <p className="max-w-2xl text-sm leading-6 text-civic-muted">
47
+ In the MVP, this screen should open the user location, show a map preview, and let the citizen correct
48
+ the pin before moving to review.
49
+ </p>
50
+ </div>
51
+ <div className="rounded-full bg-slate-100 px-4 py-2 text-sm font-semibold text-slate-700">
52
+ Routing-ready
53
+ </div>
54
+ </div>
55
+
56
+ <div className="mt-6 rounded-[1.75rem] border border-slate-200 bg-slate-50 p-5">
57
+ <div className="flex items-center gap-3">
58
+ <div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-civic-primary text-white">
59
+ <MapPinned className="h-6 w-6" />
60
+ </div>
61
+ <div>
62
+ <p className="text-sm font-semibold uppercase tracking-[0.16em] text-civic-secondary">Interactive map</p>
63
+ <h3 className="text-lg font-semibold text-civic-text">Pinned complaint location</h3>
64
+ </div>
65
+ </div>
66
+
67
+ <div className="mt-5 flex min-h-[22rem] items-center justify-center rounded-[1.5rem] border border-dashed border-slate-300 bg-[linear-gradient(135deg,rgba(15,76,129,0.05),rgba(20,184,166,0.08))] p-6 text-center">
68
+ <div className="max-w-md space-y-3">
69
+ <div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-white shadow-sm">
70
+ <Navigation className="h-6 w-6 text-civic-primary" />
71
+ </div>
72
+ <h4 className="text-xl font-semibold text-civic-text">Map preview placeholder</h4>
73
+ <p className="text-sm leading-6 text-civic-muted">
74
+ This area will use Leaflet with OpenStreetMap tiles, current location detection, drag-to-adjust pin,
75
+ and reverse geocoding for address confirmation.
76
+ </p>
77
+ </div>
78
+ </div>
79
+
80
+ <div className="mt-5 flex flex-wrap gap-3">
81
+ <button
82
+ type="button"
83
+ className="inline-flex items-center gap-2 rounded-full bg-civic-primary px-5 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5"
84
+ >
85
+ <Crosshair className="h-4 w-4" />
86
+ Use current location
87
+ </button>
88
+ <button
89
+ type="button"
90
+ className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-civic-primary hover:text-civic-primary"
91
+ >
92
+ <MapPinned className="h-4 w-4" />
93
+ Adjust map pin
94
+ </button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <div className="space-y-6">
100
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
101
+ <div className="flex items-center gap-3">
102
+ <div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-civic-secondary/15 text-civic-secondary">
103
+ <Sparkles className="h-5 w-5" />
104
+ </div>
105
+ <div>
106
+ <p className="text-sm font-semibold uppercase tracking-[0.16em] text-civic-secondary">Stored data</p>
107
+ <h2 className="text-xl font-semibold text-civic-text">Location fields for the complaint</h2>
108
+ </div>
109
+ </div>
110
+
111
+ <div className="mt-5 grid gap-3">
112
+ {locationFields.map((field) => (
113
+ <div key={field} className="rounded-2xl bg-slate-50 px-4 py-3 text-sm font-medium text-civic-text">
114
+ {field}
115
+ </div>
116
+ ))}
117
+ </div>
118
+ </section>
119
+
120
+ <section className="rounded-[2rem] border border-amber-200 bg-amber-50 p-6 shadow-sm">
121
+ <div className="flex items-start gap-3">
122
+ <ShieldAlert className="mt-0.5 h-5 w-5 text-amber-700" />
123
+ <div>
124
+ <h2 className="text-lg font-semibold text-amber-950">Accuracy matters for emergency routing</h2>
125
+ <p className="mt-2 text-sm leading-6 text-amber-900/85">
126
+ A precise location helps responders find the incident faster, especially in flood, fire, collapse,
127
+ or live wire complaints.
128
+ </p>
129
+ </div>
130
+ </div>
131
+
132
+ <div className="mt-5 space-y-3">
133
+ {locationChecks.map((item) => (
134
+ <div key={item} className="rounded-2xl bg-white/70 px-4 py-3 text-sm leading-6 text-amber-950">
135
+ {item}
136
+ </div>
137
+ ))}
138
+ </div>
139
+ </section>
140
+ </div>
141
+ </section>
142
+
143
+ <section className="flex flex-wrap items-center justify-between gap-4 rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
144
+ <div>
145
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Next step</p>
146
+ <h2 className="mt-2 text-2xl font-semibold text-civic-text">Review complaint details before submission</h2>
147
+ <p className="mt-2 max-w-2xl text-sm leading-6 text-civic-muted">
148
+ After location is confirmed, the user should see a review screen with domain, evidence, location, and the
149
+ initial routing preview.
150
+ </p>
151
+ </div>
152
+
153
+ <div className="flex flex-wrap gap-3">
154
+ <Link
155
+ className="inline-flex rounded-full border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-civic-primary hover:text-civic-primary"
156
+ href="/report"
157
+ >
158
+ Back to report
159
+ </Link>
160
+ <Link
161
+ className="inline-flex rounded-full bg-civic-primary px-5 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5"
162
+ href="/report/review"
163
+ >
164
+ Continue to review
165
+ </Link>
166
+ </div>
167
+ </section>
168
+ </AppShell>
169
+ );
170
+ }
civic-platform/app/report/page.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AppShell } from "@/components/app-shell";
2
+ import { ReportForm } from "@/components/report-form";
3
+ import { requireRole } from "@/lib/auth";
4
+
5
+ export default async function ReportIssuePage() {
6
+ const user = await requireRole(["citizen"]);
7
+
8
+ return (
9
+ <AppShell>
10
+ <div className="mx-auto max-w-3xl space-y-8 pt-8">
11
+ <div className="text-center space-y-4">
12
+ <p className="text-sm font-bold uppercase tracking-[0.2em] text-civic-secondary">
13
+ Citizen Reporting
14
+ </p>
15
+ <h1 className="text-4xl font-extrabold tracking-tight text-slate-900 sm:text-5xl">
16
+ File a Report
17
+ </h1>
18
+ <p className="mx-auto max-w-xl text-lg text-slate-500">
19
+ Tell us what happened and where. We will get it to the right department.
20
+ </p>
21
+ </div>
22
+
23
+ <ReportForm citizenId={user.id} />
24
+ </div>
25
+ </AppShell>
26
+ );
27
+ }
civic-platform/app/report/review/page.tsx ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import { AlertTriangle, CheckCircle2, ChevronRight, ClipboardList, LocateFixed, ShieldAlert } from "lucide-react";
3
+ import { AppShell } from "@/components/app-shell";
4
+ import { FlowSteps } from "@/components/flow-steps";
5
+
6
+ const steps = ["Choose domain", "Add evidence", "Confirm location", "Review", "Submit"];
7
+
8
+ const reviewSections = [
9
+ {
10
+ icon: ClipboardList,
11
+ title: "Complaint summary",
12
+ items: [
13
+ "Selected domain: Water Supply, Sewerage, and Drainage",
14
+ "Likely sub-problem: Sewage overflow",
15
+ "Urgency hint: High public impact",
16
+ "Citizen note: Overflow near market entrance and bus stop",
17
+ ],
18
+ },
19
+ {
20
+ icon: LocateFixed,
21
+ title: "Location confirmation",
22
+ items: [
23
+ "Coordinates captured from device",
24
+ "Address resolved from map pin",
25
+ "Ward and routing zone ready",
26
+ "Nearby landmark noted for field staff",
27
+ ],
28
+ },
29
+ {
30
+ icon: CheckCircle2,
31
+ title: "Evidence attached",
32
+ items: [
33
+ "1 photo uploaded",
34
+ "0 video clips",
35
+ "Optional voice note available for later support",
36
+ "Media summary ready for department review",
37
+ ],
38
+ },
39
+ ];
40
+
41
+ const routingPreview = [
42
+ "Primary department: Water and Sewerage",
43
+ "Secondary support: Drainage unit if monsoon overflow is detected",
44
+ "Initial priority: P2 High",
45
+ "Notification path: citizen confirmation after submission",
46
+ ];
47
+
48
+ const submitChecks = [
49
+ "Location is correct",
50
+ "Complaint is within city jurisdiction",
51
+ "Media is safe and relevant",
52
+ "Description is clear enough for field response",
53
+ ];
54
+
55
+ export default function ReportReviewPage() {
56
+ return (
57
+ <AppShell>
58
+ <section className="space-y-6">
59
+ <div className="space-y-3">
60
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Report flow</p>
61
+ <h1 className="text-4xl font-semibold tracking-tight text-civic-text">Review before final submission</h1>
62
+ <p className="max-w-3xl text-sm leading-7 text-civic-muted">
63
+ Check the issue details, location, and evidence once more before sending the complaint to the city team.
64
+ </p>
65
+ </div>
66
+
67
+ <FlowSteps currentStep={4} steps={steps} />
68
+ </section>
69
+
70
+ <section className="grid gap-6 lg:grid-cols-[1.12fr_0.88fr]">
71
+ <div className="space-y-6">
72
+ {reviewSections.map(({ icon: Icon, title, items }) => (
73
+ <section key={title} className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
74
+ <div className="flex items-center gap-3">
75
+ <div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-civic-secondary/15 text-civic-secondary">
76
+ <Icon className="h-5 w-5" />
77
+ </div>
78
+ <div>
79
+ <p className="text-sm font-semibold uppercase tracking-[0.16em] text-civic-secondary">Summary</p>
80
+ <h2 className="text-xl font-semibold text-civic-text">{title}</h2>
81
+ </div>
82
+ </div>
83
+
84
+ <div className="mt-5 grid gap-3">
85
+ {items.map((item) => (
86
+ <div key={item} className="rounded-2xl bg-slate-50 px-4 py-3 text-sm leading-6 text-civic-text">
87
+ {item}
88
+ </div>
89
+ ))}
90
+ </div>
91
+ </section>
92
+ ))}
93
+ </div>
94
+
95
+ <div className="space-y-6">
96
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
97
+ <div className="flex items-center gap-3">
98
+ <div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-civic-primary text-white">
99
+ <ShieldAlert className="h-5 w-5" />
100
+ </div>
101
+ <div>
102
+ <p className="text-sm font-semibold uppercase tracking-[0.16em] text-civic-secondary">Routing preview</p>
103
+ <h2 className="text-xl font-semibold text-civic-text">What the system will do next</h2>
104
+ </div>
105
+ </div>
106
+
107
+ <div className="mt-5 space-y-3">
108
+ {routingPreview.map((item) => (
109
+ <div key={item} className="rounded-2xl bg-slate-50 px-4 py-3 text-sm leading-6 text-civic-text">
110
+ {item}
111
+ </div>
112
+ ))}
113
+ </div>
114
+ </section>
115
+
116
+ <section className="rounded-[2rem] border border-amber-200 bg-amber-50 p-6 shadow-sm">
117
+ <div className="flex items-start gap-3">
118
+ <AlertTriangle className="mt-0.5 h-5 w-5 text-amber-700" />
119
+ <div>
120
+ <h2 className="text-lg font-semibold text-amber-950">Final checks before submission</h2>
121
+ <p className="mt-2 text-sm leading-6 text-amber-900/85">
122
+ A quick final check helps the city team act faster and reduces follow-up questions.
123
+ </p>
124
+ </div>
125
+ </div>
126
+
127
+ <div className="mt-5 space-y-3">
128
+ {submitChecks.map((item) => (
129
+ <div key={item} className="rounded-2xl bg-white/75 px-4 py-3 text-sm font-medium text-amber-950">
130
+ {item}
131
+ </div>
132
+ ))}
133
+ </div>
134
+ </section>
135
+ </div>
136
+ </section>
137
+
138
+ <section className="flex flex-wrap items-center justify-between gap-4 rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
139
+ <div>
140
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Submit complaint</p>
141
+ <h2 className="mt-2 text-2xl font-semibold text-civic-text">Ready to create the complaint record</h2>
142
+ <p className="mt-2 max-w-2xl text-sm leading-6 text-civic-muted">
143
+ Once submitted, your complaint will receive an ID and move into review, routing, and department assignment.
144
+ </p>
145
+ </div>
146
+
147
+ <div className="flex flex-wrap gap-3">
148
+ <Link
149
+ className="inline-flex rounded-full border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-civic-primary hover:text-civic-primary"
150
+ href="/report/location"
151
+ >
152
+ Back to location
153
+ </Link>
154
+ <Link
155
+ className="inline-flex items-center gap-2 rounded-full bg-civic-primary px-5 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5"
156
+ href="/report/success"
157
+ >
158
+ Submit complaint
159
+ <ChevronRight className="h-4 w-4" />
160
+ </Link>
161
+ </div>
162
+ </section>
163
+ </AppShell>
164
+ );
165
+ }
civic-platform/app/report/success/page.tsx ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import { ArrowRight, CheckCircle2, Clock3, Hash, MapPinned, ShieldCheck } from "lucide-react";
3
+ import { AppShell } from "@/components/app-shell";
4
+ import { FlowSteps } from "@/components/flow-steps";
5
+
6
+ const steps = ["Choose domain", "Add evidence", "Confirm location", "Review", "Submit"];
7
+
8
+ const nextActions = [
9
+ "Your report is stored with a complaint ID for tracking.",
10
+ "The first visible status is Submitted.",
11
+ "Routing and priority decide which department picks it up next.",
12
+ "You will see updates when the complaint is assigned, worked on, or resolved.",
13
+ ];
14
+
15
+ const summaryCards = [
16
+ {
17
+ icon: Hash,
18
+ label: "Complaint ID",
19
+ value: "CP-2026-00124",
20
+ },
21
+ {
22
+ icon: Clock3,
23
+ label: "Current status",
24
+ value: "Submitted",
25
+ },
26
+ {
27
+ icon: MapPinned,
28
+ label: "Location state",
29
+ value: "Geo-tag confirmed",
30
+ },
31
+ ];
32
+
33
+ type ReportSuccessPageProps = {
34
+ searchParams?: Promise<{
35
+ id?: string;
36
+ code?: string;
37
+ media?: string;
38
+ }>;
39
+ };
40
+
41
+ export default async function ReportSuccessPage({ searchParams }: ReportSuccessPageProps) {
42
+ const resolvedSearchParams = searchParams ? await searchParams : undefined;
43
+ const complaintId = resolvedSearchParams?.id ?? "sample-complaint";
44
+ const complaintCode = resolvedSearchParams?.code ?? "CP-2026-00124";
45
+ const mediaStatus = resolvedSearchParams?.media ?? "complete";
46
+
47
+ return (
48
+ <AppShell>
49
+ <section className="space-y-6">
50
+ <div className="space-y-3">
51
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Report flow</p>
52
+ <h1 className="text-4xl font-semibold tracking-tight text-civic-text">Complaint submitted successfully</h1>
53
+ <p className="max-w-3xl text-sm leading-7 text-civic-muted">
54
+ Your complaint has been recorded. Keep the complaint ID handy and use the tracking page for updates.
55
+ </p>
56
+ </div>
57
+
58
+ <FlowSteps currentStep={5} steps={steps} />
59
+ </section>
60
+
61
+ <section className="rounded-[2rem] bg-civic-primary px-6 py-8 text-white shadow-civic lg:px-8">
62
+ <div className="grid gap-8 lg:grid-cols-[1.15fr_0.85fr]">
63
+ <div className="space-y-5">
64
+ <div className="flex h-16 w-16 items-center justify-center rounded-3xl bg-white text-civic-primary">
65
+ <CheckCircle2 className="h-8 w-8" />
66
+ </div>
67
+ <div className="space-y-3">
68
+ <h2 className="text-3xl font-semibold tracking-tight">Your complaint is now in the system.</h2>
69
+ <p className="max-w-2xl text-sm leading-7 text-white/80">
70
+ It will now move through review, routing, assignment, and resolution. You can track progress from your
71
+ complaints page at any time.
72
+ </p>
73
+ {mediaStatus === "partial" ? (
74
+ <p className="rounded-2xl bg-white/10 px-4 py-3 text-sm text-white/90">
75
+ Your complaint was submitted, but one or more media files could not be uploaded.
76
+ </p>
77
+ ) : null}
78
+ </div>
79
+ <div className="flex flex-wrap gap-3">
80
+ <Link
81
+ className="inline-flex items-center gap-2 rounded-full bg-white px-5 py-3 text-sm font-semibold text-civic-primary transition hover:-translate-y-0.5"
82
+ href="/complaints"
83
+ >
84
+ View my complaints
85
+ <ArrowRight className="h-4 w-4" />
86
+ </Link>
87
+ <Link
88
+ className="inline-flex items-center gap-2 rounded-full border border-white/30 px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/10"
89
+ href={`/complaints/${complaintId}`}
90
+ >
91
+ Open complaint detail
92
+ <ArrowRight className="h-4 w-4" />
93
+ </Link>
94
+ </div>
95
+ </div>
96
+
97
+ <div className="grid gap-4">
98
+ {summaryCards.map(({ icon: Icon, label, value }) => (
99
+ <div key={label} className="rounded-3xl border border-white/15 bg-white/10 p-5 backdrop-blur">
100
+ <Icon className="mb-4 h-6 w-6 text-civic-accent" />
101
+ <p className="text-sm font-semibold uppercase tracking-[0.16em] text-white/75">{label}</p>
102
+ <p className="mt-2 text-xl font-semibold text-white">
103
+ {label === "Complaint ID" ? complaintCode : value}
104
+ </p>
105
+ </div>
106
+ ))}
107
+ </div>
108
+ </div>
109
+ </section>
110
+
111
+ <section className="grid gap-6 lg:grid-cols-[1fr_1fr]">
112
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
113
+ <div className="flex items-center gap-3">
114
+ <div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-civic-secondary/15 text-civic-secondary">
115
+ <ShieldCheck className="h-5 w-5" />
116
+ </div>
117
+ <div>
118
+ <p className="text-sm font-semibold uppercase tracking-[0.16em] text-civic-secondary">What happens next</p>
119
+ <h2 className="text-xl font-semibold text-civic-text">Post-submission workflow</h2>
120
+ </div>
121
+ </div>
122
+
123
+ <div className="mt-5 space-y-3">
124
+ {nextActions.map((item) => (
125
+ <div key={item} className="rounded-2xl bg-slate-50 px-4 py-3 text-sm leading-6 text-civic-text">
126
+ {item}
127
+ </div>
128
+ ))}
129
+ </div>
130
+ </section>
131
+
132
+ <section className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
133
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Helpful shortcuts</p>
134
+ <h2 className="mt-2 text-2xl font-semibold text-civic-text">What you can do from here</h2>
135
+ <p className="mt-3 text-sm leading-7 text-civic-muted">
136
+ You can report another issue, open the complaint detail page, or switch to emergency reporting if the
137
+ situation has become urgent.
138
+ </p>
139
+
140
+ <div className="mt-6 grid gap-3 sm:grid-cols-2">
141
+ {[
142
+ "Complaint ID",
143
+ "Current status",
144
+ "Expected next step",
145
+ "Tracking shortcut",
146
+ "Submit another issue",
147
+ "Emergency reporting shortcut",
148
+ ].map((item) => (
149
+ <div key={item} className="rounded-2xl border border-slate-200 p-4 text-sm font-medium text-civic-text">
150
+ {item}
151
+ </div>
152
+ ))}
153
+ </div>
154
+
155
+ <div className="mt-6 flex flex-wrap gap-3">
156
+ <Link
157
+ className="inline-flex rounded-full border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-civic-primary hover:text-civic-primary"
158
+ href="/report"
159
+ >
160
+ Submit another issue
161
+ </Link>
162
+ <Link
163
+ className="inline-flex rounded-full bg-civic-danger px-5 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5"
164
+ href="/emergency"
165
+ >
166
+ Emergency reporting
167
+ </Link>
168
+ </div>
169
+ </section>
170
+ </section>
171
+ </AppShell>
172
+ );
173
+ }
civic-platform/app/tasks/page.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AppShell } from "@/components/app-shell";
2
+ import { FieldTaskBoard } from "@/components/field-task-board";
3
+ import { LiveSyncBadge } from "@/components/live-sync-badge";
4
+ import { requireRole } from "@/lib/auth";
5
+ import { getOfficerAssignments, type ComplaintAssignmentItem } from "@/lib/api";
6
+
7
+ const fallbackAssignments: ComplaintAssignmentItem[] = [];
8
+
9
+ export default async function TasksPage() {
10
+ const user = await requireRole(["field_officer"]);
11
+ let assignments = fallbackAssignments;
12
+
13
+ try {
14
+ assignments = await getOfficerAssignments(user.id);
15
+ } catch {
16
+ assignments = fallbackAssignments;
17
+ }
18
+
19
+ return (
20
+ <AppShell>
21
+ <section className="space-y-3">
22
+ <div className="flex flex-wrap items-center justify-between gap-3">
23
+ <p className="text-sm font-semibold uppercase tracking-[0.22em] text-civic-secondary">Field operations</p>
24
+ <LiveSyncBadge label="Task board live refresh" />
25
+ </div>
26
+ <h1 className="text-4xl font-semibold tracking-tight text-civic-text">Work through assigned on-ground tasks.</h1>
27
+ <p className="max-w-3xl text-sm leading-7 text-civic-muted">
28
+ Accept work, start field action, and mark completion so the complaint lifecycle stays accurate for citizens and operators.
29
+ </p>
30
+ </section>
31
+
32
+ <FieldTaskBoard assignments={assignments} officerId={user.id} />
33
+ </AppShell>
34
+ );
35
+ }
civic-platform/app/template.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+
5
+ export default function Template({ children }: { children: React.ReactNode }) {
6
+ return (
7
+ <motion.div
8
+ initial={{ opacity: 0, y: 15 }}
9
+ animate={{ opacity: 1, y: 0 }}
10
+ exit={{ opacity: 0, y: -15 }}
11
+ transition={{ ease: "circOut", duration: 0.4 }}
12
+ >
13
+ {children}
14
+ </motion.div>
15
+ );
16
+ }
civic-platform/backend/.env.example ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PORT=4000
2
+ NODE_ENV=development
3
+ DB_HOST=localhost
4
+ DB_PORT=5432
5
+ DB_NAME=civicpulse
6
+ DB_USER=postgres
7
+ DB_PASSWORD=your_password_here
8
+ DB_SSL=false
9
+ DEMO_AUTH_PASSWORD=civicpulse123
10
+ CORS_ORIGINS=http://localhost:3000
11
+ RATE_LIMIT_WINDOW_MS=60000
12
+ RATE_LIMIT_MAX_REQUESTS=120
civic-platform/backend/README.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Backend
2
+
3
+ Node.js and TypeScript backend for the CivicPulse project.
4
+
5
+ ## Quick start
6
+
7
+ 1. Copy `.env.example` to `.env`
8
+ 2. Set your PostgreSQL connection values
9
+ 3. Create the `civicpulse` database
10
+ 4. Apply `sql/001_initial_schema.sql`
11
+ 5. Apply `sql/002_seed_core_data.sql`
12
+ 6. Run `npm install`
13
+ 7. Run `npm run dev`
14
+
15
+ Alternative local setup:
16
+
17
+ - Run `npm run db:init` to apply both SQL files using the configured PostgreSQL connection
18
+
19
+ ## Main folders
20
+
21
+ - `src/config` environment config
22
+ - `src/db` PostgreSQL connection
23
+ - `src/routes` shared routes
24
+ - `src/modules/complaints` complaint APIs
25
+ - `sql` database schema files
26
+
27
+ ## First routes
28
+
29
+ - `GET /api/health`
30
+ - `GET /api/departments`
31
+ - `GET /api/domains`
32
+ - `GET /api/complaints`
33
+ - `GET /api/complaints/citizen/:citizenId`
34
+ - `GET /api/complaints/:id`
35
+ - `GET /api/complaints/:id/history`
36
+ - `POST /api/complaints`
37
+ - `PATCH /api/complaints/:id/status`
38
+ - `POST /api/complaints/:id/assign`
civic-platform/backend/backend-dev.log ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+
2
+ > civicpulse-backend@0.1.0 dev
3
+ > tsx watch src/server.ts
4
+
5
+ CivicPulse backend listening on port 4000
civic-platform/backend/backend-run.log ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ CivicPulse backend listening on port 4000
2
+ ^C
civic-platform/backend/backend.crash.log ADDED
Binary file (88 Bytes). View file
 
civic-platform/backend/backend.detached.log ADDED
@@ -0,0 +1 @@
 
 
1
+ CivicPulse backend listening on port 4000
civic-platform/backend/backend.dev.log ADDED
Binary file (88 Bytes). View file
 
civic-platform/backend/package-lock.json ADDED
@@ -0,0 +1,1817 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "civicpulse-backend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "civicpulse-backend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "cors": "^2.8.5",
12
+ "dotenv": "^16.4.7",
13
+ "express": "^4.21.2",
14
+ "multer": "^2.1.1",
15
+ "pg": "^8.13.3"
16
+ },
17
+ "devDependencies": {
18
+ "@types/cors": "^2.8.17",
19
+ "@types/express": "^5.0.1",
20
+ "@types/multer": "^2.1.0",
21
+ "@types/node": "^22.13.10",
22
+ "@types/pg": "^8.11.11",
23
+ "tsx": "^4.19.3",
24
+ "typescript": "^5.8.2"
25
+ }
26
+ },
27
+ "node_modules/@esbuild/aix-ppc64": {
28
+ "version": "0.27.4",
29
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
30
+ "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
31
+ "cpu": [
32
+ "ppc64"
33
+ ],
34
+ "dev": true,
35
+ "license": "MIT",
36
+ "optional": true,
37
+ "os": [
38
+ "aix"
39
+ ],
40
+ "engines": {
41
+ "node": ">=18"
42
+ }
43
+ },
44
+ "node_modules/@esbuild/android-arm": {
45
+ "version": "0.27.4",
46
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
47
+ "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
48
+ "cpu": [
49
+ "arm"
50
+ ],
51
+ "dev": true,
52
+ "license": "MIT",
53
+ "optional": true,
54
+ "os": [
55
+ "android"
56
+ ],
57
+ "engines": {
58
+ "node": ">=18"
59
+ }
60
+ },
61
+ "node_modules/@esbuild/android-arm64": {
62
+ "version": "0.27.4",
63
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
64
+ "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
65
+ "cpu": [
66
+ "arm64"
67
+ ],
68
+ "dev": true,
69
+ "license": "MIT",
70
+ "optional": true,
71
+ "os": [
72
+ "android"
73
+ ],
74
+ "engines": {
75
+ "node": ">=18"
76
+ }
77
+ },
78
+ "node_modules/@esbuild/android-x64": {
79
+ "version": "0.27.4",
80
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
81
+ "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
82
+ "cpu": [
83
+ "x64"
84
+ ],
85
+ "dev": true,
86
+ "license": "MIT",
87
+ "optional": true,
88
+ "os": [
89
+ "android"
90
+ ],
91
+ "engines": {
92
+ "node": ">=18"
93
+ }
94
+ },
95
+ "node_modules/@esbuild/darwin-arm64": {
96
+ "version": "0.27.4",
97
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
98
+ "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
99
+ "cpu": [
100
+ "arm64"
101
+ ],
102
+ "dev": true,
103
+ "license": "MIT",
104
+ "optional": true,
105
+ "os": [
106
+ "darwin"
107
+ ],
108
+ "engines": {
109
+ "node": ">=18"
110
+ }
111
+ },
112
+ "node_modules/@esbuild/darwin-x64": {
113
+ "version": "0.27.4",
114
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
115
+ "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
116
+ "cpu": [
117
+ "x64"
118
+ ],
119
+ "dev": true,
120
+ "license": "MIT",
121
+ "optional": true,
122
+ "os": [
123
+ "darwin"
124
+ ],
125
+ "engines": {
126
+ "node": ">=18"
127
+ }
128
+ },
129
+ "node_modules/@esbuild/freebsd-arm64": {
130
+ "version": "0.27.4",
131
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
132
+ "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
133
+ "cpu": [
134
+ "arm64"
135
+ ],
136
+ "dev": true,
137
+ "license": "MIT",
138
+ "optional": true,
139
+ "os": [
140
+ "freebsd"
141
+ ],
142
+ "engines": {
143
+ "node": ">=18"
144
+ }
145
+ },
146
+ "node_modules/@esbuild/freebsd-x64": {
147
+ "version": "0.27.4",
148
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
149
+ "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
150
+ "cpu": [
151
+ "x64"
152
+ ],
153
+ "dev": true,
154
+ "license": "MIT",
155
+ "optional": true,
156
+ "os": [
157
+ "freebsd"
158
+ ],
159
+ "engines": {
160
+ "node": ">=18"
161
+ }
162
+ },
163
+ "node_modules/@esbuild/linux-arm": {
164
+ "version": "0.27.4",
165
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
166
+ "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
167
+ "cpu": [
168
+ "arm"
169
+ ],
170
+ "dev": true,
171
+ "license": "MIT",
172
+ "optional": true,
173
+ "os": [
174
+ "linux"
175
+ ],
176
+ "engines": {
177
+ "node": ">=18"
178
+ }
179
+ },
180
+ "node_modules/@esbuild/linux-arm64": {
181
+ "version": "0.27.4",
182
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
183
+ "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
184
+ "cpu": [
185
+ "arm64"
186
+ ],
187
+ "dev": true,
188
+ "license": "MIT",
189
+ "optional": true,
190
+ "os": [
191
+ "linux"
192
+ ],
193
+ "engines": {
194
+ "node": ">=18"
195
+ }
196
+ },
197
+ "node_modules/@esbuild/linux-ia32": {
198
+ "version": "0.27.4",
199
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
200
+ "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
201
+ "cpu": [
202
+ "ia32"
203
+ ],
204
+ "dev": true,
205
+ "license": "MIT",
206
+ "optional": true,
207
+ "os": [
208
+ "linux"
209
+ ],
210
+ "engines": {
211
+ "node": ">=18"
212
+ }
213
+ },
214
+ "node_modules/@esbuild/linux-loong64": {
215
+ "version": "0.27.4",
216
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
217
+ "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
218
+ "cpu": [
219
+ "loong64"
220
+ ],
221
+ "dev": true,
222
+ "license": "MIT",
223
+ "optional": true,
224
+ "os": [
225
+ "linux"
226
+ ],
227
+ "engines": {
228
+ "node": ">=18"
229
+ }
230
+ },
231
+ "node_modules/@esbuild/linux-mips64el": {
232
+ "version": "0.27.4",
233
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
234
+ "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
235
+ "cpu": [
236
+ "mips64el"
237
+ ],
238
+ "dev": true,
239
+ "license": "MIT",
240
+ "optional": true,
241
+ "os": [
242
+ "linux"
243
+ ],
244
+ "engines": {
245
+ "node": ">=18"
246
+ }
247
+ },
248
+ "node_modules/@esbuild/linux-ppc64": {
249
+ "version": "0.27.4",
250
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
251
+ "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
252
+ "cpu": [
253
+ "ppc64"
254
+ ],
255
+ "dev": true,
256
+ "license": "MIT",
257
+ "optional": true,
258
+ "os": [
259
+ "linux"
260
+ ],
261
+ "engines": {
262
+ "node": ">=18"
263
+ }
264
+ },
265
+ "node_modules/@esbuild/linux-riscv64": {
266
+ "version": "0.27.4",
267
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
268
+ "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
269
+ "cpu": [
270
+ "riscv64"
271
+ ],
272
+ "dev": true,
273
+ "license": "MIT",
274
+ "optional": true,
275
+ "os": [
276
+ "linux"
277
+ ],
278
+ "engines": {
279
+ "node": ">=18"
280
+ }
281
+ },
282
+ "node_modules/@esbuild/linux-s390x": {
283
+ "version": "0.27.4",
284
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
285
+ "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
286
+ "cpu": [
287
+ "s390x"
288
+ ],
289
+ "dev": true,
290
+ "license": "MIT",
291
+ "optional": true,
292
+ "os": [
293
+ "linux"
294
+ ],
295
+ "engines": {
296
+ "node": ">=18"
297
+ }
298
+ },
299
+ "node_modules/@esbuild/linux-x64": {
300
+ "version": "0.27.4",
301
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
302
+ "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
303
+ "cpu": [
304
+ "x64"
305
+ ],
306
+ "dev": true,
307
+ "license": "MIT",
308
+ "optional": true,
309
+ "os": [
310
+ "linux"
311
+ ],
312
+ "engines": {
313
+ "node": ">=18"
314
+ }
315
+ },
316
+ "node_modules/@esbuild/netbsd-arm64": {
317
+ "version": "0.27.4",
318
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
319
+ "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
320
+ "cpu": [
321
+ "arm64"
322
+ ],
323
+ "dev": true,
324
+ "license": "MIT",
325
+ "optional": true,
326
+ "os": [
327
+ "netbsd"
328
+ ],
329
+ "engines": {
330
+ "node": ">=18"
331
+ }
332
+ },
333
+ "node_modules/@esbuild/netbsd-x64": {
334
+ "version": "0.27.4",
335
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
336
+ "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
337
+ "cpu": [
338
+ "x64"
339
+ ],
340
+ "dev": true,
341
+ "license": "MIT",
342
+ "optional": true,
343
+ "os": [
344
+ "netbsd"
345
+ ],
346
+ "engines": {
347
+ "node": ">=18"
348
+ }
349
+ },
350
+ "node_modules/@esbuild/openbsd-arm64": {
351
+ "version": "0.27.4",
352
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
353
+ "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
354
+ "cpu": [
355
+ "arm64"
356
+ ],
357
+ "dev": true,
358
+ "license": "MIT",
359
+ "optional": true,
360
+ "os": [
361
+ "openbsd"
362
+ ],
363
+ "engines": {
364
+ "node": ">=18"
365
+ }
366
+ },
367
+ "node_modules/@esbuild/openbsd-x64": {
368
+ "version": "0.27.4",
369
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
370
+ "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
371
+ "cpu": [
372
+ "x64"
373
+ ],
374
+ "dev": true,
375
+ "license": "MIT",
376
+ "optional": true,
377
+ "os": [
378
+ "openbsd"
379
+ ],
380
+ "engines": {
381
+ "node": ">=18"
382
+ }
383
+ },
384
+ "node_modules/@esbuild/openharmony-arm64": {
385
+ "version": "0.27.4",
386
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
387
+ "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
388
+ "cpu": [
389
+ "arm64"
390
+ ],
391
+ "dev": true,
392
+ "license": "MIT",
393
+ "optional": true,
394
+ "os": [
395
+ "openharmony"
396
+ ],
397
+ "engines": {
398
+ "node": ">=18"
399
+ }
400
+ },
401
+ "node_modules/@esbuild/sunos-x64": {
402
+ "version": "0.27.4",
403
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
404
+ "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
405
+ "cpu": [
406
+ "x64"
407
+ ],
408
+ "dev": true,
409
+ "license": "MIT",
410
+ "optional": true,
411
+ "os": [
412
+ "sunos"
413
+ ],
414
+ "engines": {
415
+ "node": ">=18"
416
+ }
417
+ },
418
+ "node_modules/@esbuild/win32-arm64": {
419
+ "version": "0.27.4",
420
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
421
+ "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
422
+ "cpu": [
423
+ "arm64"
424
+ ],
425
+ "dev": true,
426
+ "license": "MIT",
427
+ "optional": true,
428
+ "os": [
429
+ "win32"
430
+ ],
431
+ "engines": {
432
+ "node": ">=18"
433
+ }
434
+ },
435
+ "node_modules/@esbuild/win32-ia32": {
436
+ "version": "0.27.4",
437
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
438
+ "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
439
+ "cpu": [
440
+ "ia32"
441
+ ],
442
+ "dev": true,
443
+ "license": "MIT",
444
+ "optional": true,
445
+ "os": [
446
+ "win32"
447
+ ],
448
+ "engines": {
449
+ "node": ">=18"
450
+ }
451
+ },
452
+ "node_modules/@esbuild/win32-x64": {
453
+ "version": "0.27.4",
454
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
455
+ "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
456
+ "cpu": [
457
+ "x64"
458
+ ],
459
+ "dev": true,
460
+ "license": "MIT",
461
+ "optional": true,
462
+ "os": [
463
+ "win32"
464
+ ],
465
+ "engines": {
466
+ "node": ">=18"
467
+ }
468
+ },
469
+ "node_modules/@types/body-parser": {
470
+ "version": "1.19.6",
471
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
472
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
473
+ "dev": true,
474
+ "license": "MIT",
475
+ "dependencies": {
476
+ "@types/connect": "*",
477
+ "@types/node": "*"
478
+ }
479
+ },
480
+ "node_modules/@types/connect": {
481
+ "version": "3.4.38",
482
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
483
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
484
+ "dev": true,
485
+ "license": "MIT",
486
+ "dependencies": {
487
+ "@types/node": "*"
488
+ }
489
+ },
490
+ "node_modules/@types/cors": {
491
+ "version": "2.8.19",
492
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
493
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
494
+ "dev": true,
495
+ "license": "MIT",
496
+ "dependencies": {
497
+ "@types/node": "*"
498
+ }
499
+ },
500
+ "node_modules/@types/express": {
501
+ "version": "5.0.6",
502
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
503
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
504
+ "dev": true,
505
+ "license": "MIT",
506
+ "dependencies": {
507
+ "@types/body-parser": "*",
508
+ "@types/express-serve-static-core": "^5.0.0",
509
+ "@types/serve-static": "^2"
510
+ }
511
+ },
512
+ "node_modules/@types/express-serve-static-core": {
513
+ "version": "5.1.1",
514
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
515
+ "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
516
+ "dev": true,
517
+ "license": "MIT",
518
+ "dependencies": {
519
+ "@types/node": "*",
520
+ "@types/qs": "*",
521
+ "@types/range-parser": "*",
522
+ "@types/send": "*"
523
+ }
524
+ },
525
+ "node_modules/@types/http-errors": {
526
+ "version": "2.0.5",
527
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
528
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
529
+ "dev": true,
530
+ "license": "MIT"
531
+ },
532
+ "node_modules/@types/multer": {
533
+ "version": "2.1.0",
534
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz",
535
+ "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==",
536
+ "dev": true,
537
+ "license": "MIT",
538
+ "dependencies": {
539
+ "@types/express": "*"
540
+ }
541
+ },
542
+ "node_modules/@types/node": {
543
+ "version": "22.19.15",
544
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
545
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
546
+ "dev": true,
547
+ "license": "MIT",
548
+ "dependencies": {
549
+ "undici-types": "~6.21.0"
550
+ }
551
+ },
552
+ "node_modules/@types/pg": {
553
+ "version": "8.20.0",
554
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
555
+ "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
556
+ "dev": true,
557
+ "license": "MIT",
558
+ "dependencies": {
559
+ "@types/node": "*",
560
+ "pg-protocol": "*",
561
+ "pg-types": "^2.2.0"
562
+ }
563
+ },
564
+ "node_modules/@types/qs": {
565
+ "version": "6.15.0",
566
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
567
+ "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
568
+ "dev": true,
569
+ "license": "MIT"
570
+ },
571
+ "node_modules/@types/range-parser": {
572
+ "version": "1.2.7",
573
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
574
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
575
+ "dev": true,
576
+ "license": "MIT"
577
+ },
578
+ "node_modules/@types/send": {
579
+ "version": "1.2.1",
580
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
581
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
582
+ "dev": true,
583
+ "license": "MIT",
584
+ "dependencies": {
585
+ "@types/node": "*"
586
+ }
587
+ },
588
+ "node_modules/@types/serve-static": {
589
+ "version": "2.2.0",
590
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
591
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
592
+ "dev": true,
593
+ "license": "MIT",
594
+ "dependencies": {
595
+ "@types/http-errors": "*",
596
+ "@types/node": "*"
597
+ }
598
+ },
599
+ "node_modules/accepts": {
600
+ "version": "1.3.8",
601
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
602
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
603
+ "license": "MIT",
604
+ "dependencies": {
605
+ "mime-types": "~2.1.34",
606
+ "negotiator": "0.6.3"
607
+ },
608
+ "engines": {
609
+ "node": ">= 0.6"
610
+ }
611
+ },
612
+ "node_modules/append-field": {
613
+ "version": "1.0.0",
614
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
615
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
616
+ "license": "MIT"
617
+ },
618
+ "node_modules/array-flatten": {
619
+ "version": "1.1.1",
620
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
621
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
622
+ "license": "MIT"
623
+ },
624
+ "node_modules/body-parser": {
625
+ "version": "1.20.4",
626
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
627
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
628
+ "license": "MIT",
629
+ "dependencies": {
630
+ "bytes": "~3.1.2",
631
+ "content-type": "~1.0.5",
632
+ "debug": "2.6.9",
633
+ "depd": "2.0.0",
634
+ "destroy": "~1.2.0",
635
+ "http-errors": "~2.0.1",
636
+ "iconv-lite": "~0.4.24",
637
+ "on-finished": "~2.4.1",
638
+ "qs": "~6.14.0",
639
+ "raw-body": "~2.5.3",
640
+ "type-is": "~1.6.18",
641
+ "unpipe": "~1.0.0"
642
+ },
643
+ "engines": {
644
+ "node": ">= 0.8",
645
+ "npm": "1.2.8000 || >= 1.4.16"
646
+ }
647
+ },
648
+ "node_modules/buffer-from": {
649
+ "version": "1.1.2",
650
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
651
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
652
+ "license": "MIT"
653
+ },
654
+ "node_modules/busboy": {
655
+ "version": "1.6.0",
656
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
657
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
658
+ "dependencies": {
659
+ "streamsearch": "^1.1.0"
660
+ },
661
+ "engines": {
662
+ "node": ">=10.16.0"
663
+ }
664
+ },
665
+ "node_modules/bytes": {
666
+ "version": "3.1.2",
667
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
668
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
669
+ "license": "MIT",
670
+ "engines": {
671
+ "node": ">= 0.8"
672
+ }
673
+ },
674
+ "node_modules/call-bind-apply-helpers": {
675
+ "version": "1.0.2",
676
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
677
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
678
+ "license": "MIT",
679
+ "dependencies": {
680
+ "es-errors": "^1.3.0",
681
+ "function-bind": "^1.1.2"
682
+ },
683
+ "engines": {
684
+ "node": ">= 0.4"
685
+ }
686
+ },
687
+ "node_modules/call-bound": {
688
+ "version": "1.0.4",
689
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
690
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
691
+ "license": "MIT",
692
+ "dependencies": {
693
+ "call-bind-apply-helpers": "^1.0.2",
694
+ "get-intrinsic": "^1.3.0"
695
+ },
696
+ "engines": {
697
+ "node": ">= 0.4"
698
+ },
699
+ "funding": {
700
+ "url": "https://github.com/sponsors/ljharb"
701
+ }
702
+ },
703
+ "node_modules/concat-stream": {
704
+ "version": "2.0.0",
705
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
706
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
707
+ "engines": [
708
+ "node >= 6.0"
709
+ ],
710
+ "license": "MIT",
711
+ "dependencies": {
712
+ "buffer-from": "^1.0.0",
713
+ "inherits": "^2.0.3",
714
+ "readable-stream": "^3.0.2",
715
+ "typedarray": "^0.0.6"
716
+ }
717
+ },
718
+ "node_modules/content-disposition": {
719
+ "version": "0.5.4",
720
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
721
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
722
+ "license": "MIT",
723
+ "dependencies": {
724
+ "safe-buffer": "5.2.1"
725
+ },
726
+ "engines": {
727
+ "node": ">= 0.6"
728
+ }
729
+ },
730
+ "node_modules/content-type": {
731
+ "version": "1.0.5",
732
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
733
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
734
+ "license": "MIT",
735
+ "engines": {
736
+ "node": ">= 0.6"
737
+ }
738
+ },
739
+ "node_modules/cookie": {
740
+ "version": "0.7.2",
741
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
742
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
743
+ "license": "MIT",
744
+ "engines": {
745
+ "node": ">= 0.6"
746
+ }
747
+ },
748
+ "node_modules/cookie-signature": {
749
+ "version": "1.0.7",
750
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
751
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
752
+ "license": "MIT"
753
+ },
754
+ "node_modules/cors": {
755
+ "version": "2.8.6",
756
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
757
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
758
+ "license": "MIT",
759
+ "dependencies": {
760
+ "object-assign": "^4",
761
+ "vary": "^1"
762
+ },
763
+ "engines": {
764
+ "node": ">= 0.10"
765
+ },
766
+ "funding": {
767
+ "type": "opencollective",
768
+ "url": "https://opencollective.com/express"
769
+ }
770
+ },
771
+ "node_modules/debug": {
772
+ "version": "2.6.9",
773
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
774
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
775
+ "license": "MIT",
776
+ "dependencies": {
777
+ "ms": "2.0.0"
778
+ }
779
+ },
780
+ "node_modules/depd": {
781
+ "version": "2.0.0",
782
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
783
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
784
+ "license": "MIT",
785
+ "engines": {
786
+ "node": ">= 0.8"
787
+ }
788
+ },
789
+ "node_modules/destroy": {
790
+ "version": "1.2.0",
791
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
792
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
793
+ "license": "MIT",
794
+ "engines": {
795
+ "node": ">= 0.8",
796
+ "npm": "1.2.8000 || >= 1.4.16"
797
+ }
798
+ },
799
+ "node_modules/dotenv": {
800
+ "version": "16.6.1",
801
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
802
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
803
+ "license": "BSD-2-Clause",
804
+ "engines": {
805
+ "node": ">=12"
806
+ },
807
+ "funding": {
808
+ "url": "https://dotenvx.com"
809
+ }
810
+ },
811
+ "node_modules/dunder-proto": {
812
+ "version": "1.0.1",
813
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
814
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
815
+ "license": "MIT",
816
+ "dependencies": {
817
+ "call-bind-apply-helpers": "^1.0.1",
818
+ "es-errors": "^1.3.0",
819
+ "gopd": "^1.2.0"
820
+ },
821
+ "engines": {
822
+ "node": ">= 0.4"
823
+ }
824
+ },
825
+ "node_modules/ee-first": {
826
+ "version": "1.1.1",
827
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
828
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
829
+ "license": "MIT"
830
+ },
831
+ "node_modules/encodeurl": {
832
+ "version": "2.0.0",
833
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
834
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
835
+ "license": "MIT",
836
+ "engines": {
837
+ "node": ">= 0.8"
838
+ }
839
+ },
840
+ "node_modules/es-define-property": {
841
+ "version": "1.0.1",
842
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
843
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
844
+ "license": "MIT",
845
+ "engines": {
846
+ "node": ">= 0.4"
847
+ }
848
+ },
849
+ "node_modules/es-errors": {
850
+ "version": "1.3.0",
851
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
852
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
853
+ "license": "MIT",
854
+ "engines": {
855
+ "node": ">= 0.4"
856
+ }
857
+ },
858
+ "node_modules/es-object-atoms": {
859
+ "version": "1.1.1",
860
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
861
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
862
+ "license": "MIT",
863
+ "dependencies": {
864
+ "es-errors": "^1.3.0"
865
+ },
866
+ "engines": {
867
+ "node": ">= 0.4"
868
+ }
869
+ },
870
+ "node_modules/esbuild": {
871
+ "version": "0.27.4",
872
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
873
+ "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
874
+ "dev": true,
875
+ "hasInstallScript": true,
876
+ "license": "MIT",
877
+ "bin": {
878
+ "esbuild": "bin/esbuild"
879
+ },
880
+ "engines": {
881
+ "node": ">=18"
882
+ },
883
+ "optionalDependencies": {
884
+ "@esbuild/aix-ppc64": "0.27.4",
885
+ "@esbuild/android-arm": "0.27.4",
886
+ "@esbuild/android-arm64": "0.27.4",
887
+ "@esbuild/android-x64": "0.27.4",
888
+ "@esbuild/darwin-arm64": "0.27.4",
889
+ "@esbuild/darwin-x64": "0.27.4",
890
+ "@esbuild/freebsd-arm64": "0.27.4",
891
+ "@esbuild/freebsd-x64": "0.27.4",
892
+ "@esbuild/linux-arm": "0.27.4",
893
+ "@esbuild/linux-arm64": "0.27.4",
894
+ "@esbuild/linux-ia32": "0.27.4",
895
+ "@esbuild/linux-loong64": "0.27.4",
896
+ "@esbuild/linux-mips64el": "0.27.4",
897
+ "@esbuild/linux-ppc64": "0.27.4",
898
+ "@esbuild/linux-riscv64": "0.27.4",
899
+ "@esbuild/linux-s390x": "0.27.4",
900
+ "@esbuild/linux-x64": "0.27.4",
901
+ "@esbuild/netbsd-arm64": "0.27.4",
902
+ "@esbuild/netbsd-x64": "0.27.4",
903
+ "@esbuild/openbsd-arm64": "0.27.4",
904
+ "@esbuild/openbsd-x64": "0.27.4",
905
+ "@esbuild/openharmony-arm64": "0.27.4",
906
+ "@esbuild/sunos-x64": "0.27.4",
907
+ "@esbuild/win32-arm64": "0.27.4",
908
+ "@esbuild/win32-ia32": "0.27.4",
909
+ "@esbuild/win32-x64": "0.27.4"
910
+ }
911
+ },
912
+ "node_modules/escape-html": {
913
+ "version": "1.0.3",
914
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
915
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
916
+ "license": "MIT"
917
+ },
918
+ "node_modules/etag": {
919
+ "version": "1.8.1",
920
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
921
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
922
+ "license": "MIT",
923
+ "engines": {
924
+ "node": ">= 0.6"
925
+ }
926
+ },
927
+ "node_modules/express": {
928
+ "version": "4.22.1",
929
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
930
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
931
+ "license": "MIT",
932
+ "dependencies": {
933
+ "accepts": "~1.3.8",
934
+ "array-flatten": "1.1.1",
935
+ "body-parser": "~1.20.3",
936
+ "content-disposition": "~0.5.4",
937
+ "content-type": "~1.0.4",
938
+ "cookie": "~0.7.1",
939
+ "cookie-signature": "~1.0.6",
940
+ "debug": "2.6.9",
941
+ "depd": "2.0.0",
942
+ "encodeurl": "~2.0.0",
943
+ "escape-html": "~1.0.3",
944
+ "etag": "~1.8.1",
945
+ "finalhandler": "~1.3.1",
946
+ "fresh": "~0.5.2",
947
+ "http-errors": "~2.0.0",
948
+ "merge-descriptors": "1.0.3",
949
+ "methods": "~1.1.2",
950
+ "on-finished": "~2.4.1",
951
+ "parseurl": "~1.3.3",
952
+ "path-to-regexp": "~0.1.12",
953
+ "proxy-addr": "~2.0.7",
954
+ "qs": "~6.14.0",
955
+ "range-parser": "~1.2.1",
956
+ "safe-buffer": "5.2.1",
957
+ "send": "~0.19.0",
958
+ "serve-static": "~1.16.2",
959
+ "setprototypeof": "1.2.0",
960
+ "statuses": "~2.0.1",
961
+ "type-is": "~1.6.18",
962
+ "utils-merge": "1.0.1",
963
+ "vary": "~1.1.2"
964
+ },
965
+ "engines": {
966
+ "node": ">= 0.10.0"
967
+ },
968
+ "funding": {
969
+ "type": "opencollective",
970
+ "url": "https://opencollective.com/express"
971
+ }
972
+ },
973
+ "node_modules/finalhandler": {
974
+ "version": "1.3.2",
975
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
976
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
977
+ "license": "MIT",
978
+ "dependencies": {
979
+ "debug": "2.6.9",
980
+ "encodeurl": "~2.0.0",
981
+ "escape-html": "~1.0.3",
982
+ "on-finished": "~2.4.1",
983
+ "parseurl": "~1.3.3",
984
+ "statuses": "~2.0.2",
985
+ "unpipe": "~1.0.0"
986
+ },
987
+ "engines": {
988
+ "node": ">= 0.8"
989
+ }
990
+ },
991
+ "node_modules/forwarded": {
992
+ "version": "0.2.0",
993
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
994
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
995
+ "license": "MIT",
996
+ "engines": {
997
+ "node": ">= 0.6"
998
+ }
999
+ },
1000
+ "node_modules/fresh": {
1001
+ "version": "0.5.2",
1002
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
1003
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
1004
+ "license": "MIT",
1005
+ "engines": {
1006
+ "node": ">= 0.6"
1007
+ }
1008
+ },
1009
+ "node_modules/fsevents": {
1010
+ "version": "2.3.3",
1011
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1012
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1013
+ "dev": true,
1014
+ "hasInstallScript": true,
1015
+ "license": "MIT",
1016
+ "optional": true,
1017
+ "os": [
1018
+ "darwin"
1019
+ ],
1020
+ "engines": {
1021
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1022
+ }
1023
+ },
1024
+ "node_modules/function-bind": {
1025
+ "version": "1.1.2",
1026
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1027
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1028
+ "license": "MIT",
1029
+ "funding": {
1030
+ "url": "https://github.com/sponsors/ljharb"
1031
+ }
1032
+ },
1033
+ "node_modules/get-intrinsic": {
1034
+ "version": "1.3.0",
1035
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1036
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1037
+ "license": "MIT",
1038
+ "dependencies": {
1039
+ "call-bind-apply-helpers": "^1.0.2",
1040
+ "es-define-property": "^1.0.1",
1041
+ "es-errors": "^1.3.0",
1042
+ "es-object-atoms": "^1.1.1",
1043
+ "function-bind": "^1.1.2",
1044
+ "get-proto": "^1.0.1",
1045
+ "gopd": "^1.2.0",
1046
+ "has-symbols": "^1.1.0",
1047
+ "hasown": "^2.0.2",
1048
+ "math-intrinsics": "^1.1.0"
1049
+ },
1050
+ "engines": {
1051
+ "node": ">= 0.4"
1052
+ },
1053
+ "funding": {
1054
+ "url": "https://github.com/sponsors/ljharb"
1055
+ }
1056
+ },
1057
+ "node_modules/get-proto": {
1058
+ "version": "1.0.1",
1059
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1060
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1061
+ "license": "MIT",
1062
+ "dependencies": {
1063
+ "dunder-proto": "^1.0.1",
1064
+ "es-object-atoms": "^1.0.0"
1065
+ },
1066
+ "engines": {
1067
+ "node": ">= 0.4"
1068
+ }
1069
+ },
1070
+ "node_modules/get-tsconfig": {
1071
+ "version": "4.13.7",
1072
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
1073
+ "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
1074
+ "dev": true,
1075
+ "license": "MIT",
1076
+ "dependencies": {
1077
+ "resolve-pkg-maps": "^1.0.0"
1078
+ },
1079
+ "funding": {
1080
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
1081
+ }
1082
+ },
1083
+ "node_modules/gopd": {
1084
+ "version": "1.2.0",
1085
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1086
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1087
+ "license": "MIT",
1088
+ "engines": {
1089
+ "node": ">= 0.4"
1090
+ },
1091
+ "funding": {
1092
+ "url": "https://github.com/sponsors/ljharb"
1093
+ }
1094
+ },
1095
+ "node_modules/has-symbols": {
1096
+ "version": "1.1.0",
1097
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1098
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1099
+ "license": "MIT",
1100
+ "engines": {
1101
+ "node": ">= 0.4"
1102
+ },
1103
+ "funding": {
1104
+ "url": "https://github.com/sponsors/ljharb"
1105
+ }
1106
+ },
1107
+ "node_modules/hasown": {
1108
+ "version": "2.0.2",
1109
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1110
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1111
+ "license": "MIT",
1112
+ "dependencies": {
1113
+ "function-bind": "^1.1.2"
1114
+ },
1115
+ "engines": {
1116
+ "node": ">= 0.4"
1117
+ }
1118
+ },
1119
+ "node_modules/http-errors": {
1120
+ "version": "2.0.1",
1121
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
1122
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
1123
+ "license": "MIT",
1124
+ "dependencies": {
1125
+ "depd": "~2.0.0",
1126
+ "inherits": "~2.0.4",
1127
+ "setprototypeof": "~1.2.0",
1128
+ "statuses": "~2.0.2",
1129
+ "toidentifier": "~1.0.1"
1130
+ },
1131
+ "engines": {
1132
+ "node": ">= 0.8"
1133
+ },
1134
+ "funding": {
1135
+ "type": "opencollective",
1136
+ "url": "https://opencollective.com/express"
1137
+ }
1138
+ },
1139
+ "node_modules/iconv-lite": {
1140
+ "version": "0.4.24",
1141
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1142
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1143
+ "license": "MIT",
1144
+ "dependencies": {
1145
+ "safer-buffer": ">= 2.1.2 < 3"
1146
+ },
1147
+ "engines": {
1148
+ "node": ">=0.10.0"
1149
+ }
1150
+ },
1151
+ "node_modules/inherits": {
1152
+ "version": "2.0.4",
1153
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1154
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1155
+ "license": "ISC"
1156
+ },
1157
+ "node_modules/ipaddr.js": {
1158
+ "version": "1.9.1",
1159
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1160
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1161
+ "license": "MIT",
1162
+ "engines": {
1163
+ "node": ">= 0.10"
1164
+ }
1165
+ },
1166
+ "node_modules/math-intrinsics": {
1167
+ "version": "1.1.0",
1168
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1169
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1170
+ "license": "MIT",
1171
+ "engines": {
1172
+ "node": ">= 0.4"
1173
+ }
1174
+ },
1175
+ "node_modules/media-typer": {
1176
+ "version": "0.3.0",
1177
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1178
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
1179
+ "license": "MIT",
1180
+ "engines": {
1181
+ "node": ">= 0.6"
1182
+ }
1183
+ },
1184
+ "node_modules/merge-descriptors": {
1185
+ "version": "1.0.3",
1186
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
1187
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
1188
+ "license": "MIT",
1189
+ "funding": {
1190
+ "url": "https://github.com/sponsors/sindresorhus"
1191
+ }
1192
+ },
1193
+ "node_modules/methods": {
1194
+ "version": "1.1.2",
1195
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1196
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1197
+ "license": "MIT",
1198
+ "engines": {
1199
+ "node": ">= 0.6"
1200
+ }
1201
+ },
1202
+ "node_modules/mime": {
1203
+ "version": "1.6.0",
1204
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1205
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1206
+ "license": "MIT",
1207
+ "bin": {
1208
+ "mime": "cli.js"
1209
+ },
1210
+ "engines": {
1211
+ "node": ">=4"
1212
+ }
1213
+ },
1214
+ "node_modules/mime-db": {
1215
+ "version": "1.52.0",
1216
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1217
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1218
+ "license": "MIT",
1219
+ "engines": {
1220
+ "node": ">= 0.6"
1221
+ }
1222
+ },
1223
+ "node_modules/mime-types": {
1224
+ "version": "2.1.35",
1225
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1226
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1227
+ "license": "MIT",
1228
+ "dependencies": {
1229
+ "mime-db": "1.52.0"
1230
+ },
1231
+ "engines": {
1232
+ "node": ">= 0.6"
1233
+ }
1234
+ },
1235
+ "node_modules/ms": {
1236
+ "version": "2.0.0",
1237
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1238
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1239
+ "license": "MIT"
1240
+ },
1241
+ "node_modules/multer": {
1242
+ "version": "2.1.1",
1243
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
1244
+ "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
1245
+ "license": "MIT",
1246
+ "dependencies": {
1247
+ "append-field": "^1.0.0",
1248
+ "busboy": "^1.6.0",
1249
+ "concat-stream": "^2.0.0",
1250
+ "type-is": "^1.6.18"
1251
+ },
1252
+ "engines": {
1253
+ "node": ">= 10.16.0"
1254
+ },
1255
+ "funding": {
1256
+ "type": "opencollective",
1257
+ "url": "https://opencollective.com/express"
1258
+ }
1259
+ },
1260
+ "node_modules/negotiator": {
1261
+ "version": "0.6.3",
1262
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1263
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1264
+ "license": "MIT",
1265
+ "engines": {
1266
+ "node": ">= 0.6"
1267
+ }
1268
+ },
1269
+ "node_modules/object-assign": {
1270
+ "version": "4.1.1",
1271
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1272
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1273
+ "license": "MIT",
1274
+ "engines": {
1275
+ "node": ">=0.10.0"
1276
+ }
1277
+ },
1278
+ "node_modules/object-inspect": {
1279
+ "version": "1.13.4",
1280
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1281
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1282
+ "license": "MIT",
1283
+ "engines": {
1284
+ "node": ">= 0.4"
1285
+ },
1286
+ "funding": {
1287
+ "url": "https://github.com/sponsors/ljharb"
1288
+ }
1289
+ },
1290
+ "node_modules/on-finished": {
1291
+ "version": "2.4.1",
1292
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1293
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1294
+ "license": "MIT",
1295
+ "dependencies": {
1296
+ "ee-first": "1.1.1"
1297
+ },
1298
+ "engines": {
1299
+ "node": ">= 0.8"
1300
+ }
1301
+ },
1302
+ "node_modules/parseurl": {
1303
+ "version": "1.3.3",
1304
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1305
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1306
+ "license": "MIT",
1307
+ "engines": {
1308
+ "node": ">= 0.8"
1309
+ }
1310
+ },
1311
+ "node_modules/path-to-regexp": {
1312
+ "version": "0.1.13",
1313
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
1314
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
1315
+ "license": "MIT"
1316
+ },
1317
+ "node_modules/pg": {
1318
+ "version": "8.20.0",
1319
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
1320
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
1321
+ "license": "MIT",
1322
+ "dependencies": {
1323
+ "pg-connection-string": "^2.12.0",
1324
+ "pg-pool": "^3.13.0",
1325
+ "pg-protocol": "^1.13.0",
1326
+ "pg-types": "2.2.0",
1327
+ "pgpass": "1.0.5"
1328
+ },
1329
+ "engines": {
1330
+ "node": ">= 16.0.0"
1331
+ },
1332
+ "optionalDependencies": {
1333
+ "pg-cloudflare": "^1.3.0"
1334
+ },
1335
+ "peerDependencies": {
1336
+ "pg-native": ">=3.0.1"
1337
+ },
1338
+ "peerDependenciesMeta": {
1339
+ "pg-native": {
1340
+ "optional": true
1341
+ }
1342
+ }
1343
+ },
1344
+ "node_modules/pg-cloudflare": {
1345
+ "version": "1.3.0",
1346
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
1347
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
1348
+ "license": "MIT",
1349
+ "optional": true
1350
+ },
1351
+ "node_modules/pg-connection-string": {
1352
+ "version": "2.12.0",
1353
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
1354
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
1355
+ "license": "MIT"
1356
+ },
1357
+ "node_modules/pg-int8": {
1358
+ "version": "1.0.1",
1359
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
1360
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
1361
+ "license": "ISC",
1362
+ "engines": {
1363
+ "node": ">=4.0.0"
1364
+ }
1365
+ },
1366
+ "node_modules/pg-pool": {
1367
+ "version": "3.13.0",
1368
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
1369
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
1370
+ "license": "MIT",
1371
+ "peerDependencies": {
1372
+ "pg": ">=8.0"
1373
+ }
1374
+ },
1375
+ "node_modules/pg-protocol": {
1376
+ "version": "1.13.0",
1377
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
1378
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
1379
+ "license": "MIT"
1380
+ },
1381
+ "node_modules/pg-types": {
1382
+ "version": "2.2.0",
1383
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
1384
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
1385
+ "license": "MIT",
1386
+ "dependencies": {
1387
+ "pg-int8": "1.0.1",
1388
+ "postgres-array": "~2.0.0",
1389
+ "postgres-bytea": "~1.0.0",
1390
+ "postgres-date": "~1.0.4",
1391
+ "postgres-interval": "^1.1.0"
1392
+ },
1393
+ "engines": {
1394
+ "node": ">=4"
1395
+ }
1396
+ },
1397
+ "node_modules/pgpass": {
1398
+ "version": "1.0.5",
1399
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
1400
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
1401
+ "license": "MIT",
1402
+ "dependencies": {
1403
+ "split2": "^4.1.0"
1404
+ }
1405
+ },
1406
+ "node_modules/postgres-array": {
1407
+ "version": "2.0.0",
1408
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
1409
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
1410
+ "license": "MIT",
1411
+ "engines": {
1412
+ "node": ">=4"
1413
+ }
1414
+ },
1415
+ "node_modules/postgres-bytea": {
1416
+ "version": "1.0.1",
1417
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
1418
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
1419
+ "license": "MIT",
1420
+ "engines": {
1421
+ "node": ">=0.10.0"
1422
+ }
1423
+ },
1424
+ "node_modules/postgres-date": {
1425
+ "version": "1.0.7",
1426
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
1427
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
1428
+ "license": "MIT",
1429
+ "engines": {
1430
+ "node": ">=0.10.0"
1431
+ }
1432
+ },
1433
+ "node_modules/postgres-interval": {
1434
+ "version": "1.2.0",
1435
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
1436
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
1437
+ "license": "MIT",
1438
+ "dependencies": {
1439
+ "xtend": "^4.0.0"
1440
+ },
1441
+ "engines": {
1442
+ "node": ">=0.10.0"
1443
+ }
1444
+ },
1445
+ "node_modules/proxy-addr": {
1446
+ "version": "2.0.7",
1447
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1448
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1449
+ "license": "MIT",
1450
+ "dependencies": {
1451
+ "forwarded": "0.2.0",
1452
+ "ipaddr.js": "1.9.1"
1453
+ },
1454
+ "engines": {
1455
+ "node": ">= 0.10"
1456
+ }
1457
+ },
1458
+ "node_modules/qs": {
1459
+ "version": "6.14.2",
1460
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
1461
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
1462
+ "license": "BSD-3-Clause",
1463
+ "dependencies": {
1464
+ "side-channel": "^1.1.0"
1465
+ },
1466
+ "engines": {
1467
+ "node": ">=0.6"
1468
+ },
1469
+ "funding": {
1470
+ "url": "https://github.com/sponsors/ljharb"
1471
+ }
1472
+ },
1473
+ "node_modules/range-parser": {
1474
+ "version": "1.2.1",
1475
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1476
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1477
+ "license": "MIT",
1478
+ "engines": {
1479
+ "node": ">= 0.6"
1480
+ }
1481
+ },
1482
+ "node_modules/raw-body": {
1483
+ "version": "2.5.3",
1484
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
1485
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
1486
+ "license": "MIT",
1487
+ "dependencies": {
1488
+ "bytes": "~3.1.2",
1489
+ "http-errors": "~2.0.1",
1490
+ "iconv-lite": "~0.4.24",
1491
+ "unpipe": "~1.0.0"
1492
+ },
1493
+ "engines": {
1494
+ "node": ">= 0.8"
1495
+ }
1496
+ },
1497
+ "node_modules/readable-stream": {
1498
+ "version": "3.6.2",
1499
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
1500
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
1501
+ "license": "MIT",
1502
+ "dependencies": {
1503
+ "inherits": "^2.0.3",
1504
+ "string_decoder": "^1.1.1",
1505
+ "util-deprecate": "^1.0.1"
1506
+ },
1507
+ "engines": {
1508
+ "node": ">= 6"
1509
+ }
1510
+ },
1511
+ "node_modules/resolve-pkg-maps": {
1512
+ "version": "1.0.0",
1513
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
1514
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
1515
+ "dev": true,
1516
+ "license": "MIT",
1517
+ "funding": {
1518
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
1519
+ }
1520
+ },
1521
+ "node_modules/safe-buffer": {
1522
+ "version": "5.2.1",
1523
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1524
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1525
+ "funding": [
1526
+ {
1527
+ "type": "github",
1528
+ "url": "https://github.com/sponsors/feross"
1529
+ },
1530
+ {
1531
+ "type": "patreon",
1532
+ "url": "https://www.patreon.com/feross"
1533
+ },
1534
+ {
1535
+ "type": "consulting",
1536
+ "url": "https://feross.org/support"
1537
+ }
1538
+ ],
1539
+ "license": "MIT"
1540
+ },
1541
+ "node_modules/safer-buffer": {
1542
+ "version": "2.1.2",
1543
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1544
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1545
+ "license": "MIT"
1546
+ },
1547
+ "node_modules/send": {
1548
+ "version": "0.19.2",
1549
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
1550
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
1551
+ "license": "MIT",
1552
+ "dependencies": {
1553
+ "debug": "2.6.9",
1554
+ "depd": "2.0.0",
1555
+ "destroy": "1.2.0",
1556
+ "encodeurl": "~2.0.0",
1557
+ "escape-html": "~1.0.3",
1558
+ "etag": "~1.8.1",
1559
+ "fresh": "~0.5.2",
1560
+ "http-errors": "~2.0.1",
1561
+ "mime": "1.6.0",
1562
+ "ms": "2.1.3",
1563
+ "on-finished": "~2.4.1",
1564
+ "range-parser": "~1.2.1",
1565
+ "statuses": "~2.0.2"
1566
+ },
1567
+ "engines": {
1568
+ "node": ">= 0.8.0"
1569
+ }
1570
+ },
1571
+ "node_modules/send/node_modules/ms": {
1572
+ "version": "2.1.3",
1573
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1574
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1575
+ "license": "MIT"
1576
+ },
1577
+ "node_modules/serve-static": {
1578
+ "version": "1.16.3",
1579
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
1580
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
1581
+ "license": "MIT",
1582
+ "dependencies": {
1583
+ "encodeurl": "~2.0.0",
1584
+ "escape-html": "~1.0.3",
1585
+ "parseurl": "~1.3.3",
1586
+ "send": "~0.19.1"
1587
+ },
1588
+ "engines": {
1589
+ "node": ">= 0.8.0"
1590
+ }
1591
+ },
1592
+ "node_modules/setprototypeof": {
1593
+ "version": "1.2.0",
1594
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1595
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1596
+ "license": "ISC"
1597
+ },
1598
+ "node_modules/side-channel": {
1599
+ "version": "1.1.0",
1600
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1601
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1602
+ "license": "MIT",
1603
+ "dependencies": {
1604
+ "es-errors": "^1.3.0",
1605
+ "object-inspect": "^1.13.3",
1606
+ "side-channel-list": "^1.0.0",
1607
+ "side-channel-map": "^1.0.1",
1608
+ "side-channel-weakmap": "^1.0.2"
1609
+ },
1610
+ "engines": {
1611
+ "node": ">= 0.4"
1612
+ },
1613
+ "funding": {
1614
+ "url": "https://github.com/sponsors/ljharb"
1615
+ }
1616
+ },
1617
+ "node_modules/side-channel-list": {
1618
+ "version": "1.0.0",
1619
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1620
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1621
+ "license": "MIT",
1622
+ "dependencies": {
1623
+ "es-errors": "^1.3.0",
1624
+ "object-inspect": "^1.13.3"
1625
+ },
1626
+ "engines": {
1627
+ "node": ">= 0.4"
1628
+ },
1629
+ "funding": {
1630
+ "url": "https://github.com/sponsors/ljharb"
1631
+ }
1632
+ },
1633
+ "node_modules/side-channel-map": {
1634
+ "version": "1.0.1",
1635
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1636
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1637
+ "license": "MIT",
1638
+ "dependencies": {
1639
+ "call-bound": "^1.0.2",
1640
+ "es-errors": "^1.3.0",
1641
+ "get-intrinsic": "^1.2.5",
1642
+ "object-inspect": "^1.13.3"
1643
+ },
1644
+ "engines": {
1645
+ "node": ">= 0.4"
1646
+ },
1647
+ "funding": {
1648
+ "url": "https://github.com/sponsors/ljharb"
1649
+ }
1650
+ },
1651
+ "node_modules/side-channel-weakmap": {
1652
+ "version": "1.0.2",
1653
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1654
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1655
+ "license": "MIT",
1656
+ "dependencies": {
1657
+ "call-bound": "^1.0.2",
1658
+ "es-errors": "^1.3.0",
1659
+ "get-intrinsic": "^1.2.5",
1660
+ "object-inspect": "^1.13.3",
1661
+ "side-channel-map": "^1.0.1"
1662
+ },
1663
+ "engines": {
1664
+ "node": ">= 0.4"
1665
+ },
1666
+ "funding": {
1667
+ "url": "https://github.com/sponsors/ljharb"
1668
+ }
1669
+ },
1670
+ "node_modules/split2": {
1671
+ "version": "4.2.0",
1672
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
1673
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
1674
+ "license": "ISC",
1675
+ "engines": {
1676
+ "node": ">= 10.x"
1677
+ }
1678
+ },
1679
+ "node_modules/statuses": {
1680
+ "version": "2.0.2",
1681
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1682
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1683
+ "license": "MIT",
1684
+ "engines": {
1685
+ "node": ">= 0.8"
1686
+ }
1687
+ },
1688
+ "node_modules/streamsearch": {
1689
+ "version": "1.1.0",
1690
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
1691
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
1692
+ "engines": {
1693
+ "node": ">=10.0.0"
1694
+ }
1695
+ },
1696
+ "node_modules/string_decoder": {
1697
+ "version": "1.3.0",
1698
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
1699
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
1700
+ "license": "MIT",
1701
+ "dependencies": {
1702
+ "safe-buffer": "~5.2.0"
1703
+ }
1704
+ },
1705
+ "node_modules/toidentifier": {
1706
+ "version": "1.0.1",
1707
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1708
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1709
+ "license": "MIT",
1710
+ "engines": {
1711
+ "node": ">=0.6"
1712
+ }
1713
+ },
1714
+ "node_modules/tsx": {
1715
+ "version": "4.21.0",
1716
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
1717
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
1718
+ "dev": true,
1719
+ "license": "MIT",
1720
+ "dependencies": {
1721
+ "esbuild": "~0.27.0",
1722
+ "get-tsconfig": "^4.7.5"
1723
+ },
1724
+ "bin": {
1725
+ "tsx": "dist/cli.mjs"
1726
+ },
1727
+ "engines": {
1728
+ "node": ">=18.0.0"
1729
+ },
1730
+ "optionalDependencies": {
1731
+ "fsevents": "~2.3.3"
1732
+ }
1733
+ },
1734
+ "node_modules/type-is": {
1735
+ "version": "1.6.18",
1736
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1737
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1738
+ "license": "MIT",
1739
+ "dependencies": {
1740
+ "media-typer": "0.3.0",
1741
+ "mime-types": "~2.1.24"
1742
+ },
1743
+ "engines": {
1744
+ "node": ">= 0.6"
1745
+ }
1746
+ },
1747
+ "node_modules/typedarray": {
1748
+ "version": "0.0.6",
1749
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
1750
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
1751
+ "license": "MIT"
1752
+ },
1753
+ "node_modules/typescript": {
1754
+ "version": "5.9.3",
1755
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1756
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1757
+ "dev": true,
1758
+ "license": "Apache-2.0",
1759
+ "bin": {
1760
+ "tsc": "bin/tsc",
1761
+ "tsserver": "bin/tsserver"
1762
+ },
1763
+ "engines": {
1764
+ "node": ">=14.17"
1765
+ }
1766
+ },
1767
+ "node_modules/undici-types": {
1768
+ "version": "6.21.0",
1769
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1770
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
1771
+ "dev": true,
1772
+ "license": "MIT"
1773
+ },
1774
+ "node_modules/unpipe": {
1775
+ "version": "1.0.0",
1776
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1777
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1778
+ "license": "MIT",
1779
+ "engines": {
1780
+ "node": ">= 0.8"
1781
+ }
1782
+ },
1783
+ "node_modules/util-deprecate": {
1784
+ "version": "1.0.2",
1785
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1786
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
1787
+ "license": "MIT"
1788
+ },
1789
+ "node_modules/utils-merge": {
1790
+ "version": "1.0.1",
1791
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1792
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1793
+ "license": "MIT",
1794
+ "engines": {
1795
+ "node": ">= 0.4.0"
1796
+ }
1797
+ },
1798
+ "node_modules/vary": {
1799
+ "version": "1.1.2",
1800
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1801
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1802
+ "license": "MIT",
1803
+ "engines": {
1804
+ "node": ">= 0.8"
1805
+ }
1806
+ },
1807
+ "node_modules/xtend": {
1808
+ "version": "4.0.2",
1809
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
1810
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
1811
+ "license": "MIT",
1812
+ "engines": {
1813
+ "node": ">=0.4"
1814
+ }
1815
+ }
1816
+ }
1817
+ }
civic-platform/backend/package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "civicpulse-backend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts",
8
+ "build": "tsc -p tsconfig.json",
9
+ "start": "node dist/server.js",
10
+ "db:init": "tsx src/scripts/init-db.ts",
11
+ "db:seed": "tsx src/scripts/seed-db.ts",
12
+ "smoke": "tsx src/scripts/smoke-test.ts"
13
+ },
14
+ "dependencies": {
15
+ "cors": "^2.8.5",
16
+ "dotenv": "^16.4.7",
17
+ "express": "^4.21.2",
18
+ "multer": "^2.1.1",
19
+ "pg": "^8.13.3"
20
+ },
21
+ "devDependencies": {
22
+ "@types/cors": "^2.8.17",
23
+ "@types/express": "^5.0.1",
24
+ "@types/multer": "^2.1.0",
25
+ "@types/node": "^22.13.10",
26
+ "@types/pg": "^8.11.11",
27
+ "tsx": "^4.19.3",
28
+ "typescript": "^5.8.2"
29
+ }
30
+ }
civic-platform/backend/server.err.log ADDED
File without changes
civic-platform/backend/server.out.log ADDED
File without changes
civic-platform/backend/sql/001_initial_schema.sql ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CREATE EXTENSION IF NOT EXISTS "pgcrypto";
2
+
3
+ CREATE TABLE roles (
4
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
5
+ name VARCHAR(50) NOT NULL UNIQUE,
6
+ description TEXT,
7
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
8
+ );
9
+
10
+ CREATE TABLE departments (
11
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
12
+ name VARCHAR(120) NOT NULL UNIQUE,
13
+ code VARCHAR(40) NOT NULL UNIQUE,
14
+ description TEXT,
15
+ is_emergency BOOLEAN NOT NULL DEFAULT FALSE,
16
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
17
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
18
+ );
19
+
20
+ CREATE TABLE wards (
21
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
22
+ name VARCHAR(120) NOT NULL,
23
+ code VARCHAR(40) NOT NULL UNIQUE,
24
+ city_name VARCHAR(120) NOT NULL,
25
+ state_name VARCHAR(120),
26
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
27
+ );
28
+
29
+ CREATE TABLE users (
30
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
31
+ full_name VARCHAR(160) NOT NULL,
32
+ email VARCHAR(160) UNIQUE,
33
+ phone VARCHAR(20) UNIQUE,
34
+ password_hash TEXT,
35
+ role_id UUID NOT NULL REFERENCES roles(id),
36
+ department_id UUID REFERENCES departments(id),
37
+ ward_id UUID REFERENCES wards(id),
38
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
39
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
40
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
41
+ );
42
+
43
+ CREATE TABLE domains (
44
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
45
+ name VARCHAR(120) NOT NULL UNIQUE,
46
+ description TEXT,
47
+ is_emergency BOOLEAN NOT NULL DEFAULT FALSE,
48
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
49
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
50
+ );
51
+
52
+ CREATE TABLE sub_problems (
53
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
54
+ domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
55
+ name VARCHAR(120) NOT NULL,
56
+ description TEXT,
57
+ severity_hint VARCHAR(20),
58
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
59
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
60
+ CONSTRAINT unique_sub_problem_per_domain UNIQUE (domain_id, name)
61
+ );
62
+
63
+ CREATE TABLE complaints (
64
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
65
+ complaint_code VARCHAR(40) NOT NULL UNIQUE,
66
+ citizen_id UUID NOT NULL REFERENCES users(id),
67
+ domain_id UUID REFERENCES domains(id),
68
+ sub_problem_id UUID REFERENCES sub_problems(id),
69
+ title VARCHAR(220) NOT NULL,
70
+ description TEXT,
71
+ status VARCHAR(32) NOT NULL,
72
+ priority_level VARCHAR(2) NOT NULL,
73
+ is_emergency BOOLEAN NOT NULL DEFAULT FALSE,
74
+ department_id UUID REFERENCES departments(id),
75
+ ward_id UUID REFERENCES wards(id),
76
+ submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
77
+ resolved_at TIMESTAMPTZ,
78
+ closed_at TIMESTAMPTZ,
79
+ reopened_count INTEGER NOT NULL DEFAULT 0,
80
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
81
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
82
+ CONSTRAINT complaints_status_check CHECK (
83
+ status IN (
84
+ 'submitted',
85
+ 'validated',
86
+ 'classified',
87
+ 'prioritized',
88
+ 'assigned',
89
+ 'accepted',
90
+ 'in_progress',
91
+ 'resolved',
92
+ 'citizen_verified',
93
+ 'closed',
94
+ 'escalated',
95
+ 'reopened',
96
+ 'duplicate',
97
+ 'rejected',
98
+ 'on_hold'
99
+ )
100
+ ),
101
+ CONSTRAINT complaints_priority_check CHECK (priority_level IN ('P1', 'P2', 'P3', 'P4'))
102
+ );
103
+
104
+ CREATE TABLE complaint_locations (
105
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
106
+ complaint_id UUID NOT NULL UNIQUE REFERENCES complaints(id) ON DELETE CASCADE,
107
+ latitude NUMERIC(10, 7) NOT NULL,
108
+ longitude NUMERIC(10, 7) NOT NULL,
109
+ address_line TEXT,
110
+ landmark VARCHAR(220),
111
+ city_name VARCHAR(120),
112
+ state_name VARCHAR(120),
113
+ postal_code VARCHAR(20),
114
+ ward_id UUID REFERENCES wards(id),
115
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
116
+ );
117
+
118
+ CREATE TABLE complaint_media (
119
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
120
+ complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE,
121
+ uploaded_by UUID REFERENCES users(id),
122
+ media_type VARCHAR(20) NOT NULL,
123
+ file_path TEXT NOT NULL,
124
+ file_url TEXT,
125
+ mime_type VARCHAR(120),
126
+ is_resolution_proof BOOLEAN NOT NULL DEFAULT FALSE,
127
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
128
+ CONSTRAINT complaint_media_type_check CHECK (media_type IN ('image', 'video', 'audio'))
129
+ );
130
+
131
+ CREATE TABLE complaint_status_history (
132
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
133
+ complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE,
134
+ old_status VARCHAR(32),
135
+ new_status VARCHAR(32) NOT NULL,
136
+ changed_by UUID REFERENCES users(id),
137
+ change_reason TEXT,
138
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
139
+ );
140
+
141
+ CREATE TABLE assignments (
142
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
143
+ complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE,
144
+ department_id UUID NOT NULL REFERENCES departments(id),
145
+ assigned_to_user_id UUID REFERENCES users(id),
146
+ assigned_by_user_id UUID REFERENCES users(id),
147
+ assignment_status VARCHAR(20) NOT NULL,
148
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
149
+ accepted_at TIMESTAMPTZ,
150
+ completed_at TIMESTAMPTZ,
151
+ notes TEXT,
152
+ CONSTRAINT assignments_status_check CHECK (
153
+ assignment_status IN ('assigned', 'accepted', 'in_progress', 'completed', 'reassigned')
154
+ )
155
+ );
156
+
157
+ CREATE TABLE routing_rules (
158
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
159
+ domain_id UUID REFERENCES domains(id),
160
+ sub_problem_id UUID REFERENCES sub_problems(id),
161
+ ward_id UUID REFERENCES wards(id),
162
+ department_id UUID NOT NULL REFERENCES departments(id),
163
+ priority_override VARCHAR(2),
164
+ is_emergency_route BOOLEAN NOT NULL DEFAULT FALSE,
165
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
166
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
167
+ CONSTRAINT routing_rules_priority_check CHECK (priority_override IS NULL OR priority_override IN ('P1', 'P2', 'P3', 'P4'))
168
+ );
169
+
170
+ CREATE TABLE priority_rules (
171
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
172
+ domain_id UUID REFERENCES domains(id),
173
+ sub_problem_id UUID REFERENCES sub_problems(id),
174
+ base_priority VARCHAR(2) NOT NULL,
175
+ near_sensitive_zone_boost INTEGER NOT NULL DEFAULT 0,
176
+ repeat_complaint_boost INTEGER NOT NULL DEFAULT 0,
177
+ reopened_boost INTEGER NOT NULL DEFAULT 0,
178
+ sla_breach_boost INTEGER NOT NULL DEFAULT 0,
179
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
180
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
181
+ CONSTRAINT priority_rules_base_check CHECK (base_priority IN ('P1', 'P2', 'P3', 'P4'))
182
+ );
183
+
184
+ CREATE TABLE complaint_notes (
185
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
186
+ complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE,
187
+ author_id UUID REFERENCES users(id),
188
+ note_type VARCHAR(20) NOT NULL,
189
+ note_text TEXT NOT NULL,
190
+ is_internal BOOLEAN NOT NULL DEFAULT TRUE,
191
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
192
+ CONSTRAINT complaint_notes_type_check CHECK (
193
+ note_type IN ('operator_note', 'field_note', 'citizen_note', 'system_note')
194
+ )
195
+ );
196
+
197
+ CREATE TABLE notifications (
198
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
199
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
200
+ complaint_id UUID REFERENCES complaints(id) ON DELETE CASCADE,
201
+ title VARCHAR(220) NOT NULL,
202
+ message TEXT NOT NULL,
203
+ notification_type VARCHAR(50) NOT NULL,
204
+ is_read BOOLEAN NOT NULL DEFAULT FALSE,
205
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
206
+ );
207
+
208
+ CREATE TABLE feedback (
209
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
210
+ complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE,
211
+ citizen_id UUID NOT NULL REFERENCES users(id),
212
+ rating INTEGER CHECK (rating BETWEEN 1 AND 5),
213
+ comment TEXT,
214
+ reopen_requested BOOLEAN NOT NULL DEFAULT FALSE,
215
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
216
+ );
217
+
218
+ CREATE TABLE audit_logs (
219
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
220
+ actor_user_id UUID REFERENCES users(id),
221
+ entity_type VARCHAR(50) NOT NULL,
222
+ entity_id UUID,
223
+ action VARCHAR(120) NOT NULL,
224
+ details JSONB,
225
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
226
+ );
227
+
228
+ CREATE INDEX idx_users_role_id ON users(role_id);
229
+ CREATE INDEX idx_users_department_id ON users(department_id);
230
+ CREATE INDEX idx_users_ward_id ON users(ward_id);
231
+ CREATE INDEX idx_sub_problems_domain_id ON sub_problems(domain_id);
232
+ CREATE INDEX idx_complaints_citizen_id ON complaints(citizen_id);
233
+ CREATE INDEX idx_complaints_domain_id ON complaints(domain_id);
234
+ CREATE INDEX idx_complaints_department_id ON complaints(department_id);
235
+ CREATE INDEX idx_complaints_ward_id ON complaints(ward_id);
236
+ CREATE INDEX idx_complaints_status ON complaints(status);
237
+ CREATE INDEX idx_complaints_priority_level ON complaints(priority_level);
238
+ CREATE INDEX idx_complaints_submitted_at ON complaints(submitted_at);
239
+ CREATE INDEX idx_complaint_media_complaint_id ON complaint_media(complaint_id);
240
+ CREATE INDEX idx_complaint_status_history_complaint_id ON complaint_status_history(complaint_id);
241
+ CREATE INDEX idx_assignments_complaint_id ON assignments(complaint_id);
242
+ CREATE INDEX idx_assignments_department_id ON assignments(department_id);
243
+ CREATE INDEX idx_routing_rules_department_id ON routing_rules(department_id);
244
+ CREATE INDEX idx_notifications_user_id ON notifications(user_id);
245
+ CREATE INDEX idx_feedback_complaint_id ON feedback(complaint_id);
246
+ CREATE INDEX idx_audit_logs_actor_user_id ON audit_logs(actor_user_id);
civic-platform/backend/sql/002_seed_core_data.sql ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ INSERT INTO roles (id, name, description)
2
+ VALUES
3
+ ('10000000-0000-0000-0000-000000000001', 'citizen', 'Citizen user who reports and tracks complaints'),
4
+ ('10000000-0000-0000-0000-000000000002', 'department_operator', 'Department operator who validates and assigns complaints'),
5
+ ('10000000-0000-0000-0000-000000000003', 'municipal_admin', 'Municipal administrator with cross-department access'),
6
+ ('10000000-0000-0000-0000-000000000004', 'field_officer', 'Field officer who executes assigned work on the ground')
7
+ ON CONFLICT (name) DO NOTHING;
8
+
9
+ INSERT INTO departments (id, name, code, description, is_emergency)
10
+ VALUES
11
+ ('20000000-0000-0000-0000-000000000001', 'Roads and Public Works', 'roads-public-works', 'Handles roads, potholes, and transport infrastructure issues', FALSE),
12
+ ('20000000-0000-0000-0000-000000000002', 'Sanitation and Solid Waste', 'sanitation-solid-waste', 'Handles waste collection, garbage overflow, and sanitation complaints', FALSE),
13
+ ('20000000-0000-0000-0000-000000000003', 'Water and Sewerage', 'water-sewerage', 'Handles water leakage, sewage, and drainage issues', FALSE),
14
+ ('20000000-0000-0000-0000-000000000004', 'Electrical and Street Lighting', 'electrical-street-lighting', 'Handles streetlight and public electrical complaints', FALSE),
15
+ ('20000000-0000-0000-0000-000000000005', 'Municipal Maintenance', 'municipal-maintenance', 'Handles open manholes and civic infrastructure maintenance', FALSE),
16
+ ('20000000-0000-0000-0000-000000000006', 'Disaster Management Authority', 'disaster-management', 'Handles flood, collapse, rescue, and emergency coordination', TRUE)
17
+ ON CONFLICT (code) DO NOTHING;
18
+
19
+ INSERT INTO wards (id, name, code, city_name, state_name)
20
+ VALUES
21
+ ('30000000-0000-0000-0000-000000000001', 'Ward 4', 'ward-4', 'Sample City', 'Sample State'),
22
+ ('30000000-0000-0000-0000-000000000002', 'Ward 8', 'ward-8', 'Sample City', 'Sample State'),
23
+ ('30000000-0000-0000-0000-000000000003', 'Ward 12', 'ward-12', 'Sample City', 'Sample State')
24
+ ON CONFLICT (code) DO NOTHING;
25
+
26
+ INSERT INTO domains (id, name, description, is_emergency)
27
+ VALUES
28
+ ('40000000-0000-0000-0000-000000000001', 'Roads and Transportation', 'Road safety, traffic support, and mobility complaints', FALSE),
29
+ ('40000000-0000-0000-0000-000000000002', 'Sanitation and Waste Management', 'Waste collection, cleanliness, and sanitation complaints', FALSE),
30
+ ('40000000-0000-0000-0000-000000000003', 'Water Supply, Sewerage, and Drainage', 'Water access, sewage, and drainage complaints', FALSE),
31
+ ('40000000-0000-0000-0000-000000000004', 'Street Lighting and Electrical Infrastructure', 'Public lighting and electrical safety complaints', FALSE),
32
+ ('40000000-0000-0000-0000-000000000005', 'Public Infrastructure and Amenities', 'Maintenance needs for public assets and shared facilities', FALSE),
33
+ ('40000000-0000-0000-0000-000000000006', 'Environment and Public Health', 'Public hygiene and environmental safety concerns', FALSE),
34
+ ('40000000-0000-0000-0000-000000000007', 'Fire Emergencies', 'Emergency fire and smoke incidents', TRUE),
35
+ ('40000000-0000-0000-0000-000000000008', 'Flood and Water Disaster', 'Critical flood and water disaster events', TRUE)
36
+ ON CONFLICT (name) DO NOTHING;
37
+
38
+ INSERT INTO users (id, full_name, email, phone, password_hash, role_id, department_id, ward_id)
39
+ VALUES
40
+ (
41
+ '00000000-0000-0000-0000-000000000001',
42
+ 'Sample Citizen',
43
+ 'citizen@example.com',
44
+ '9000000001',
45
+ 'dev-placeholder-hash',
46
+ '10000000-0000-0000-0000-000000000001',
47
+ NULL,
48
+ '30000000-0000-0000-0000-000000000003'
49
+ ),
50
+ (
51
+ '11111111-1111-1111-1111-111111111111',
52
+ 'Sample Operator',
53
+ 'operator@example.com',
54
+ '9000000002',
55
+ 'dev-placeholder-hash',
56
+ '10000000-0000-0000-0000-000000000002',
57
+ '20000000-0000-0000-0000-000000000005',
58
+ '30000000-0000-0000-0000-000000000001'
59
+ ),
60
+ (
61
+ '22222222-2222-2222-2222-222222222222',
62
+ 'Sample Field Officer',
63
+ 'officer@example.com',
64
+ '9000000003',
65
+ 'dev-placeholder-hash',
66
+ '10000000-0000-0000-0000-000000000004',
67
+ '20000000-0000-0000-0000-000000000005',
68
+ '30000000-0000-0000-0000-000000000001'
69
+ ),
70
+ (
71
+ '22222222-2222-2222-2222-222222222223',
72
+ 'Road Field Officer',
73
+ 'roads.officer@example.com',
74
+ '9000000004',
75
+ 'dev-placeholder-hash',
76
+ '10000000-0000-0000-0000-000000000004',
77
+ '20000000-0000-0000-0000-000000000001',
78
+ '30000000-0000-0000-0000-000000000001'
79
+ ),
80
+ (
81
+ '22222222-2222-2222-2222-222222222224',
82
+ 'Sanitation Field Officer',
83
+ 'sanitation.officer@example.com',
84
+ '9000000005',
85
+ 'dev-placeholder-hash',
86
+ '10000000-0000-0000-0000-000000000004',
87
+ '20000000-0000-0000-0000-000000000002',
88
+ '30000000-0000-0000-0000-000000000002'
89
+ ),
90
+ (
91
+ '22222222-2222-2222-2222-222222222225',
92
+ 'Water Field Officer',
93
+ 'water.officer@example.com',
94
+ '9000000006',
95
+ 'dev-placeholder-hash',
96
+ '10000000-0000-0000-0000-000000000004',
97
+ '20000000-0000-0000-0000-000000000003',
98
+ '30000000-0000-0000-0000-000000000002'
99
+ ),
100
+ (
101
+ '22222222-2222-2222-2222-222222222226',
102
+ 'Electrical Field Officer',
103
+ 'electrical.officer@example.com',
104
+ '9000000007',
105
+ 'dev-placeholder-hash',
106
+ '10000000-0000-0000-0000-000000000004',
107
+ '20000000-0000-0000-0000-000000000004',
108
+ '30000000-0000-0000-0000-000000000003'
109
+ )
110
+ ON CONFLICT (email) DO NOTHING;
111
+
112
+ INSERT INTO routing_rules (domain_id, sub_problem_id, ward_id, department_id, priority_override, is_emergency_route)
113
+ VALUES
114
+ ('40000000-0000-0000-0000-000000000001', NULL, NULL, '20000000-0000-0000-0000-000000000001', NULL, FALSE),
115
+ ('40000000-0000-0000-0000-000000000002', NULL, NULL, '20000000-0000-0000-0000-000000000002', NULL, FALSE),
116
+ ('40000000-0000-0000-0000-000000000003', NULL, NULL, '20000000-0000-0000-0000-000000000003', NULL, FALSE),
117
+ ('40000000-0000-0000-0000-000000000004', NULL, NULL, '20000000-0000-0000-0000-000000000004', NULL, FALSE),
118
+ ('40000000-0000-0000-0000-000000000005', NULL, NULL, '20000000-0000-0000-0000-000000000005', 'P1', FALSE),
119
+ ('40000000-0000-0000-0000-000000000006', NULL, NULL, '20000000-0000-0000-0000-000000000002', NULL, FALSE),
120
+ ('40000000-0000-0000-0000-000000000007', NULL, NULL, '20000000-0000-0000-0000-000000000006', 'P1', TRUE),
121
+ ('40000000-0000-0000-0000-000000000008', NULL, NULL, '20000000-0000-0000-0000-000000000006', 'P1', TRUE);
122
+
123
+ INSERT INTO priority_rules (domain_id, sub_problem_id, base_priority)
124
+ VALUES
125
+ ('40000000-0000-0000-0000-000000000001', NULL, 'P3'),
126
+ ('40000000-0000-0000-0000-000000000002', NULL, 'P3'),
127
+ ('40000000-0000-0000-0000-000000000003', NULL, 'P2'),
128
+ ('40000000-0000-0000-0000-000000000004', NULL, 'P3'),
129
+ ('40000000-0000-0000-0000-000000000005', NULL, 'P2'),
130
+ ('40000000-0000-0000-0000-000000000006', NULL, 'P3'),
131
+ ('40000000-0000-0000-0000-000000000007', NULL, 'P1'),
132
+ ('40000000-0000-0000-0000-000000000008', NULL, 'P1')
133
+ ON CONFLICT DO NOTHING;
civic-platform/backend/sql/003_performance_indexes.sql ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CREATE INDEX IF NOT EXISTS idx_complaints_status_submitted_at
2
+ ON complaints (status, submitted_at DESC);
3
+
4
+ CREATE INDEX IF NOT EXISTS idx_complaints_department_status
5
+ ON complaints (department_id, status, submitted_at DESC);
6
+
7
+ CREATE INDEX IF NOT EXISTS idx_complaints_domain_submitted_at
8
+ ON complaints (domain_id, submitted_at DESC);
9
+
10
+ CREATE INDEX IF NOT EXISTS idx_complaints_priority_status
11
+ ON complaints (priority_level, status, submitted_at DESC);
12
+
13
+ CREATE INDEX IF NOT EXISTS idx_assignments_assigned_to_status
14
+ ON assignments (assigned_to_user_id, assignment_status, assigned_at DESC);
15
+
16
+ CREATE INDEX IF NOT EXISTS idx_assignments_complaint_assigned_at
17
+ ON assignments (complaint_id, assigned_at DESC);
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_notifications_user_read_created
20
+ ON notifications (user_id, is_read, created_at DESC);
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_status_history_complaint_created
23
+ ON complaint_status_history (complaint_id, created_at DESC);
24
+
25
+ CREATE INDEX IF NOT EXISTS idx_feedback_complaint_created
26
+ ON feedback (complaint_id, created_at DESC);
27
+
28
+ CREATE INDEX IF NOT EXISTS idx_notes_complaint_created
29
+ ON complaint_notes (complaint_id, created_at DESC);
30
+
31
+ CREATE INDEX IF NOT EXISTS idx_locations_lat_lng
32
+ ON complaint_locations (latitude, longitude);
civic-platform/backend/sql/README.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SQL Files
2
+
3
+ ## Current files
4
+
5
+ - `001_initial_schema.sql`
6
+ - `002_seed_core_data.sql`
7
+
8
+ ## Usage
9
+
10
+ Apply the schema file to the `civicpulse` PostgreSQL database before running the complaint APIs.
11
+
12
+ Recommended order:
13
+
14
+ 1. create database
15
+ 2. apply `001_initial_schema.sql`
16
+ 3. apply `002_seed_core_data.sql`
17
+ 4. start backend
18
+
19
+ Future migrations should continue with:
20
+
21
+ - `003_...`
22
+ - `004_...`
civic-platform/backend/src/app.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import cors from "cors";
4
+ import express from "express";
5
+ import { aiRouter } from "./modules/ai/ai.routes.js";
6
+ import { authRouter } from "./modules/auth/auth.routes.js";
7
+ import { env } from "./config/env.js";
8
+ import { rateLimitMiddleware, securityHeaders } from "./lib/security.js";
9
+ import { complaintRouter } from "./modules/complaints/complaint.routes.js";
10
+ import { departmentRouter } from "./modules/departments/department.routes.js";
11
+ import { domainRouter } from "./modules/domains/domain.routes.js";
12
+ import { userRouter } from "./modules/users/user.routes.js";
13
+ import { healthRouter } from "./routes/health.js";
14
+
15
+ export function createApp() {
16
+ const app = express();
17
+ const uploadsDir = path.resolve(process.cwd(), "uploads");
18
+
19
+ fs.mkdirSync(path.join(uploadsDir, "complaints"), { recursive: true });
20
+
21
+ app.set("trust proxy", 1);
22
+ app.use(
23
+ cors({
24
+ origin: env.corsOrigins.split(",").map((value) => value.trim()),
25
+ }),
26
+ );
27
+ app.use(securityHeaders);
28
+ app.use(rateLimitMiddleware);
29
+ app.use(express.json({ limit: "10mb" }));
30
+ app.use("/uploads", express.static(uploadsDir));
31
+
32
+ app.get("/", (_req, res) => {
33
+ res.json({
34
+ name: "civicpulse-backend",
35
+ message: "CivicPulse backend is running",
36
+ });
37
+ });
38
+
39
+ app.use("/api/health", healthRouter);
40
+ app.use("/api/ai", aiRouter);
41
+ app.use("/api/auth", authRouter);
42
+ app.use("/api/departments", departmentRouter);
43
+ app.use("/api/domains", domainRouter);
44
+ app.use("/api/users", userRouter);
45
+ app.use("/api/complaints", complaintRouter);
46
+
47
+ return app;
48
+ }
civic-platform/backend/src/config/env.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dotenv from "dotenv";
2
+
3
+ dotenv.config();
4
+
5
+ function requireEnv(name: string, fallback?: string): string {
6
+ const value = process.env[name] ?? fallback;
7
+
8
+ if (!value) {
9
+ throw new Error(`Missing required environment variable: ${name}`);
10
+ }
11
+
12
+ return value;
13
+ }
14
+
15
+ export const env = {
16
+ nodeEnv: requireEnv("NODE_ENV", "development"),
17
+ port: Number(requireEnv("PORT", "4000")),
18
+ corsOrigins: requireEnv("CORS_ORIGINS", "http://localhost:3000"),
19
+ dbHost: requireEnv("DB_HOST", "localhost"),
20
+ dbPort: Number(requireEnv("DB_PORT", "5432")),
21
+ dbName: requireEnv("DB_NAME", "civicpulse"),
22
+ dbUser: requireEnv("DB_USER", "postgres"),
23
+ dbPassword: requireEnv("DB_PASSWORD"),
24
+ dbSsl: requireEnv("DB_SSL", "false") === "true",
25
+ demoAuthPassword: requireEnv("DEMO_AUTH_PASSWORD", "civicpulse123"),
26
+ rateLimitWindowMs: Number(requireEnv("RATE_LIMIT_WINDOW_MS", "60000")),
27
+ rateLimitMaxRequests: Number(requireEnv("RATE_LIMIT_MAX_REQUESTS", "120")),
28
+ };
civic-platform/backend/src/db/pool.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Pool } from "pg";
2
+ import { env } from "../config/env.js";
3
+
4
+ export const db = new Pool({
5
+ host: env.dbHost,
6
+ port: env.dbPort,
7
+ database: env.dbName,
8
+ user: env.dbUser,
9
+ password: env.dbPassword,
10
+ ssl: env.dbSsl ? { rejectUnauthorized: false } : false,
11
+ });
civic-platform/backend/src/lib/audit.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { db } from "../db/pool.js";
2
+
3
+ export async function writeAuditLog(input: {
4
+ actorUserId?: string | null;
5
+ entityType: string;
6
+ entityId?: string | null;
7
+ action: string;
8
+ details?: Record<string, unknown>;
9
+ }) {
10
+ await db.query(
11
+ `
12
+ INSERT INTO audit_logs (
13
+ actor_user_id,
14
+ entity_type,
15
+ entity_id,
16
+ action,
17
+ details
18
+ )
19
+ VALUES ($1, $2, $3, $4, $5::jsonb)
20
+ `,
21
+ [
22
+ input.actorUserId ?? null,
23
+ input.entityType,
24
+ input.entityId ?? null,
25
+ input.action,
26
+ JSON.stringify(input.details ?? {}),
27
+ ],
28
+ );
29
+ }
civic-platform/backend/src/lib/cache.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ type CacheEntry<T> = {
2
+ value: T;
3
+ expiresAt: number;
4
+ };
5
+
6
+ const store = new Map<string, CacheEntry<unknown>>();
7
+
8
+ export async function withCache<T>(key: string, ttlMs: number, producer: () => Promise<T>): Promise<T> {
9
+ const now = Date.now();
10
+ const existing = store.get(key);
11
+
12
+ if (existing && existing.expiresAt > now) {
13
+ return existing.value as T;
14
+ }
15
+
16
+ const value = await producer();
17
+ store.set(key, {
18
+ value,
19
+ expiresAt: now + ttlMs,
20
+ });
21
+
22
+ return value;
23
+ }
24
+
25
+ export function clearCache(keyPrefix?: string) {
26
+ if (!keyPrefix) {
27
+ store.clear();
28
+ return;
29
+ }
30
+
31
+ for (const key of store.keys()) {
32
+ if (key.startsWith(keyPrefix)) {
33
+ store.delete(key);
34
+ }
35
+ }
36
+ }
37
+