diff --git a/api.py b/api.py index ae6578c37e1ecec32b7452791f110ebfc7eb9c11..f2947754405b7cd5d54e58afe3d4cce7c0509cd9 100644 --- a/api.py +++ b/api.py @@ -27,7 +27,7 @@ app = FastAPI() # The ALLOWED_ORIGINS env var can be set on HF Spaces to your exact Vercel URL. _raw_origins = os.environ.get( "ALLOWED_ORIGINS", - "http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,http://127.0.0.1:4173,http://localhost:3000,http://127.0.0.1:3000" + "http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,http://127.0.0.1:4173,http://localhost:3000,http://127.0.0.1:3000,https://praveendatascience-crowd-detection.hf.space" ) ALLOWED_ORIGINS = [o.strip() for o in _raw_origins.split(",") if o.strip()] diff --git a/civic-platform/.env.example b/civic-platform/.env.example deleted file mode 100644 index 460ce9fc8c213819257d84ca807702b5ff7487a0..0000000000000000000000000000000000000000 --- a/civic-platform/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -NEXT_PUBLIC_API_BASE_URL=http://localhost:4000/api - -# Optional. If set, the public map uses Mapbox streets tiles. -# Leave blank to use free OpenStreetMap tiles. -MAPBOX_TOKEN= -NEXT_PUBLIC_MAPBOX_TOKEN= diff --git a/civic-platform/.gitignore b/civic-platform/.gitignore deleted file mode 100644 index 288e0233a6e304a0c50066b16ede4a509e6d4d28..0000000000000000000000000000000000000000 --- a/civic-platform/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/ -.next/ -dist/ -.env -.env.local -backend/.env -backend/dist/ diff --git a/civic-platform/README.md b/civic-platform/README.md deleted file mode 100644 index d28f152cc395e7f6b5c1ad8c4d28f83c118a03ce..0000000000000000000000000000000000000000 --- a/civic-platform/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# Civic Platform - -A mobile-first civic issue reporting and resolution platform for day-to-day public problems and emergency incidents. - -## Vision - -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. - -## Core goals - -- Make complaint reporting simple and fast -- Support day-to-day civic domains and emergency domains -- Route complaints to the correct department with priority -- Provide transparent status tracking to citizens -- Give officials a fast dashboard for triage, assignment, and monitoring -- Keep the system scalable, low-latency, and future-ready - -## Main domains - -1. Roads and Transportation -2. Sanitation and Waste Management -3. Water Supply, Sewerage, and Drainage -4. Street Lighting and Electrical Infrastructure -5. Public Infrastructure and Amenities -6. Environment and Public Health - -## Emergency domains - -1. Fire Emergencies -2. Flood and Water Disaster -3. Structural and Infrastructure Hazard -4. Electrical Hazard -5. Disaster and Rescue - -## Complaint lifecycle - -1. Draft -2. Submitted -3. Validated -4. Classified -5. Prioritized -6. Assigned -7. Accepted -8. In Progress -9. Resolved -10. Citizen Verified -11. Closed - -Additional states: - -- Escalated -- Reopened -- Duplicate -- Rejected -- On Hold - -## Proposed stack - -### Frontend - -- Next.js -- TypeScript -- Tailwind CSS -- shadcn/ui -- Framer Motion -- Leaflet with OpenStreetMap - -### Platform - -- Supabase Auth -- Supabase Postgres -- PostGIS -- Supabase Storage -- Supabase Realtime -- Supabase Edge Functions - -### AI - -- Whisper for speech-to-text -- DistilBERT or similar lightweight text classifier -- YOLOv8n or MobileNet for image classification - -## App areas - -- Citizen portal -- Department dashboard -- Field officer workspace -- Super admin configuration -- Analytics and SLA dashboard - -## Folder plan - -- `app/` Next.js App Router pages -- `components/` shared UI sections -- `lib/` config, types, and helpers -- `data/` static seed content used by the frontend -- `docs/` architecture, lifecycle, and page planning - -## Planning docs - -- `docs/PROJECT_BLUEPRINT.md` -- `docs/FRONTEND_PLAN.md` -- `docs/PAGE_MAP.md` -- `docs/ROLES_AND_PERMISSIONS.md` -- `docs/COMPLAINT_LIFECYCLE.md` -- `docs/DOMAIN_ROUTING_MAP.md` -- `docs/PRIORITY_SCORING.md` -- `docs/DATABASE_SCHEMA.md` -- `docs/BACKEND_STRUCTURE.md` -- `docs/LOCAL_DATABASE_SETUP.md` - -## Next build steps - -1. Finalize data model and routing rules -2. Build the design system and page structure -3. Implement citizen report flow -4. Add admin dashboard and complaint lifecycle updates -5. Connect Supabase auth, database, storage, and realtime -6. Add AI-assisted classification after the MVP workflow is stable diff --git a/civic-platform/ai/README.md b/civic-platform/ai/README.md deleted file mode 100644 index 59b57eac67a70a9ec7643746fdf445f1e502aad2..0000000000000000000000000000000000000000 --- a/civic-platform/ai/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# CivicPulse AI Workspace - -This folder is reserved for model training assets, dataset schemas, experiments, and evaluation results. - -Recommended subfolders: - -- `datasets/text` -- `datasets/vision` -- `datasets/audio` -- `evals` -- `notebooks` -- `schemas` - -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. - diff --git a/civic-platform/ai/schemas/civic-image-record.example.json b/civic-platform/ai/schemas/civic-image-record.example.json deleted file mode 100644 index e83b0cd2870c749c9d931df637cb07ccbd92f2f9..0000000000000000000000000000000000000000 --- a/civic-platform/ai/schemas/civic-image-record.example.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "image": "path-or-huggingface-image-object", - "label": "Roads and Transportation", - "metadata": { - "complaintCode": "CP-2026-000001", - "source": "citizen_upload", - "city": "Hyderabad", - "latitude": 17.385, - "longitude": 78.4867 - } -} diff --git a/civic-platform/ai/schemas/complaint-text-record.example.json b/civic-platform/ai/schemas/complaint-text-record.example.json deleted file mode 100644 index 3a9e051e87b350970824463dca79c8ac5845ba48..0000000000000000000000000000000000000000 --- a/civic-platform/ai/schemas/complaint-text-record.example.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "Streetlight not working near school gate", - "description": "Two streetlights are off near the main gate and the road is dark at night.", - "address_line": "School Road", - "landmark": "Near Government High School", - "city_name": "Sample City", - "state_name": "Sample State", - "domain": "Street Lighting and Electrical Infrastructure", - "sub_problem": "Broken streetlight", - "priority": "P2", - "is_emergency": false -} diff --git a/civic-platform/ai/training/README.md b/civic-platform/ai/training/README.md deleted file mode 100644 index 80b5f966ae97bbafeaa35a07dc31c18a70387063..0000000000000000000000000000000000000000 --- a/civic-platform/ai/training/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# CivicPulse Vision Training Plan - -The current app has a safe baseline for image evidence: - -- It extracts GPS metadata from JPEG photos when available. -- It uses address search, GPS, or map pinning when photo GPS metadata is missing. -- It passes lightweight visual signals into the AI assistant when filenames contain civic issue hints. - -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. - -## Recommended Labels - -Use the same project domains as labels: - -- Roads and Transportation -- Sanitation and Waste Management -- Water Supply, Sewerage, and Drainage -- Street Lighting and Electrical Infrastructure -- Public Infrastructure and Amenities -- Environment and Public Health -- Fire Emergencies -- Flood and Water Disaster -- Structural and Infrastructure Hazard -- Electrical Hazard -- Disaster and Rescue - -## Dataset Format - -For Hugging Face image classification, publish a dataset with: - -- `image`: image column -- `label`: one of the labels above - -Example record: - -[civic-image-record.example.json](/C:/Users/Praveen/Documents/New%20project/civic-platform/ai/schemas/civic-image-record.example.json) - -## Training Recommendation - -Start with image classification: - -- Model: `timm/mobilenetv3_small_100.lamb_in1k` for fast iteration -- Upgrade: `timm/resnet50.a1_in1k` or `timm/vit_base_patch16_dinov3.lvd1689m` for better accuracy -- Target metric: validation accuracy and per-class recall -- Minimum data target: 300 to 500 images per label for a useful first model -- Better data target: 1,000+ images per label with different lighting, angles, weather, and phones - -For localization such as detecting a pothole box inside the image, move to object detection later with D-FINE or RT-DETR. - -## Deployment Path - -1. Collect and label civic images from real reports. -2. Upload the dataset to Hugging Face Hub. -3. Validate the dataset format before training. -4. Fine-tune an image classifier using Hugging Face Jobs. -5. Push the trained model to the Hub. -6. Add a backend inference endpoint that calls the trained model and returns: - - predicted domain - - confidence - - top 3 alternatives - - model version -7. Keep the operator override flow because image models can be wrong. - -## Important Note - -Do not rely only on image classification. The best production result should combine: - -- image prediction -- text/voice description -- issue domain chosen by the citizen -- geo-location and ward -- nearby duplicate complaints -- operator override diff --git a/civic-platform/ai/training/civic-image-labels.md b/civic-platform/ai/training/civic-image-labels.md deleted file mode 100644 index ecb59fce9e3fe29c5579b298f112f28e070432c8..0000000000000000000000000000000000000000 --- a/civic-platform/ai/training/civic-image-labels.md +++ /dev/null @@ -1,19 +0,0 @@ -# CivicPulse Image Labels - -Use these labels for the first image classification dataset. - -| Label | Example visual cases | -|---|---| -| Roads and Transportation | potholes, broken roads, road blockage, damaged footpath, damaged divider | -| Sanitation and Waste Management | garbage piles, overflowing bins, illegal dumping, dirty streets | -| Water Supply, Sewerage, and Drainage | water leakage, sewage overflow, clogged drains, waterlogging | -| Street Lighting and Electrical Infrastructure | broken streetlights, damaged poles, exposed wires, transformer issues | -| Public Infrastructure and Amenities | open manholes, broken bus stops, damaged public toilets, park damage | -| Environment and Public Health | fallen trees, stagnant water, smoke, dead animals, mosquito breeding areas | -| Fire Emergencies | visible fire, heavy smoke, burning debris | -| Flood and Water Disaster | floodwater, severe rain overflow, storm damage | -| Structural and Infrastructure Hazard | wall cracks, building collapse signs, bridge damage | -| Electrical Hazard | live wires, transformer blast, major short circuit evidence | -| Disaster and Rescue | landslide, storm collapse, road-blocking tree fall, rescue hazard | - -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. diff --git a/civic-platform/app/api/session/login/route.ts b/civic-platform/app/api/session/login/route.ts deleted file mode 100644 index 0494a20a8f696d997018f2be50d2c2f5f6a87106..0000000000000000000000000000000000000000 --- a/civic-platform/app/api/session/login/route.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; -import bcrypt from "bcryptjs"; -import pool from "@/lib/db"; -import { encodeSession, SESSION_COOKIE_NAME, type SessionUser } from "@/lib/session"; - -type UserRow = SessionUser & { passwordHash: string | null }; - -export async function POST(request: NextRequest) { - try { - const body = (await request.json()) as { email?: string; password?: string }; - const email = body.email?.trim().toLowerCase() ?? ""; - const password = body.password ?? ""; - - if (!email) { - return NextResponse.json({ error: "Email is required" }, { status: 400 }); - } - if (!password) { - return NextResponse.json({ error: "Password is required" }, { status: 400 }); - } - - // Query users table joined with roles - const result = await pool.query( - `SELECT - u.id, - u.full_name AS "fullName", - u.email, - r.name AS role, - u.department_id AS "departmentId", - u.ward_id AS "wardId", - u.password_hash AS "passwordHash" - FROM users u - INNER JOIN roles r ON r.id = u.role_id - WHERE lower(u.email) = $1 - AND u.is_active = TRUE - LIMIT 1`, - [email], - ); - - const user = result.rows[0] ?? null; - - if (!user) { - return NextResponse.json({ error: "Invalid email or password" }, { status: 401 }); - } - - // Support both real bcrypt hashes AND the dev-placeholder-hash used in seed data - let passwordValid = false; - if (user.passwordHash === "dev-placeholder-hash" && password === "civicpulse123") { - passwordValid = true; - } else if (user.passwordHash) { - passwordValid = await bcrypt.compare(password, user.passwordHash); - } - - if (!passwordValid) { - return NextResponse.json({ error: "Invalid email or password" }, { status: 401 }); - } - - const { passwordHash: _ph, ...sessionUser } = user; - const cookieStore = await cookies(); - - cookieStore.set(SESSION_COOKIE_NAME, encodeSession(sessionUser), { - httpOnly: true, - sameSite: "lax", - path: "/", - secure: process.env.NODE_ENV === "production", - maxAge: 60 * 60 * 12, - }); - - return NextResponse.json({ data: sessionUser }); - } catch (error) { - console.error("[login] Error:", error); - return NextResponse.json( - { error: error instanceof Error ? error.message : "Login failed" }, - { status: 500 }, - ); - } -} diff --git a/civic-platform/app/api/session/logout/route.ts b/civic-platform/app/api/session/logout/route.ts deleted file mode 100644 index 4af07410ec4eff0eee8a10642db7b7ff8e773454..0000000000000000000000000000000000000000 --- a/civic-platform/app/api/session/logout/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { cookies } from "next/headers"; -import { NextResponse } from "next/server"; -import { SESSION_COOKIE_NAME } from "@/lib/session"; - -export async function POST() { - const cookieStore = await cookies(); - cookieStore.delete(SESSION_COOKIE_NAME); - - return NextResponse.json({ data: { ok: true } }); -} diff --git a/civic-platform/app/api/session/register/route.ts b/civic-platform/app/api/session/register/route.ts deleted file mode 100644 index 8b3462bf2cb51b538057b7b41b103f284b15e67d..0000000000000000000000000000000000000000 --- a/civic-platform/app/api/session/register/route.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; -import bcrypt from "bcryptjs"; -import pool from "@/lib/db"; -import { encodeSession, SESSION_COOKIE_NAME, type SessionUser } from "@/lib/session"; - -type UserRow = SessionUser & { passwordHash: string | null }; - -export async function POST(request: NextRequest) { - try { - const body = (await request.json()) as { - fullName?: string; - email?: string; - phone?: string; - password?: string; - }; - - const fullName = body.fullName?.trim() ?? ""; - const email = body.email?.trim().toLowerCase() ?? ""; - const phone = body.phone?.trim() || null; - const password = body.password ?? ""; - - // Validation - if (!fullName) { - return NextResponse.json({ error: "Full name is required" }, { status: 400 }); - } - if (!email) { - return NextResponse.json({ error: "Email is required" }, { status: 400 }); - } - if (!password || password.length < 8) { - return NextResponse.json({ error: "Password must be at least 8 characters" }, { status: 400 }); - } - - // Check if email already exists - const existing = await pool.query("SELECT id FROM users WHERE lower(email) = $1", [email]); - if (existing.rows.length > 0) { - return NextResponse.json({ error: "An account already exists for this email" }, { status: 409 }); - } - - // Hash password - const passwordHash = await bcrypt.hash(password, 12); - - // Look up citizen role ID - const roleResult = await pool.query("SELECT id FROM roles WHERE name = 'citizen' LIMIT 1"); - const roleId = roleResult.rows[0]?.id; - if (!roleId) { - return NextResponse.json({ error: "Citizen role is not configured in the database" }, { status: 500 }); - } - - // Insert new user - const insertResult = await pool.query( - `INSERT INTO users (full_name, email, phone, password_hash, role_id) - VALUES ($1, $2, $3, $4, $5) - RETURNING - id, - full_name AS "fullName", - email, - 'citizen' AS role, - department_id AS "departmentId", - ward_id AS "wardId", - password_hash AS "passwordHash"`, - [fullName, email, phone, passwordHash, roleId], - ); - - const user = insertResult.rows[0]; - const { passwordHash: _ph, ...sessionUser } = user; - - const cookieStore = await cookies(); - cookieStore.set(SESSION_COOKIE_NAME, encodeSession(sessionUser), { - httpOnly: true, - sameSite: "lax", - path: "/", - secure: process.env.NODE_ENV === "production", - maxAge: 60 * 60 * 12, - }); - - return NextResponse.json({ data: sessionUser }, { status: 201 }); - } catch (error) { - console.error("[register] Error:", error); - const message = error instanceof Error ? error.message : "Registration failed"; - return NextResponse.json({ error: message }, { status: 500 }); - } -} diff --git a/civic-platform/app/complaints/[id]/page.tsx b/civic-platform/app/complaints/[id]/page.tsx deleted file mode 100644 index dc591238e337cdb75a9a89b36b7c35d860739cf6..0000000000000000000000000000000000000000 --- a/civic-platform/app/complaints/[id]/page.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import Link from "next/link"; -import { AlertTriangle, Clock3, MapPinned, ShieldCheck, Waves } from "lucide-react"; -import { AppShell } from "@/components/app-shell"; -import { CitizenFollowUpPanel } from "@/components/citizen-follow-up-panel"; -import { ComplaintMediaGallery } from "@/components/complaint-media-gallery"; -import { SectionHeader } from "@/components/section-header"; -import { requireRole } from "@/lib/auth"; -import { - getComplaintDetail, - getComplaintFeedback, - getComplaintHistory, - getComplaintMedia, - type ComplaintFeedbackItem, - type ComplaintMediaItem, - type ComplaintStatusHistoryItem, -} from "@/lib/api"; - -type ComplaintDetailPageProps = { - params: Promise<{ - id: string; - }>; -}; - -function formatStatus(status: string) { - return status - .split("_") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" "); -} - -function formatPriority(priority: string) { - switch (priority) { - case "P1": - return "P1 Critical"; - case "P2": - return "P2 High"; - case "P3": - return "P3 Medium"; - default: - return "P4 Low"; - } -} - -function formatDateTime(value: string) { - return new Date(value).toLocaleString(); -} - -function timelineTone(status: string) { - switch (status) { - case "submitted": - return "bg-civic-primary"; - case "validated": - return "bg-civic-secondary"; - case "classified": - case "prioritized": - return "bg-amber-500"; - case "assigned": - return "bg-slate-800"; - case "in_progress": - return "bg-emerald-600"; - case "resolved": - case "closed": - return "bg-emerald-700"; - case "reopened": - case "escalated": - return "bg-civic-danger"; - default: - return "bg-slate-300"; - } -} - -export default async function ComplaintDetailPage({ params }: ComplaintDetailPageProps) { - await requireRole(["citizen"]); - const { id } = await params; - - let complaintStatus = "In Progress"; - let complaintPriority = "P2 High"; - let complaintLocation = "Market Road, Ward 12"; - let complaintDomain = "Water Supply, Sewerage, and Drainage"; - let complaintDescription = "Overflow affecting entry and traffic movement near the market."; - let complaintCode = "CP-2026-00124"; - let complaintDepartment = "Water and Sewerage"; - let media: ComplaintMediaItem[] = []; - let feedback: ComplaintFeedbackItem[] = []; - let rawStatus = "in_progress"; - let citizenId = "00000000-0000-0000-0000-000000000001"; - let timeline: Array<{ title: string; time: string; note: string; tone: string }> = [ - { - title: "Submitted", - time: "Pending", - note: "Complaint created by citizen with image, description, and geo-tagged location.", - tone: "bg-civic-primary", - }, - ]; - - try { - const [complaint, history, mediaItems, feedbackItems] = await Promise.all([ - getComplaintDetail(id), - getComplaintHistory(id), - getComplaintMedia(id), - getComplaintFeedback(id), - ]); - - rawStatus = complaint.status; - complaintStatus = formatStatus(complaint.status); - complaintPriority = formatPriority(complaint.priorityLevel); - complaintLocation = complaint.addressLine ?? complaint.landmark ?? "Location pending"; - complaintDomain = complaint.domainName ?? "Unclassified"; - complaintDescription = complaint.description ?? complaintDescription; - complaintCode = complaint.complaintCode; - complaintDepartment = complaint.departmentName ?? complaintDepartment; - citizenId = complaint.citizenId; - media = mediaItems; - feedback = feedbackItems; - - if (history.length > 0) { - timeline = history.map((entry: ComplaintStatusHistoryItem) => ({ - title: formatStatus(entry.newStatus), - time: formatDateTime(entry.createdAt), - note: entry.changeReason ?? `Complaint moved to ${formatStatus(entry.newStatus)}.`, - tone: timelineTone(entry.newStatus), - })); - } - } catch { - // Keep fallback values for design flow when backend is unavailable. - } - - return ( - -
-
-

Complaint detail

-

Track your complaint in one place.

-

- Check the current status, department ownership, attached evidence, and any updates from the city team. -

-
- -
- {[ - { label: "Complaint ID", value: complaintCode }, - { label: "Current status", value: complaintStatus }, - { label: "Priority", value: complaintPriority }, - { label: "Assigned department", value: complaintDepartment }, - ].map((card) => ( -
-

{card.label}

-

{card.value}

-
- ))} -
-
- -
-
-
-
-

Timeline

-

Status history

-
-
- Current state: {complaintStatus} -
-
- -
- {timeline.map((entry, index) => ( -
-
-
- {index !== timeline.length - 1 ?
: null} -
-
-
-

{entry.title}

-
- - {entry.time} -
-
-

{entry.note}

-
-
- ))} -
-
- -
-
- - -
- {[complaintLocation, `Domain: ${complaintDomain}`, complaintDescription].map((item) => ( -
- {item} -
- ))} -
-
- - - - -
-
- -
-
-
- -
-

What you can see here

-

- This page shows your complaint progress and public-safe updates without exposing internal staff notes. -

-
-
-
- -
-
- -
-

Need more help?

-

- If the issue is marked resolved but still exists, use the reopen option below so the team can review it again. -

-
-
-
-
- -
-
-

Quick actions

-

Keep track of this report or return to your list.

-

- You can go back to all complaints, stay on this page for updates, or report another issue if needed. -

-
- -
- - Back to complaints - - - Report another issue - - -
-
-
- ); -} diff --git a/civic-platform/app/complaints/page.tsx b/civic-platform/app/complaints/page.tsx deleted file mode 100644 index 8f34cc135e0a9ee8fc025fd471f3e81b04974bff..0000000000000000000000000000000000000000 --- a/civic-platform/app/complaints/page.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import Link from "next/link"; -import { Clock3, MapPinned, Search } from "lucide-react"; -import { AppShell } from "@/components/app-shell"; -import { PriorityBadge } from "@/components/priority-badge"; -import { StatusBadge } from "@/components/status-badge"; -import { getCitizenComplaints } from "@/lib/api"; -import { requireRole } from "@/lib/auth"; - -const complaintStats = [ - { label: "Active", value: "04" }, - { label: "Resolved", value: "09" }, - { label: "Reopened", value: "01" }, -]; - -type ComplaintCard = { - id: string; - complaintCode?: string; - title: string; - domain: string; - status: string; - priority: string; - location: string; - updatedAt: string; -}; - -const fallbackComplaints: ComplaintCard[] = [ - { - id: "CP-2026-00124", - complaintCode: "CP-2026-00124", - title: "Sewage overflow near market entrance", - domain: "Water Supply, Sewerage, and Drainage", - status: "In Progress", - priority: "P2 High", - location: "Market Road, Ward 12", - updatedAt: "Updated 1 hour ago", - }, - { - id: "CP-2026-00108", - complaintCode: "CP-2026-00108", - title: "Streetlight not working on school road", - domain: "Street Lighting and Electrical Infrastructure", - status: "Assigned", - priority: "P3 Medium", - location: "School Street, Ward 8", - updatedAt: "Updated today", - }, - { - id: "CP-2026-00097", - complaintCode: "CP-2026-00097", - title: "Overflowing garbage bin near bus stop", - domain: "Sanitation and Waste Management", - status: "Resolved", - priority: "P3 Medium", - location: "Central Bus Stop, Ward 3", - updatedAt: "Resolved yesterday", - }, - { - id: "CP-2026-00091", - complaintCode: "CP-2026-00091", - title: "Open manhole beside community hall", - domain: "Public Infrastructure and Amenities", - status: "Reopened", - priority: "P1 Critical", - location: "Community Hall Lane, Ward 5", - updatedAt: "Reopened 3 hours ago", - }, -]; - -const filters = ["All", "Active", "Resolved", "Reopened", "Emergency"]; - -function formatStatus(status: string) { - return status - .split("_") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" "); -} - -function formatPriority(priority: string) { - switch (priority) { - case "P1": - return "P1 Critical"; - case "P2": - return "P2 High"; - case "P3": - return "P3 Medium"; - default: - return "P4 Low"; - } -} - -export default async function ComplaintsPage() { - const user = await requireRole(["citizen"]); - let complaints: ComplaintCard[] = fallbackComplaints; - - try { - const apiComplaints = await getCitizenComplaints(user.id); - - if (apiComplaints.length > 0) { - complaints = apiComplaints.map((complaint) => ({ - id: complaint.id, - title: complaint.title, - domain: complaint.domainName ?? "Unclassified", - status: formatStatus(complaint.status), - priority: formatPriority(complaint.priorityLevel), - location: complaint.departmentName ?? "Department pending", - updatedAt: new Date(complaint.submittedAt).toLocaleString(), - complaintCode: complaint.complaintCode, - })); - } - } catch { - complaints = fallbackComplaints; - } - - return ( - -
-
- -
-
-

My Reports

-

Track the status of the issues you've reported in your neighborhood.

-
- - Report a new issue - -
- -
- {complaintStats.map((stat) => ( -
-

{stat.label}

-

{stat.value}

-
- ))} -
-
- -
-
-
-
- {filters.map((filter) => ( - - ))} -
- -
- - Search by complaint ID or area -
-
-
- -
-

Recent Activity

- -
- {complaints.map((complaint) => ( -
-
-
-
- - {complaint.complaintCode ?? complaint.id} - - - -
-

{complaint.title}

-

{complaint.domain}

-
- -
- - {complaint.updatedAt} -
-
- -
- - {complaint.location} -
- -
- - View Details - -
-
- ))} -
-
-
-
- ); -} diff --git a/civic-platform/app/dashboard/analytics/page.tsx b/civic-platform/app/dashboard/analytics/page.tsx deleted file mode 100644 index 9efba8a1fc746a7fe22fc9de5ae58259f9fb2d23..0000000000000000000000000000000000000000 --- a/civic-platform/app/dashboard/analytics/page.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import Link from "next/link"; -import { AppShell } from "@/components/app-shell"; -import { EmptyState } from "@/components/empty-state"; -import { LiveSyncBadge } from "@/components/live-sync-badge"; -import { requireRole } from "@/lib/auth"; -import { getAnalyticsExportUrl, getAnalyticsSummary, type AnalyticsSummary } from "@/lib/api"; -import { ArrowDownToLine, BarChart3, Clock3, Siren, TrendingUp } from "lucide-react"; - -const fallbackAnalytics: AnalyticsSummary = { - trends: [], - domainBreakdown: [], - departmentPerformance: [], - kpis: { - emergencyCount: 0, - reopenedCount: 0, - repeatComplaintRiskCount: 0, - slaBreaches: 0, - }, -}; - -export default async function DashboardAnalyticsPage() { - await requireRole(["department_operator", "municipal_admin"]); - - let analytics = fallbackAnalytics; - - try { - analytics = await getAnalyticsSummary(); - } catch { - analytics = fallbackAnalytics; - } - - const maxTrend = Math.max( - 1, - ...analytics.trends.flatMap((item) => [item.reportedCount, item.resolvedCount]), - ); - - return ( - -
-
-
-

Analytics

-

Track performance, trends, and launch readiness.

-

- Use this view to monitor complaint trends, department throughput, SLA risk, and repeat civic failures. -

-
- -
-
- -
- {[ - { label: "Emergency", value: analytics.kpis.emergencyCount, icon: Siren }, - { label: "Reopened", value: analytics.kpis.reopenedCount, icon: TrendingUp }, - { label: "Repeat risk", value: analytics.kpis.repeatComplaintRiskCount, icon: BarChart3 }, - { label: "SLA breaches", value: analytics.kpis.slaBreaches, icon: Clock3 }, - ].map((item) => { - const Icon = item.icon; - return ( -
-
- -
-

{item.label}

-

{item.value}

-
- ); - })} -
- -
-
-
-

Trends

-

Reported vs resolved over the last 7 days

-
-
- {analytics.trends.length === 0 ? ( - - ) : ( - analytics.trends.map((item) => ( -
-
- {item.day} - - reported {item.reportedCount} · resolved {item.resolvedCount} - -
-
-
-
-
-
-
-
-
-
- )) - )} -
-
- -
-
-

Domains

-

Complaint share by category

-
-
- {analytics.domainBreakdown.length === 0 ? ( - - ) : ( - analytics.domainBreakdown.map((item) => ( -
-
- {item.domainName} - - {item.complaintCount} - -
-
- )) - )} -
-
-
- -
-
-
-

Departments

-

Performance and response time

-
- - Back to overview - -
- -
- - - - - - - - - - - {analytics.departmentPerformance.map((item) => ( - - - - - - - ))} - -
DepartmentPendingResolvedAvg resolution (hrs)
{item.departmentName}{item.pendingCount}{item.resolvedCount}{item.averageResolutionHours.toFixed(2)}
-
-
-
- ); -} diff --git a/civic-platform/app/dashboard/complaints/[id]/page.tsx b/civic-platform/app/dashboard/complaints/[id]/page.tsx deleted file mode 100644 index fd89d91c74c7f7056a3edad029d20c66ea7d0083..0000000000000000000000000000000000000000 --- a/civic-platform/app/dashboard/complaints/[id]/page.tsx +++ /dev/null @@ -1,383 +0,0 @@ -import Link from "next/link"; -import { - ArrowRight, - CheckCircle2, - ClipboardList, - Clock3, - MessageSquareText, - ShieldAlert, - UserCog, -} from "lucide-react"; -import { AdminAssignAction } from "@/components/admin-assign-action"; -import { AdminAssignmentHistory } from "@/components/admin-assignment-history"; -import { AdminNotesPanel } from "@/components/admin-notes-panel"; -import { AppShell } from "@/components/app-shell"; -import { AdminStatusActions } from "@/components/admin-status-actions"; -import { ComplaintAiPanel } from "@/components/complaint-ai-panel"; -import { ComplaintMediaGallery } from "@/components/complaint-media-gallery"; -import { ResolutionProofUploader } from "@/components/resolution-proof-uploader"; -import { SectionHeader } from "@/components/section-header"; -import { requireRole } from "@/lib/auth"; -import { - getComplaintDetail, - getComplaintAssignments, - getComplaintAiInsights, - getComplaintHistory, - getComplaintMedia, - getComplaintNotes, - type ComplaintAiInsights, - type ComplaintAssignmentItem, - type ComplaintMediaItem, - type ComplaintNoteItem, - type ComplaintStatusHistoryItem, -} from "@/lib/api"; - -const internalActions = [ - "Validate complaint", - "Adjust domain or sub-problem if needed", - "Assign responsible department", - "Assign field officer later", - "Change lifecycle state", - "Escalate if public danger is immediate", -]; - -type DashboardComplaintDetailPageProps = { - params: Promise<{ - id: string; - }>; -}; - -function formatStatus(status: string) { - return status - .split("_") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" "); -} - -function formatPriority(priority: string) { - switch (priority) { - case "P1": - return "P1 Critical"; - case "P2": - return "P2 High"; - case "P3": - return "P3 Medium"; - default: - return "P4 Low"; - } -} - -function formatDateTime(value: string) { - return new Date(value).toLocaleString(); -} - -function getPriorityRationale(priority: string, isEmergency: boolean) { - if (isEmergency || priority === "P1 Critical") { - return "Emergency or public-danger signals require immediate routing and shortest possible response time."; - } - - if (priority === "P2 High") { - return "High-impact infrastructure or safety risk needs fast ownership before it grows into a wider area failure."; - } - - if (priority === "P3 Medium") { - return "Standard operational issue with visible citizen impact. Keep it moving through the active queue."; - } - - return "Lower-impact maintenance issue that can stay in the planned service backlog unless conditions worsen."; -} - -function getRoutingConfidence(domain: string, assignments: ComplaintAssignmentItem[], department: string) { - if (assignments.length > 0) { - return `Routing has already been acted on. The latest ownership sits with ${assignments[0]?.departmentName ?? department}.`; - } - - if (domain === "Fire Emergencies" || domain === "Flood and Water Disaster" || domain === "Disaster and Rescue") { - return `Emergency domain strongly matches ${department} and should stay on the fastest escalation path.`; - } - - return `Current domain and complaint context point to ${department} as the primary queue owner.`; -} - -function getOperationalState(status: string, assignments: ComplaintAssignmentItem[]) { - if (status === "Resolved" || status === "Closed") { - return "Work appears completed. Review proof, citizen follow-up, and any reopen risk."; - } - - if (status === "Reopened" || status === "Escalated") { - return "Complaint needs renewed attention. Recheck ownership, on-ground risk, and SLA exposure."; - } - - if (assignments.length > 0) { - return "Complaint is in active departmental handling. Track acceptance and field progress closely."; - } - - return "Complaint still needs clear ownership. Assignment is the next critical operator action."; -} - -function timelineTone(status: string) { - switch (status) { - case "submitted": - return "bg-civic-primary"; - case "validated": - return "bg-civic-secondary"; - case "classified": - case "prioritized": - return "bg-amber-500"; - case "assigned": - return "bg-slate-800"; - case "in_progress": - return "bg-emerald-600"; - case "resolved": - case "closed": - return "bg-emerald-700"; - case "reopened": - case "escalated": - return "bg-civic-danger"; - default: - return "bg-slate-300"; - } -} - -export default async function DashboardComplaintDetailPage({ params }: DashboardComplaintDetailPageProps) { - const user = await requireRole(["department_operator", "municipal_admin"]); - const { id } = await params; - - let complaintCode = "CP-2026-00141"; - let complaintStatusValue = "submitted"; - let complaintStatus = "Submitted"; - let complaintPriority = "P1 Critical"; - let complaintDomain = "Public Infrastructure and Amenities"; - let complaintTitle = "Open manhole on high-traffic road"; - let complaintLocation = "Temple Junction, Ward 4"; - let complaintDescription = "High-risk public hazard affecting traffic near the junction."; - let complaintDepartment = "Municipal Maintenance"; - let isEmergency = true; - let media: ComplaintMediaItem[] = []; - let assignments: ComplaintAssignmentItem[] = []; - let notes: ComplaintNoteItem[] = []; - let aiInsights: ComplaintAiInsights | null = null; - let internalTimeline: Array<{ title: string; time: string; note: string; tone: string }> = [ - { - title: "Submitted", - time: "Pending", - note: "Complaint created by citizen with image and geo-tag.", - tone: "bg-civic-primary", - }, - ]; - - try { - const [complaint, assignmentItems, history, mediaItems, noteItems, aiData] = await Promise.all([ - getComplaintDetail(id), - getComplaintAssignments(id), - getComplaintHistory(id), - getComplaintMedia(id), - getComplaintNotes(id), - getComplaintAiInsights(id), - ]); - - complaintCode = complaint.complaintCode; - complaintStatusValue = complaint.status; - complaintStatus = formatStatus(complaint.status); - complaintPriority = formatPriority(complaint.priorityLevel); - complaintDomain = complaint.domainName ?? complaintDomain; - complaintTitle = complaint.title; - complaintLocation = complaint.addressLine ?? complaint.landmark ?? complaintLocation; - complaintDescription = complaint.description ?? complaintDescription; - complaintDepartment = complaint.departmentName ?? complaintDepartment; - isEmergency = complaint.isEmergency; - assignments = assignmentItems; - media = mediaItems; - notes = noteItems; - aiInsights = aiData; - - if (history.length > 0) { - internalTimeline = history.map((entry: ComplaintStatusHistoryItem) => ({ - title: formatStatus(entry.newStatus), - time: formatDateTime(entry.createdAt), - note: entry.changeReason ?? `Complaint moved to ${formatStatus(entry.newStatus)}.`, - tone: timelineTone(entry.newStatus), - })); - } - } catch { - // Keep fallback values when backend is unavailable. - } - - const complaintMeta = [ - { label: "Complaint ID", value: complaintCode }, - { label: "Current status", value: complaintStatus }, - { label: "Priority", value: complaintPriority }, - { label: "Domain", value: complaintDomain }, - ]; - - const complaintContext = [ - `Issue: ${complaintTitle}`, - `Location: ${complaintLocation}`, - `Likely department: ${complaintDepartment}`, - complaintDescription, - ]; - - const routingDecisionItems = [ - `Current queue owner: ${assignments[0]?.departmentName ?? complaintDepartment}`, - `Operational mode: ${isEmergency ? "Emergency escalation path" : "Standard civic workflow"}`, - `Priority rationale: ${getPriorityRationale(complaintPriority, isEmergency)}`, - `Routing confidence: ${getRoutingConfidence(complaintDomain, assignments, complaintDepartment)}`, - `Current action state: ${getOperationalState(complaintStatus, assignments)}`, - ]; - - return ( - -
-
-

Internal complaint view

-

Manage one complaint from triage to resolution.

-

- Review context, route the work, assign the right team, and move the complaint through the next action. -

-
- -
- {complaintMeta.map((item) => ( -
-

{item.label}

-

{item.value}

-
- ))} -
-
- -
-
-
- - -
- {complaintContext.map((item) => ( -
- {item} -
- ))} -
-
- -
- - -
- {internalTimeline.map((entry, index) => ( -
-
-
- {index !== internalTimeline.length - 1 ?
: null} -
-
-
-

{entry.title}

-
- - {entry.time} -
-
-

{entry.note}

-
-
- ))} -
-
- -
- - -
- -
-
- - -
- -
-
- - -
- {routingDecisionItems.map((item) => ( -
- {item} -
- ))} -
- -
- -
-
- - - -
- - -
- {internalActions.map((action) => ( -
- {action} -
- ))} -
- -
- -
-
- - - - - -
-
- -
-

Internal-only context

-

- Notes, routing context, and assignment controls stay visible only to internal teams. -

-
-
-
-
-
- -
-
-

Queue navigation

-

Jump back to the queue or move into emergency operations.

-

- Use the queue to continue triage work, or switch to emergency reporting when a fast-response situation needs immediate handling. -

-
- -
- - Back to queue - - - Open emergency reporting - - -
-
-
- ); -} diff --git a/civic-platform/app/dashboard/complaints/page.tsx b/civic-platform/app/dashboard/complaints/page.tsx deleted file mode 100644 index bb67ab5bcf4f380851435fe39b4d59f550fe62e4..0000000000000000000000000000000000000000 --- a/civic-platform/app/dashboard/complaints/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { AppShell } from "@/components/app-shell"; -import { - DashboardQueueClient, - type QueueComplaintCard, -} from "@/components/dashboard-queue-client"; -import { getAllComplaints } from "@/lib/api"; -import { requireRole } from "@/lib/auth"; - -const filterGroups = [ - { - title: "Status" as const, - options: ["All", "Submitted", "Assigned", "In Progress", "Resolved", "Reopened"], - }, - { - title: "Priority" as const, - options: ["All", "P1 Critical", "P2 High", "P3 Medium", "P4 Low"], - }, - { - title: "Domain" as const, - options: ["All", "Roads", "Sanitation", "Water", "Electrical", "Infrastructure", "Environment"], - }, -]; - -const fallbackComplaints: QueueComplaintCard[] = [ - { - id: "CP-2026-00141", - complaintCode: "CP-2026-00141", - title: "Open manhole on high-traffic road", - domain: "Public Infrastructure and Amenities", - status: "Submitted", - priority: "P1 Critical", - department: "Municipal Maintenance", - location: "Temple Junction, Ward 4", - updatedAt: "5 minutes ago", - submittedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - }, - { - id: "CP-2026-00139", - complaintCode: "CP-2026-00139", - title: "Live wire hanging near bus stop", - domain: "Electrical Hazard", - status: "Assigned", - priority: "P1 Critical", - department: "Electrical Safety Response", - location: "Bus Stand Road, Ward 9", - updatedAt: "12 minutes ago", - submittedAt: new Date(Date.now() - 12 * 60 * 1000).toISOString(), - }, - { - id: "CP-2026-00124", - complaintCode: "CP-2026-00124", - title: "Sewage overflow near market entrance", - domain: "Water Supply, Sewerage, and Drainage", - status: "In Progress", - priority: "P2 High", - department: "Water and Sewerage", - location: "Market Road, Ward 12", - updatedAt: "1 hour ago", - submittedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(), - }, -]; - -function formatStatus(status: string) { - return status - .split("_") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" "); -} - -function formatPriority(priority: string) { - switch (priority) { - case "P1": - return "P1 Critical"; - case "P2": - return "P2 High"; - case "P3": - return "P3 Medium"; - default: - return "P4 Low"; - } -} - -export default async function DashboardComplaintsPage() { - const user = await requireRole(["department_operator", "municipal_admin"]); - let complaints: QueueComplaintCard[] = fallbackComplaints; - - try { - const apiComplaints = await getAllComplaints(); - - if (apiComplaints.length > 0) { - complaints = apiComplaints.map((complaint) => ({ - id: complaint.id, - complaintCode: complaint.complaintCode, - title: complaint.title, - domain: complaint.domainName ?? "Unclassified", - status: formatStatus(complaint.status), - priority: formatPriority(complaint.priorityLevel), - department: complaint.departmentName ?? "Department pending", - location: complaint.addressLine ?? complaint.landmark ?? "Location pending", - updatedAt: new Date(complaint.submittedAt).toLocaleString(), - submittedAt: complaint.submittedAt, - })); - } - } catch { - complaints = fallbackComplaints; - } - - return ( - - - - ); -} diff --git a/civic-platform/app/dashboard/page.tsx b/civic-platform/app/dashboard/page.tsx deleted file mode 100644 index 3c73485dccd97a1a258e3af7ea62f13db5e0990f..0000000000000000000000000000000000000000 --- a/civic-platform/app/dashboard/page.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import Link from "next/link"; -import { - ArrowRight, - Building2, - Clock3, - MapPinned, - Siren, - TimerReset, -} from "lucide-react"; -import { AppShell } from "@/components/app-shell"; -import { EmptyState } from "@/components/empty-state"; -import { LiveSyncBadge } from "@/components/live-sync-badge"; -import { PriorityBadge } from "@/components/priority-badge"; -import { requireRole } from "@/lib/auth"; -import { getDashboardSummary, type DashboardSummary } from "@/lib/api"; - -const focusItems = [ - "Urgent complaints", - "Unaccepted assignments", - "SLA breaches", - "Pending field updates", -]; - -export default async function DashboardPage() { - await requireRole(["department_operator", "municipal_admin"]); - - const fallbackSummary: DashboardSummary = { - overview: { - openComplaints: 184, - criticalComplaints: 7, - slaRisk: 19, - resolvedToday: 42, - }, - urgentQueue: [ - { - id: "sample-urgent-1", - complaintCode: "CP-2026-00141", - title: "Open manhole on high-traffic road", - domainName: "Public Infrastructure and Amenities", - priorityLevel: "P1", - location: "Temple Junction, Ward 4", - submittedAt: new Date().toISOString(), - }, - { - id: "sample-urgent-2", - complaintCode: "CP-2026-00139", - title: "Live wire hanging near bus stop", - domainName: "Electrical Hazard", - priorityLevel: "P1", - location: "Bus Stand Road, Ward 9", - submittedAt: new Date().toISOString(), - }, - ], - departmentWorkload: [ - { departmentName: "Roads and Public Works", pendingCount: 31 }, - { departmentName: "Sanitation and Solid Waste", pendingCount: 28 }, - { departmentName: "Water and Sewerage", pendingCount: 44 }, - { departmentName: "Electrical and Street Lighting", pendingCount: 22 }, - ], - }; - - let summary: DashboardSummary = fallbackSummary; - - try { - summary = await getDashboardSummary(); - } catch { - summary = fallbackSummary; - } - - const overviewStats = [ - { label: "Open complaints", value: String(summary.overview.openComplaints), tone: "text-civic-text" }, - { label: "Critical", value: String(summary.overview.criticalComplaints), tone: "text-civic-danger" }, - { label: "SLA risk", value: String(summary.overview.slaRisk), tone: "text-amber-700" }, - { label: "Resolved today", value: String(summary.overview.resolvedToday), tone: "text-emerald-700" }, - ]; - - return ( - -
-
-
-
-

Operations

-

Command Center

-
-
- - - Analytics - - - - Open Queue - - -
-
- -
- {overviewStats.map((stat) => ( -
-

{stat.label}

-

{stat.value}

-
- ))} -
-
- -
-
-
-
-

Urgent queue

-

Priority Incidents

-
-
- - Live Feed -
-
- -
- {summary.urgentQueue.length === 0 ? ( - - ) : ( - summary.urgentQueue.map((item) => ( -
-
-
-
-
- - {item.complaintCode} - - -
-

{item.title}

-

{item.domainName ?? "Unclassified"}

-
- -
- {new Date(item.submittedAt).toLocaleString()} -
-
- -
-
- - {item.location ?? "Location pending"} -
-
- -
- - Open Incident - - -
-
- )) - )} -
-
- -
-
-
-
- -
-
-

Departments

-

Current Workload

-
-
- -
- {summary.departmentWorkload.length === 0 ? ( - - ) : ( - summary.departmentWorkload.map((item) => ( -
-
-

{item.departmentName}

- - {item.pendingCount} pending - -
-
- )) - )} -
-
- -
-
-
- -
-
-

Focus

-

Immediate Attention

-
-
- -
- {focusItems.map((item) => ( -
- {item} -
- ))} -
-
- -
-
-
- -
-
-

Queue

-

Continue Operations

-
-
- -
- - Go to Complaint Queue - - -
-
-
-
-
- ); -} diff --git a/civic-platform/app/emergency/page.tsx b/civic-platform/app/emergency/page.tsx deleted file mode 100644 index 60c70024243cb46669578a60ed628ee7da1acc29..0000000000000000000000000000000000000000 --- a/civic-platform/app/emergency/page.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import Link from "next/link"; -import { AlertTriangle, Flame, RadioTower, ShieldAlert, Siren, Waves, Zap } from "lucide-react"; -import { AppShell } from "@/components/app-shell"; - -const emergencyTypes = [ - { - title: "Fire", - summary: "Fire outbreak, smoke, electrical fire, or public flame hazard.", - icon: Flame, - }, - { - title: "Flood", - summary: "Flooding, dangerous water accumulation, or rapid overflow.", - icon: Waves, - }, - { - title: "Electrical Hazard", - summary: "Live wire, transformer blast, or severe public electrical risk.", - icon: Zap, - }, - { - title: "Rescue / Disaster", - summary: "Collapse, storm damage, trapped people, or multi-agency incident.", - icon: RadioTower, - }, -]; - -const emergencyRules = [ - "Emergency reports should take fewer steps than standard complaints.", - "Location must be captured first or confirmed manually.", - "Priority should default to P1 unless the complaint is invalid.", - "The system should route directly to the emergency queue.", -]; - -const minimalFields = [ - "Emergency type", - "Photo or short video", - "Current location or exact pin", - "Short description of danger", - "Optional contact number", -]; - -export default function EmergencyPage() { - return ( - -
-
-
- - Emergency reporting flow -
-

Use the fastest possible flow for urgent public danger.

-

- Report fire, flood, electrical danger, or rescue situations with the fewest possible steps so the response team can act quickly. -

-
- -
-

Default response behavior

-
- {[ - "Priority set to P1 Critical", - "Emergency queue routing", - "Immediate operator visibility", - "Stronger notification path", - ].map((item) => ( -
- {item} -
- ))} -
-
-
- -
-
-
-
-

Emergency type

-

Choose the urgent incident category

-
-
- High priority flow -
-
- -
- {emergencyTypes.map(({ title, summary, icon: Icon }) => ( - - ))} -
-
- -
-
-
-
- -
-
-

Form scope

-

Minimal fields for emergency submission

-
-
- -
- {minimalFields.map((field) => ( -
- {field} -
- ))} -
-
- -
-
- -
-

Emergency reporting principles

-

- This flow stays shorter than normal reporting and focuses only on what responders need first. -

-
-
- -
- {emergencyRules.map((rule) => ( -
- {rule} -
- ))} -
-
-
-
- -
-
-

Switch reporting modes

-

Return to the normal report flow or operations dashboard.

-

- Use the standard reporting flow for non-emergency complaints, or return to the dashboard to monitor active work. -

-
- -
- - Back to normal report flow - - - Return to dashboard - -
-
-
- ); -} diff --git a/civic-platform/app/globals.css b/civic-platform/app/globals.css deleted file mode 100644 index 0858db955ecc665b319bb4f39bce8770deeb34ba..0000000000000000000000000000000000000000 --- a/civic-platform/app/globals.css +++ /dev/null @@ -1,61 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); -@import "mapbox-gl/dist/mapbox-gl.css"; - -:root { - color-scheme: light; -} - -html { - scroll-behavior: smooth; -} - -body { - margin: 0; - min-height: 100vh; - background: - radial-gradient(circle at top left, rgba(20, 184, 166, 0.08), transparent 45%), - radial-gradient(circle at bottom right, rgba(15, 76, 129, 0.08), transparent 40%), - linear-gradient(to bottom, #f8fafc, #f1f5f9); - background-attachment: fixed; - color: #0f172a; - font-family: 'Inter', Arial, Helvetica, sans-serif; - letter-spacing: -0.015em; - overflow-x: hidden; - -webkit-font-smoothing: antialiased; -} - -/* Premium Scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(100, 116, 139, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(100, 116, 139, 0.5); -} - -* { - box-sizing: border-box; -} - -a { - color: inherit; - text-decoration: none; -} - -a { - color: inherit; - text-decoration: none; -} diff --git a/civic-platform/app/layout.tsx b/civic-platform/app/layout.tsx deleted file mode 100644 index 83652f926bc45e283d076bec8636869006e6019b..0000000000000000000000000000000000000000 --- a/civic-platform/app/layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { Metadata } from "next"; -import "./globals.css"; - -export const metadata: Metadata = { - title: "Civic Platform", - description: "Crowdsourced civic issue reporting and resolution system", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/civic-platform/app/login/page.tsx b/civic-platform/app/login/page.tsx deleted file mode 100644 index 7351acb0f1ead2490080bc546ede19b7d2b9378e..0000000000000000000000000000000000000000 --- a/civic-platform/app/login/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { redirect } from "next/navigation"; -import { ShieldCheck } from "lucide-react"; -import { AppShell } from "@/components/app-shell"; -import { LoginForm } from "@/components/login-form"; -import { getCurrentUser } from "@/lib/auth"; - -export default async function LoginPage() { - const user = await getCurrentUser(); - - if (user) { - redirect(user.role === "citizen" ? "/complaints" : user.role === "field_officer" ? "/tasks" : "/dashboard"); - } - - return ( - -
-
-
- -
-

- Welcome back -

-

- Sign in or create an account to participate -

-
- -
- {/* Subtle background glow */} -
- -
- -
-
-
-
- ); -} diff --git a/civic-platform/app/map/page.tsx b/civic-platform/app/map/page.tsx deleted file mode 100644 index 2600c97b65df9d73ead503511f55e6a80b080e79..0000000000000000000000000000000000000000 --- a/civic-platform/app/map/page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { AppShell } from "@/components/app-shell"; -import { IssueMapBoard } from "@/components/issue-map-board"; -import { getCurrentUser } from "@/lib/auth"; -import { - getMapComplaints, - getPublicHotspots, - type ComplaintHotspotItem, - type ComplaintMapItem, -} from "@/lib/api"; - -const fallbackComplaints: ComplaintMapItem[] = []; -const fallbackHotspots: ComplaintHotspotItem[] = []; - -export default async function MapPage() { - const user = await getCurrentUser(); - const mapboxToken = process.env.MAPBOX_TOKEN ?? process.env.NEXT_PUBLIC_MAPBOX_TOKEN; - const isOperationsView = - user?.role === "department_operator" || user?.role === "municipal_admin" || user?.role === "field_officer"; - - let complaints = fallbackComplaints; - let hotspots = fallbackHotspots; - - try { - [complaints, hotspots] = await Promise.all([ - getMapComplaints({ publicOnly: !isOperationsView }), - getPublicHotspots(), - ]); - } catch { - complaints = fallbackComplaints; - hotspots = fallbackHotspots; - } - - return ( - -
-

- {isOperationsView ? "Operations map" : "Public map"} -

-

- {isOperationsView - ? "Monitor live civic issue activity with city-wide visibility." - : "Explore active civic issues and hotspots across the city."} -

-

- {isOperationsView - ? "Use the live operations map to spot workload clusters, emergency incidents, and active complaint zones." - : "Residents can browse public-safe complaint activity, see active clusters, and understand where issues are already being addressed."} -

-
- - -
- ); -} diff --git a/civic-platform/app/notifications/page.tsx b/civic-platform/app/notifications/page.tsx deleted file mode 100644 index 4b7963ca9dbf46f76852887917f68100250452a8..0000000000000000000000000000000000000000 --- a/civic-platform/app/notifications/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { AppShell } from "@/components/app-shell"; -import { LiveSyncBadge } from "@/components/live-sync-badge"; -import { NotificationCenter } from "@/components/notification-center"; -import { requireUser } from "@/lib/auth"; -import { getNotifications, type ComplaintNotificationItem } from "@/lib/api"; - -const fallbackNotifications: ComplaintNotificationItem[] = []; - -export default async function NotificationsPage() { - const user = await requireUser(); - let notifications = fallbackNotifications; - - try { - notifications = await getNotifications(user.id); - } catch { - notifications = fallbackNotifications; - } - - return ( - -
-
-

Notifications

- -
-

See the latest updates on your work and complaints.

-

- Assignment, field progress, reopening, and citizen verification updates appear here. -

-
- - -
- ); -} diff --git a/civic-platform/app/page.tsx b/civic-platform/app/page.tsx deleted file mode 100644 index 7f24e6556baf4ec4d82b828dc1ddebbc50f92818..0000000000000000000000000000000000000000 --- a/civic-platform/app/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { DomainGrid } from "@/components/domain-grid"; -import { Hero } from "@/components/hero"; -import { AppShell } from "@/components/app-shell"; - -export default function Home() { - return ( - - - -
- -
-
- ); -} diff --git a/civic-platform/app/report/location/page.tsx b/civic-platform/app/report/location/page.tsx deleted file mode 100644 index 6f1155dfc6eba2170f520bbebfd7635d61e33579..0000000000000000000000000000000000000000 --- a/civic-platform/app/report/location/page.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import Link from "next/link"; -import { Crosshair, MapPinned, Navigation, ShieldAlert, Sparkles } from "lucide-react"; -import { AppShell } from "@/components/app-shell"; -import { FlowSteps } from "@/components/flow-steps"; - -const steps = ["Choose domain", "Add evidence", "Confirm location", "Review", "Submit"]; - -const locationChecks = [ - "Use current GPS for the quickest routing path.", - "If GPS is weak, move the map pin to the exact road, lane, or landmark.", - "Mention a nearby landmark so field staff can find the problem faster.", - "Emergency incidents should be pinned as precisely as possible.", -]; - -const locationFields = [ - "Latitude and longitude", - "Resolved address", - "Ward or zone", - "Nearby landmark", - "Road or locality name", - "Manual pin adjustment status", -]; - -export default function ReportLocationPage() { - return ( - -
-
-

Report flow

-

Confirm the issue location

-

- This step helps the system assign the correct ward, identify the responsible department, and improve - routing accuracy for field teams. -

-
- - -
- -
-
-
-
-

Step 3

-

Map and address confirmation

-

- In the MVP, this screen should open the user location, show a map preview, and let the citizen correct - the pin before moving to review. -

-
-
- Routing-ready -
-
- -
-
-
- -
-
-

Interactive map

-

Pinned complaint location

-
-
- -
-
-
- -
-

Map preview placeholder

-

- This area will use Leaflet with OpenStreetMap tiles, current location detection, drag-to-adjust pin, - and reverse geocoding for address confirmation. -

-
-
- -
- - -
-
-
- -
-
-
-
- -
-
-

Stored data

-

Location fields for the complaint

-
-
- -
- {locationFields.map((field) => ( -
- {field} -
- ))} -
-
- -
-
- -
-

Accuracy matters for emergency routing

-

- A precise location helps responders find the incident faster, especially in flood, fire, collapse, - or live wire complaints. -

-
-
- -
- {locationChecks.map((item) => ( -
- {item} -
- ))} -
-
-
-
- -
-
-

Next step

-

Review complaint details before submission

-

- After location is confirmed, the user should see a review screen with domain, evidence, location, and the - initial routing preview. -

-
- -
- - Back to report - - - Continue to review - -
-
-
- ); -} diff --git a/civic-platform/app/report/page.tsx b/civic-platform/app/report/page.tsx deleted file mode 100644 index 8a90c5540424720afb08007018fc08a2775daf18..0000000000000000000000000000000000000000 --- a/civic-platform/app/report/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { AppShell } from "@/components/app-shell"; -import { ReportForm } from "@/components/report-form"; -import { requireRole } from "@/lib/auth"; - -export default async function ReportIssuePage() { - const user = await requireRole(["citizen"]); - - return ( - -
-
-

- Citizen Reporting -

-

- File a Report -

-

- Tell us what happened and where. We will get it to the right department. -

-
- - -
-
- ); -} diff --git a/civic-platform/app/report/review/page.tsx b/civic-platform/app/report/review/page.tsx deleted file mode 100644 index 1b7cc2ce928a8772f0086fca2d37d127878211fd..0000000000000000000000000000000000000000 --- a/civic-platform/app/report/review/page.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import Link from "next/link"; -import { AlertTriangle, CheckCircle2, ChevronRight, ClipboardList, LocateFixed, ShieldAlert } from "lucide-react"; -import { AppShell } from "@/components/app-shell"; -import { FlowSteps } from "@/components/flow-steps"; - -const steps = ["Choose domain", "Add evidence", "Confirm location", "Review", "Submit"]; - -const reviewSections = [ - { - icon: ClipboardList, - title: "Complaint summary", - items: [ - "Selected domain: Water Supply, Sewerage, and Drainage", - "Likely sub-problem: Sewage overflow", - "Urgency hint: High public impact", - "Citizen note: Overflow near market entrance and bus stop", - ], - }, - { - icon: LocateFixed, - title: "Location confirmation", - items: [ - "Coordinates captured from device", - "Address resolved from map pin", - "Ward and routing zone ready", - "Nearby landmark noted for field staff", - ], - }, - { - icon: CheckCircle2, - title: "Evidence attached", - items: [ - "1 photo uploaded", - "0 video clips", - "Optional voice note available for later support", - "Media summary ready for department review", - ], - }, -]; - -const routingPreview = [ - "Primary department: Water and Sewerage", - "Secondary support: Drainage unit if monsoon overflow is detected", - "Initial priority: P2 High", - "Notification path: citizen confirmation after submission", -]; - -const submitChecks = [ - "Location is correct", - "Complaint is within city jurisdiction", - "Media is safe and relevant", - "Description is clear enough for field response", -]; - -export default function ReportReviewPage() { - return ( - -
-
-

Report flow

-

Review before final submission

-

- Check the issue details, location, and evidence once more before sending the complaint to the city team. -

-
- - -
- -
-
- {reviewSections.map(({ icon: Icon, title, items }) => ( -
-
-
- -
-
-

Summary

-

{title}

-
-
- -
- {items.map((item) => ( -
- {item} -
- ))} -
-
- ))} -
- -
-
-
-
- -
-
-

Routing preview

-

What the system will do next

-
-
- -
- {routingPreview.map((item) => ( -
- {item} -
- ))} -
-
- -
-
- -
-

Final checks before submission

-

- A quick final check helps the city team act faster and reduces follow-up questions. -

-
-
- -
- {submitChecks.map((item) => ( -
- {item} -
- ))} -
-
-
-
- -
-
-

Submit complaint

-

Ready to create the complaint record

-

- Once submitted, your complaint will receive an ID and move into review, routing, and department assignment. -

-
- -
- - Back to location - - - Submit complaint - - -
-
-
- ); -} diff --git a/civic-platform/app/report/success/page.tsx b/civic-platform/app/report/success/page.tsx deleted file mode 100644 index 97e163d2a761524f6aa59ab02eb0c8ba1cfd44b2..0000000000000000000000000000000000000000 --- a/civic-platform/app/report/success/page.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import Link from "next/link"; -import { ArrowRight, CheckCircle2, Clock3, Hash, MapPinned, ShieldCheck } from "lucide-react"; -import { AppShell } from "@/components/app-shell"; -import { FlowSteps } from "@/components/flow-steps"; - -const steps = ["Choose domain", "Add evidence", "Confirm location", "Review", "Submit"]; - -const nextActions = [ - "Your report is stored with a complaint ID for tracking.", - "The first visible status is Submitted.", - "Routing and priority decide which department picks it up next.", - "You will see updates when the complaint is assigned, worked on, or resolved.", -]; - -const summaryCards = [ - { - icon: Hash, - label: "Complaint ID", - value: "CP-2026-00124", - }, - { - icon: Clock3, - label: "Current status", - value: "Submitted", - }, - { - icon: MapPinned, - label: "Location state", - value: "Geo-tag confirmed", - }, -]; - -type ReportSuccessPageProps = { - searchParams?: Promise<{ - id?: string; - code?: string; - media?: string; - }>; -}; - -export default async function ReportSuccessPage({ searchParams }: ReportSuccessPageProps) { - const resolvedSearchParams = searchParams ? await searchParams : undefined; - const complaintId = resolvedSearchParams?.id ?? "sample-complaint"; - const complaintCode = resolvedSearchParams?.code ?? "CP-2026-00124"; - const mediaStatus = resolvedSearchParams?.media ?? "complete"; - - return ( - -
-
-

Report flow

-

Complaint submitted successfully

-

- Your complaint has been recorded. Keep the complaint ID handy and use the tracking page for updates. -

-
- - -
- -
-
-
-
- -
-
-

Your complaint is now in the system.

-

- It will now move through review, routing, assignment, and resolution. You can track progress from your - complaints page at any time. -

- {mediaStatus === "partial" ? ( -

- Your complaint was submitted, but one or more media files could not be uploaded. -

- ) : null} -
-
- - View my complaints - - - - Open complaint detail - - -
-
- -
- {summaryCards.map(({ icon: Icon, label, value }) => ( -
- -

{label}

-

- {label === "Complaint ID" ? complaintCode : value} -

-
- ))} -
-
-
- -
-
-
-
- -
-
-

What happens next

-

Post-submission workflow

-
-
- -
- {nextActions.map((item) => ( -
- {item} -
- ))} -
-
- -
-

Helpful shortcuts

-

What you can do from here

-

- You can report another issue, open the complaint detail page, or switch to emergency reporting if the - situation has become urgent. -

- -
- {[ - "Complaint ID", - "Current status", - "Expected next step", - "Tracking shortcut", - "Submit another issue", - "Emergency reporting shortcut", - ].map((item) => ( -
- {item} -
- ))} -
- -
- - Submit another issue - - - Emergency reporting - -
-
-
-
- ); -} diff --git a/civic-platform/app/tasks/page.tsx b/civic-platform/app/tasks/page.tsx deleted file mode 100644 index 0870124c7f8b4e28b8666aa13a7ef8843f900341..0000000000000000000000000000000000000000 --- a/civic-platform/app/tasks/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { AppShell } from "@/components/app-shell"; -import { FieldTaskBoard } from "@/components/field-task-board"; -import { LiveSyncBadge } from "@/components/live-sync-badge"; -import { requireRole } from "@/lib/auth"; -import { getOfficerAssignments, type ComplaintAssignmentItem } from "@/lib/api"; - -const fallbackAssignments: ComplaintAssignmentItem[] = []; - -export default async function TasksPage() { - const user = await requireRole(["field_officer"]); - let assignments = fallbackAssignments; - - try { - assignments = await getOfficerAssignments(user.id); - } catch { - assignments = fallbackAssignments; - } - - return ( - -
-
-

Field operations

- -
-

Work through assigned on-ground tasks.

-

- Accept work, start field action, and mark completion so the complaint lifecycle stays accurate for citizens and operators. -

-
- - -
- ); -} diff --git a/civic-platform/app/template.tsx b/civic-platform/app/template.tsx deleted file mode 100644 index 93aa998e624903799a4d5ed0cc55c7f9a512824e..0000000000000000000000000000000000000000 --- a/civic-platform/app/template.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import { motion } from "framer-motion"; - -export default function Template({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} diff --git a/civic-platform/backend/.env.example b/civic-platform/backend/.env.example deleted file mode 100644 index 5f80aca98d09924595811db48a41287e05e6bd94..0000000000000000000000000000000000000000 --- a/civic-platform/backend/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -PORT=4000 -NODE_ENV=development -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=civicpulse -DB_USER=postgres -DB_PASSWORD=your_password_here -DB_SSL=false -DEMO_AUTH_PASSWORD=civicpulse123 -CORS_ORIGINS=http://localhost:3000 -RATE_LIMIT_WINDOW_MS=60000 -RATE_LIMIT_MAX_REQUESTS=120 diff --git a/civic-platform/backend/README.md b/civic-platform/backend/README.md deleted file mode 100644 index 2c06a51c7e679df27f65292d89bc7d27cf2fe720..0000000000000000000000000000000000000000 --- a/civic-platform/backend/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Backend - -Node.js and TypeScript backend for the CivicPulse project. - -## Quick start - -1. Copy `.env.example` to `.env` -2. Set your PostgreSQL connection values -3. Create the `civicpulse` database -4. Apply `sql/001_initial_schema.sql` -5. Apply `sql/002_seed_core_data.sql` -6. Run `npm install` -7. Run `npm run dev` - -Alternative local setup: - -- Run `npm run db:init` to apply both SQL files using the configured PostgreSQL connection - -## Main folders - -- `src/config` environment config -- `src/db` PostgreSQL connection -- `src/routes` shared routes -- `src/modules/complaints` complaint APIs -- `sql` database schema files - -## First routes - -- `GET /api/health` -- `GET /api/departments` -- `GET /api/domains` -- `GET /api/complaints` -- `GET /api/complaints/citizen/:citizenId` -- `GET /api/complaints/:id` -- `GET /api/complaints/:id/history` -- `POST /api/complaints` -- `PATCH /api/complaints/:id/status` -- `POST /api/complaints/:id/assign` diff --git a/civic-platform/backend/backend-dev.log b/civic-platform/backend/backend-dev.log deleted file mode 100644 index 9fa3d5b2557227ac94832c9946adfb17b91ee83f..0000000000000000000000000000000000000000 --- a/civic-platform/backend/backend-dev.log +++ /dev/null @@ -1,5 +0,0 @@ - -> civicpulse-backend@0.1.0 dev -> tsx watch src/server.ts - -CivicPulse backend listening on port 4000 diff --git a/civic-platform/backend/backend-run.log b/civic-platform/backend/backend-run.log deleted file mode 100644 index ee1ec7efbe152dd971dcf70fa492a3bb493e5b14..0000000000000000000000000000000000000000 --- a/civic-platform/backend/backend-run.log +++ /dev/null @@ -1,2 +0,0 @@ -CivicPulse backend listening on port 4000 -^C \ No newline at end of file diff --git a/civic-platform/backend/backend.crash.log b/civic-platform/backend/backend.crash.log deleted file mode 100644 index df4f5c9a44683260a4734c7f70bd67b1111764ed..0000000000000000000000000000000000000000 Binary files a/civic-platform/backend/backend.crash.log and /dev/null differ diff --git a/civic-platform/backend/backend.detached.log b/civic-platform/backend/backend.detached.log deleted file mode 100644 index 634bbabd05915f6d974767d4215a2eec9c12e5a5..0000000000000000000000000000000000000000 --- a/civic-platform/backend/backend.detached.log +++ /dev/null @@ -1 +0,0 @@ -CivicPulse backend listening on port 4000 diff --git a/civic-platform/backend/backend.dev.log b/civic-platform/backend/backend.dev.log deleted file mode 100644 index df4f5c9a44683260a4734c7f70bd67b1111764ed..0000000000000000000000000000000000000000 Binary files a/civic-platform/backend/backend.dev.log and /dev/null differ diff --git a/civic-platform/backend/package-lock.json b/civic-platform/backend/package-lock.json deleted file mode 100644 index f1e8e1feeb9238597cc3d7909e7aa3c834250981..0000000000000000000000000000000000000000 --- a/civic-platform/backend/package-lock.json +++ /dev/null @@ -1,1817 +0,0 @@ -{ - "name": "civicpulse-backend", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "civicpulse-backend", - "version": "0.1.0", - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.4.7", - "express": "^4.21.2", - "multer": "^2.1.1", - "pg": "^8.13.3" - }, - "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^5.0.1", - "@types/multer": "^2.1.0", - "@types/node": "^22.13.10", - "@types/pg": "^8.11.11", - "tsx": "^4.19.3", - "typescript": "^5.8.2" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/multer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", - "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/node": { - "version": "22.19.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", - "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/multer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", - "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "type-is": "^1.6.18" - }, - "engines": { - "node": ">= 10.16.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.12.0", - "pg-pool": "^3.13.0", - "pg-protocol": "^1.13.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.3.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", - "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", - "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", - "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", - "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - } - } -} diff --git a/civic-platform/backend/package.json b/civic-platform/backend/package.json deleted file mode 100644 index 7b5ce8181cf5d29dfe2c9bdb6921ca4f64df6f1e..0000000000000000000000000000000000000000 --- a/civic-platform/backend/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "civicpulse-backend", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "tsx watch src/server.ts", - "build": "tsc -p tsconfig.json", - "start": "node dist/server.js", - "db:init": "tsx src/scripts/init-db.ts", - "db:seed": "tsx src/scripts/seed-db.ts", - "smoke": "tsx src/scripts/smoke-test.ts" - }, - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.4.7", - "express": "^4.21.2", - "multer": "^2.1.1", - "pg": "^8.13.3" - }, - "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^5.0.1", - "@types/multer": "^2.1.0", - "@types/node": "^22.13.10", - "@types/pg": "^8.11.11", - "tsx": "^4.19.3", - "typescript": "^5.8.2" - } -} diff --git a/civic-platform/backend/server.err.log b/civic-platform/backend/server.err.log deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/civic-platform/backend/server.out.log b/civic-platform/backend/server.out.log deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/civic-platform/backend/sql/001_initial_schema.sql b/civic-platform/backend/sql/001_initial_schema.sql deleted file mode 100644 index 1599b560230e6c4422d024f75b28ca9e67767a33..0000000000000000000000000000000000000000 --- a/civic-platform/backend/sql/001_initial_schema.sql +++ /dev/null @@ -1,246 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS "pgcrypto"; - -CREATE TABLE roles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(50) NOT NULL UNIQUE, - description TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE departments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(120) NOT NULL UNIQUE, - code VARCHAR(40) NOT NULL UNIQUE, - description TEXT, - is_emergency BOOLEAN NOT NULL DEFAULT FALSE, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE wards ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(120) NOT NULL, - code VARCHAR(40) NOT NULL UNIQUE, - city_name VARCHAR(120) NOT NULL, - state_name VARCHAR(120), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - full_name VARCHAR(160) NOT NULL, - email VARCHAR(160) UNIQUE, - phone VARCHAR(20) UNIQUE, - password_hash TEXT, - role_id UUID NOT NULL REFERENCES roles(id), - department_id UUID REFERENCES departments(id), - ward_id UUID REFERENCES wards(id), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE domains ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(120) NOT NULL UNIQUE, - description TEXT, - is_emergency BOOLEAN NOT NULL DEFAULT FALSE, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE sub_problems ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, - name VARCHAR(120) NOT NULL, - description TEXT, - severity_hint VARCHAR(20), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CONSTRAINT unique_sub_problem_per_domain UNIQUE (domain_id, name) -); - -CREATE TABLE complaints ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - complaint_code VARCHAR(40) NOT NULL UNIQUE, - citizen_id UUID NOT NULL REFERENCES users(id), - domain_id UUID REFERENCES domains(id), - sub_problem_id UUID REFERENCES sub_problems(id), - title VARCHAR(220) NOT NULL, - description TEXT, - status VARCHAR(32) NOT NULL, - priority_level VARCHAR(2) NOT NULL, - is_emergency BOOLEAN NOT NULL DEFAULT FALSE, - department_id UUID REFERENCES departments(id), - ward_id UUID REFERENCES wards(id), - submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - resolved_at TIMESTAMPTZ, - closed_at TIMESTAMPTZ, - reopened_count INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CONSTRAINT complaints_status_check CHECK ( - status IN ( - 'submitted', - 'validated', - 'classified', - 'prioritized', - 'assigned', - 'accepted', - 'in_progress', - 'resolved', - 'citizen_verified', - 'closed', - 'escalated', - 'reopened', - 'duplicate', - 'rejected', - 'on_hold' - ) - ), - CONSTRAINT complaints_priority_check CHECK (priority_level IN ('P1', 'P2', 'P3', 'P4')) -); - -CREATE TABLE complaint_locations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - complaint_id UUID NOT NULL UNIQUE REFERENCES complaints(id) ON DELETE CASCADE, - latitude NUMERIC(10, 7) NOT NULL, - longitude NUMERIC(10, 7) NOT NULL, - address_line TEXT, - landmark VARCHAR(220), - city_name VARCHAR(120), - state_name VARCHAR(120), - postal_code VARCHAR(20), - ward_id UUID REFERENCES wards(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE complaint_media ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE, - uploaded_by UUID REFERENCES users(id), - media_type VARCHAR(20) NOT NULL, - file_path TEXT NOT NULL, - file_url TEXT, - mime_type VARCHAR(120), - is_resolution_proof BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CONSTRAINT complaint_media_type_check CHECK (media_type IN ('image', 'video', 'audio')) -); - -CREATE TABLE complaint_status_history ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE, - old_status VARCHAR(32), - new_status VARCHAR(32) NOT NULL, - changed_by UUID REFERENCES users(id), - change_reason TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE assignments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE, - department_id UUID NOT NULL REFERENCES departments(id), - assigned_to_user_id UUID REFERENCES users(id), - assigned_by_user_id UUID REFERENCES users(id), - assignment_status VARCHAR(20) NOT NULL, - assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - accepted_at TIMESTAMPTZ, - completed_at TIMESTAMPTZ, - notes TEXT, - CONSTRAINT assignments_status_check CHECK ( - assignment_status IN ('assigned', 'accepted', 'in_progress', 'completed', 'reassigned') - ) -); - -CREATE TABLE routing_rules ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - domain_id UUID REFERENCES domains(id), - sub_problem_id UUID REFERENCES sub_problems(id), - ward_id UUID REFERENCES wards(id), - department_id UUID NOT NULL REFERENCES departments(id), - priority_override VARCHAR(2), - is_emergency_route BOOLEAN NOT NULL DEFAULT FALSE, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CONSTRAINT routing_rules_priority_check CHECK (priority_override IS NULL OR priority_override IN ('P1', 'P2', 'P3', 'P4')) -); - -CREATE TABLE priority_rules ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - domain_id UUID REFERENCES domains(id), - sub_problem_id UUID REFERENCES sub_problems(id), - base_priority VARCHAR(2) NOT NULL, - near_sensitive_zone_boost INTEGER NOT NULL DEFAULT 0, - repeat_complaint_boost INTEGER NOT NULL DEFAULT 0, - reopened_boost INTEGER NOT NULL DEFAULT 0, - sla_breach_boost INTEGER NOT NULL DEFAULT 0, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CONSTRAINT priority_rules_base_check CHECK (base_priority IN ('P1', 'P2', 'P3', 'P4')) -); - -CREATE TABLE complaint_notes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE, - author_id UUID REFERENCES users(id), - note_type VARCHAR(20) NOT NULL, - note_text TEXT NOT NULL, - is_internal BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CONSTRAINT complaint_notes_type_check CHECK ( - note_type IN ('operator_note', 'field_note', 'citizen_note', 'system_note') - ) -); - -CREATE TABLE notifications ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - complaint_id UUID REFERENCES complaints(id) ON DELETE CASCADE, - title VARCHAR(220) NOT NULL, - message TEXT NOT NULL, - notification_type VARCHAR(50) NOT NULL, - is_read BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE feedback ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - complaint_id UUID NOT NULL REFERENCES complaints(id) ON DELETE CASCADE, - citizen_id UUID NOT NULL REFERENCES users(id), - rating INTEGER CHECK (rating BETWEEN 1 AND 5), - comment TEXT, - reopen_requested BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE audit_logs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - actor_user_id UUID REFERENCES users(id), - entity_type VARCHAR(50) NOT NULL, - entity_id UUID, - action VARCHAR(120) NOT NULL, - details JSONB, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX idx_users_role_id ON users(role_id); -CREATE INDEX idx_users_department_id ON users(department_id); -CREATE INDEX idx_users_ward_id ON users(ward_id); -CREATE INDEX idx_sub_problems_domain_id ON sub_problems(domain_id); -CREATE INDEX idx_complaints_citizen_id ON complaints(citizen_id); -CREATE INDEX idx_complaints_domain_id ON complaints(domain_id); -CREATE INDEX idx_complaints_department_id ON complaints(department_id); -CREATE INDEX idx_complaints_ward_id ON complaints(ward_id); -CREATE INDEX idx_complaints_status ON complaints(status); -CREATE INDEX idx_complaints_priority_level ON complaints(priority_level); -CREATE INDEX idx_complaints_submitted_at ON complaints(submitted_at); -CREATE INDEX idx_complaint_media_complaint_id ON complaint_media(complaint_id); -CREATE INDEX idx_complaint_status_history_complaint_id ON complaint_status_history(complaint_id); -CREATE INDEX idx_assignments_complaint_id ON assignments(complaint_id); -CREATE INDEX idx_assignments_department_id ON assignments(department_id); -CREATE INDEX idx_routing_rules_department_id ON routing_rules(department_id); -CREATE INDEX idx_notifications_user_id ON notifications(user_id); -CREATE INDEX idx_feedback_complaint_id ON feedback(complaint_id); -CREATE INDEX idx_audit_logs_actor_user_id ON audit_logs(actor_user_id); diff --git a/civic-platform/backend/sql/002_seed_core_data.sql b/civic-platform/backend/sql/002_seed_core_data.sql deleted file mode 100644 index a9db8dc0fd9624e8461feb91025b5affa94edd8c..0000000000000000000000000000000000000000 --- a/civic-platform/backend/sql/002_seed_core_data.sql +++ /dev/null @@ -1,133 +0,0 @@ -INSERT INTO roles (id, name, description) -VALUES - ('10000000-0000-0000-0000-000000000001', 'citizen', 'Citizen user who reports and tracks complaints'), - ('10000000-0000-0000-0000-000000000002', 'department_operator', 'Department operator who validates and assigns complaints'), - ('10000000-0000-0000-0000-000000000003', 'municipal_admin', 'Municipal administrator with cross-department access'), - ('10000000-0000-0000-0000-000000000004', 'field_officer', 'Field officer who executes assigned work on the ground') -ON CONFLICT (name) DO NOTHING; - -INSERT INTO departments (id, name, code, description, is_emergency) -VALUES - ('20000000-0000-0000-0000-000000000001', 'Roads and Public Works', 'roads-public-works', 'Handles roads, potholes, and transport infrastructure issues', FALSE), - ('20000000-0000-0000-0000-000000000002', 'Sanitation and Solid Waste', 'sanitation-solid-waste', 'Handles waste collection, garbage overflow, and sanitation complaints', FALSE), - ('20000000-0000-0000-0000-000000000003', 'Water and Sewerage', 'water-sewerage', 'Handles water leakage, sewage, and drainage issues', FALSE), - ('20000000-0000-0000-0000-000000000004', 'Electrical and Street Lighting', 'electrical-street-lighting', 'Handles streetlight and public electrical complaints', FALSE), - ('20000000-0000-0000-0000-000000000005', 'Municipal Maintenance', 'municipal-maintenance', 'Handles open manholes and civic infrastructure maintenance', FALSE), - ('20000000-0000-0000-0000-000000000006', 'Disaster Management Authority', 'disaster-management', 'Handles flood, collapse, rescue, and emergency coordination', TRUE) -ON CONFLICT (code) DO NOTHING; - -INSERT INTO wards (id, name, code, city_name, state_name) -VALUES - ('30000000-0000-0000-0000-000000000001', 'Ward 4', 'ward-4', 'Sample City', 'Sample State'), - ('30000000-0000-0000-0000-000000000002', 'Ward 8', 'ward-8', 'Sample City', 'Sample State'), - ('30000000-0000-0000-0000-000000000003', 'Ward 12', 'ward-12', 'Sample City', 'Sample State') -ON CONFLICT (code) DO NOTHING; - -INSERT INTO domains (id, name, description, is_emergency) -VALUES - ('40000000-0000-0000-0000-000000000001', 'Roads and Transportation', 'Road safety, traffic support, and mobility complaints', FALSE), - ('40000000-0000-0000-0000-000000000002', 'Sanitation and Waste Management', 'Waste collection, cleanliness, and sanitation complaints', FALSE), - ('40000000-0000-0000-0000-000000000003', 'Water Supply, Sewerage, and Drainage', 'Water access, sewage, and drainage complaints', FALSE), - ('40000000-0000-0000-0000-000000000004', 'Street Lighting and Electrical Infrastructure', 'Public lighting and electrical safety complaints', FALSE), - ('40000000-0000-0000-0000-000000000005', 'Public Infrastructure and Amenities', 'Maintenance needs for public assets and shared facilities', FALSE), - ('40000000-0000-0000-0000-000000000006', 'Environment and Public Health', 'Public hygiene and environmental safety concerns', FALSE), - ('40000000-0000-0000-0000-000000000007', 'Fire Emergencies', 'Emergency fire and smoke incidents', TRUE), - ('40000000-0000-0000-0000-000000000008', 'Flood and Water Disaster', 'Critical flood and water disaster events', TRUE) -ON CONFLICT (name) DO NOTHING; - -INSERT INTO users (id, full_name, email, phone, password_hash, role_id, department_id, ward_id) -VALUES - ( - '00000000-0000-0000-0000-000000000001', - 'Sample Citizen', - 'citizen@example.com', - '9000000001', - 'dev-placeholder-hash', - '10000000-0000-0000-0000-000000000001', - NULL, - '30000000-0000-0000-0000-000000000003' - ), - ( - '11111111-1111-1111-1111-111111111111', - 'Sample Operator', - 'operator@example.com', - '9000000002', - 'dev-placeholder-hash', - '10000000-0000-0000-0000-000000000002', - '20000000-0000-0000-0000-000000000005', - '30000000-0000-0000-0000-000000000001' - ), - ( - '22222222-2222-2222-2222-222222222222', - 'Sample Field Officer', - 'officer@example.com', - '9000000003', - 'dev-placeholder-hash', - '10000000-0000-0000-0000-000000000004', - '20000000-0000-0000-0000-000000000005', - '30000000-0000-0000-0000-000000000001' - ), - ( - '22222222-2222-2222-2222-222222222223', - 'Road Field Officer', - 'roads.officer@example.com', - '9000000004', - 'dev-placeholder-hash', - '10000000-0000-0000-0000-000000000004', - '20000000-0000-0000-0000-000000000001', - '30000000-0000-0000-0000-000000000001' - ), - ( - '22222222-2222-2222-2222-222222222224', - 'Sanitation Field Officer', - 'sanitation.officer@example.com', - '9000000005', - 'dev-placeholder-hash', - '10000000-0000-0000-0000-000000000004', - '20000000-0000-0000-0000-000000000002', - '30000000-0000-0000-0000-000000000002' - ), - ( - '22222222-2222-2222-2222-222222222225', - 'Water Field Officer', - 'water.officer@example.com', - '9000000006', - 'dev-placeholder-hash', - '10000000-0000-0000-0000-000000000004', - '20000000-0000-0000-0000-000000000003', - '30000000-0000-0000-0000-000000000002' - ), - ( - '22222222-2222-2222-2222-222222222226', - 'Electrical Field Officer', - 'electrical.officer@example.com', - '9000000007', - 'dev-placeholder-hash', - '10000000-0000-0000-0000-000000000004', - '20000000-0000-0000-0000-000000000004', - '30000000-0000-0000-0000-000000000003' - ) -ON CONFLICT (email) DO NOTHING; - -INSERT INTO routing_rules (domain_id, sub_problem_id, ward_id, department_id, priority_override, is_emergency_route) -VALUES - ('40000000-0000-0000-0000-000000000001', NULL, NULL, '20000000-0000-0000-0000-000000000001', NULL, FALSE), - ('40000000-0000-0000-0000-000000000002', NULL, NULL, '20000000-0000-0000-0000-000000000002', NULL, FALSE), - ('40000000-0000-0000-0000-000000000003', NULL, NULL, '20000000-0000-0000-0000-000000000003', NULL, FALSE), - ('40000000-0000-0000-0000-000000000004', NULL, NULL, '20000000-0000-0000-0000-000000000004', NULL, FALSE), - ('40000000-0000-0000-0000-000000000005', NULL, NULL, '20000000-0000-0000-0000-000000000005', 'P1', FALSE), - ('40000000-0000-0000-0000-000000000006', NULL, NULL, '20000000-0000-0000-0000-000000000002', NULL, FALSE), - ('40000000-0000-0000-0000-000000000007', NULL, NULL, '20000000-0000-0000-0000-000000000006', 'P1', TRUE), - ('40000000-0000-0000-0000-000000000008', NULL, NULL, '20000000-0000-0000-0000-000000000006', 'P1', TRUE); - -INSERT INTO priority_rules (domain_id, sub_problem_id, base_priority) -VALUES - ('40000000-0000-0000-0000-000000000001', NULL, 'P3'), - ('40000000-0000-0000-0000-000000000002', NULL, 'P3'), - ('40000000-0000-0000-0000-000000000003', NULL, 'P2'), - ('40000000-0000-0000-0000-000000000004', NULL, 'P3'), - ('40000000-0000-0000-0000-000000000005', NULL, 'P2'), - ('40000000-0000-0000-0000-000000000006', NULL, 'P3'), - ('40000000-0000-0000-0000-000000000007', NULL, 'P1'), - ('40000000-0000-0000-0000-000000000008', NULL, 'P1') -ON CONFLICT DO NOTHING; diff --git a/civic-platform/backend/sql/003_performance_indexes.sql b/civic-platform/backend/sql/003_performance_indexes.sql deleted file mode 100644 index 1db8f51ac20a76ce5605ea32ed8cd34a69528963..0000000000000000000000000000000000000000 --- a/civic-platform/backend/sql/003_performance_indexes.sql +++ /dev/null @@ -1,32 +0,0 @@ -CREATE INDEX IF NOT EXISTS idx_complaints_status_submitted_at - ON complaints (status, submitted_at DESC); - -CREATE INDEX IF NOT EXISTS idx_complaints_department_status - ON complaints (department_id, status, submitted_at DESC); - -CREATE INDEX IF NOT EXISTS idx_complaints_domain_submitted_at - ON complaints (domain_id, submitted_at DESC); - -CREATE INDEX IF NOT EXISTS idx_complaints_priority_status - ON complaints (priority_level, status, submitted_at DESC); - -CREATE INDEX IF NOT EXISTS idx_assignments_assigned_to_status - ON assignments (assigned_to_user_id, assignment_status, assigned_at DESC); - -CREATE INDEX IF NOT EXISTS idx_assignments_complaint_assigned_at - ON assignments (complaint_id, assigned_at DESC); - -CREATE INDEX IF NOT EXISTS idx_notifications_user_read_created - ON notifications (user_id, is_read, created_at DESC); - -CREATE INDEX IF NOT EXISTS idx_status_history_complaint_created - ON complaint_status_history (complaint_id, created_at DESC); - -CREATE INDEX IF NOT EXISTS idx_feedback_complaint_created - ON feedback (complaint_id, created_at DESC); - -CREATE INDEX IF NOT EXISTS idx_notes_complaint_created - ON complaint_notes (complaint_id, created_at DESC); - -CREATE INDEX IF NOT EXISTS idx_locations_lat_lng - ON complaint_locations (latitude, longitude); diff --git a/civic-platform/backend/sql/README.md b/civic-platform/backend/sql/README.md deleted file mode 100644 index e9a53920e9d5aa7b58d2898fd5c77c77c616a02b..0000000000000000000000000000000000000000 --- a/civic-platform/backend/sql/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# SQL Files - -## Current files - -- `001_initial_schema.sql` -- `002_seed_core_data.sql` - -## Usage - -Apply the schema file to the `civicpulse` PostgreSQL database before running the complaint APIs. - -Recommended order: - -1. create database -2. apply `001_initial_schema.sql` -3. apply `002_seed_core_data.sql` -4. start backend - -Future migrations should continue with: - -- `003_...` -- `004_...` diff --git a/civic-platform/backend/src/app.ts b/civic-platform/backend/src/app.ts deleted file mode 100644 index fc4171e347f80aafe76c98802aeca48031c94349..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/app.ts +++ /dev/null @@ -1,48 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import cors from "cors"; -import express from "express"; -import { aiRouter } from "./modules/ai/ai.routes.js"; -import { authRouter } from "./modules/auth/auth.routes.js"; -import { env } from "./config/env.js"; -import { rateLimitMiddleware, securityHeaders } from "./lib/security.js"; -import { complaintRouter } from "./modules/complaints/complaint.routes.js"; -import { departmentRouter } from "./modules/departments/department.routes.js"; -import { domainRouter } from "./modules/domains/domain.routes.js"; -import { userRouter } from "./modules/users/user.routes.js"; -import { healthRouter } from "./routes/health.js"; - -export function createApp() { - const app = express(); - const uploadsDir = path.resolve(process.cwd(), "uploads"); - - fs.mkdirSync(path.join(uploadsDir, "complaints"), { recursive: true }); - - app.set("trust proxy", 1); - app.use( - cors({ - origin: env.corsOrigins.split(",").map((value) => value.trim()), - }), - ); - app.use(securityHeaders); - app.use(rateLimitMiddleware); - app.use(express.json({ limit: "10mb" })); - app.use("/uploads", express.static(uploadsDir)); - - app.get("/", (_req, res) => { - res.json({ - name: "civicpulse-backend", - message: "CivicPulse backend is running", - }); - }); - - app.use("/api/health", healthRouter); - app.use("/api/ai", aiRouter); - app.use("/api/auth", authRouter); - app.use("/api/departments", departmentRouter); - app.use("/api/domains", domainRouter); - app.use("/api/users", userRouter); - app.use("/api/complaints", complaintRouter); - - return app; -} diff --git a/civic-platform/backend/src/config/env.ts b/civic-platform/backend/src/config/env.ts deleted file mode 100644 index 168fce43b7d418146ae01f40d9b49320a231fd28..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/config/env.ts +++ /dev/null @@ -1,28 +0,0 @@ -import dotenv from "dotenv"; - -dotenv.config(); - -function requireEnv(name: string, fallback?: string): string { - const value = process.env[name] ?? fallback; - - if (!value) { - throw new Error(`Missing required environment variable: ${name}`); - } - - return value; -} - -export const env = { - nodeEnv: requireEnv("NODE_ENV", "development"), - port: Number(requireEnv("PORT", "4000")), - corsOrigins: requireEnv("CORS_ORIGINS", "http://localhost:3000"), - dbHost: requireEnv("DB_HOST", "localhost"), - dbPort: Number(requireEnv("DB_PORT", "5432")), - dbName: requireEnv("DB_NAME", "civicpulse"), - dbUser: requireEnv("DB_USER", "postgres"), - dbPassword: requireEnv("DB_PASSWORD"), - dbSsl: requireEnv("DB_SSL", "false") === "true", - demoAuthPassword: requireEnv("DEMO_AUTH_PASSWORD", "civicpulse123"), - rateLimitWindowMs: Number(requireEnv("RATE_LIMIT_WINDOW_MS", "60000")), - rateLimitMaxRequests: Number(requireEnv("RATE_LIMIT_MAX_REQUESTS", "120")), -}; diff --git a/civic-platform/backend/src/db/pool.ts b/civic-platform/backend/src/db/pool.ts deleted file mode 100644 index 1a515477d46661120bfac0a50ee6e91388defe7a..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/db/pool.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Pool } from "pg"; -import { env } from "../config/env.js"; - -export const db = new Pool({ - host: env.dbHost, - port: env.dbPort, - database: env.dbName, - user: env.dbUser, - password: env.dbPassword, - ssl: env.dbSsl ? { rejectUnauthorized: false } : false, -}); diff --git a/civic-platform/backend/src/lib/audit.ts b/civic-platform/backend/src/lib/audit.ts deleted file mode 100644 index 92b3eca5274e20127eaa414b4fc7c1bb68eba8e4..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/lib/audit.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { db } from "../db/pool.js"; - -export async function writeAuditLog(input: { - actorUserId?: string | null; - entityType: string; - entityId?: string | null; - action: string; - details?: Record; -}) { - await db.query( - ` - INSERT INTO audit_logs ( - actor_user_id, - entity_type, - entity_id, - action, - details - ) - VALUES ($1, $2, $3, $4, $5::jsonb) - `, - [ - input.actorUserId ?? null, - input.entityType, - input.entityId ?? null, - input.action, - JSON.stringify(input.details ?? {}), - ], - ); -} diff --git a/civic-platform/backend/src/lib/cache.ts b/civic-platform/backend/src/lib/cache.ts deleted file mode 100644 index 269ccfe751d66ef2ba32c29f525280fe13fb122c..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/lib/cache.ts +++ /dev/null @@ -1,37 +0,0 @@ -type CacheEntry = { - value: T; - expiresAt: number; -}; - -const store = new Map>(); - -export async function withCache(key: string, ttlMs: number, producer: () => Promise): Promise { - const now = Date.now(); - const existing = store.get(key); - - if (existing && existing.expiresAt > now) { - return existing.value as T; - } - - const value = await producer(); - store.set(key, { - value, - expiresAt: now + ttlMs, - }); - - return value; -} - -export function clearCache(keyPrefix?: string) { - if (!keyPrefix) { - store.clear(); - return; - } - - for (const key of store.keys()) { - if (key.startsWith(keyPrefix)) { - store.delete(key); - } - } -} - diff --git a/civic-platform/backend/src/lib/security.ts b/civic-platform/backend/src/lib/security.ts deleted file mode 100644 index d741061e7424364d341bafd88cce5750b6199eb9..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/lib/security.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { NextFunction, Request, Response } from "express"; -import { env } from "../config/env.js"; - -const requestBuckets = new Map(); - -export function securityHeaders(_req: Request, res: Response, next: NextFunction) { - res.setHeader("X-Content-Type-Options", "nosniff"); - res.setHeader("X-Frame-Options", "DENY"); - res.setHeader("Referrer-Policy", "same-origin"); - res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(self)"); - res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); - next(); -} - -export function rateLimitMiddleware(req: Request, res: Response, next: NextFunction) { - const key = req.ip || req.socket.remoteAddress || "unknown"; - const now = Date.now(); - const current = requestBuckets.get(key); - - if (!current || current.resetAt <= now) { - requestBuckets.set(key, { - count: 1, - resetAt: now + env.rateLimitWindowMs, - }); - next(); - return; - } - - if (current.count >= env.rateLimitMaxRequests) { - res.status(429).json({ - error: "Too many requests. Please wait and try again.", - }); - return; - } - - current.count += 1; - requestBuckets.set(key, current); - next(); -} - -export function sanitizeText(input?: string, maxLength = 2000) { - if (!input) { - return undefined; - } - - return input - .replace(/<[^>]*>/g, " ") - .replace(/\s+/g, " ") - .trim() - .slice(0, maxLength); -} - diff --git a/civic-platform/backend/src/modules/ai/ai.engine.ts b/civic-platform/backend/src/modules/ai/ai.engine.ts deleted file mode 100644 index 24ac91c92faf6e32226bf49764802086efa62d3e..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/ai/ai.engine.ts +++ /dev/null @@ -1,247 +0,0 @@ -import type { ComplaintMapItem, NearbyComplaintItem } from "../complaints/complaint.types.js"; -import type { AiDomainSuggestion, DraftAnalysisInput, DraftAnalysisResult } from "./ai.types.js"; - -type DomainRule = { - id: string; - name: string; - departmentCode: string; - departmentName: string; - emergency: boolean; - keywords: string[]; -}; - -const domainRules: DomainRule[] = [ - { - id: "40000000-0000-0000-0000-000000000001", - name: "Roads and Transportation", - departmentCode: "roads-public-works", - departmentName: "Roads and Public Works", - emergency: false, - keywords: ["pothole", "road", "traffic", "signal", "divider", "footpath", "sidewalk", "junction", "blockage"], - }, - { - id: "40000000-0000-0000-0000-000000000002", - name: "Sanitation and Waste Management", - departmentCode: "sanitation-solid-waste", - departmentName: "Sanitation and Solid Waste", - emergency: false, - keywords: ["garbage", "waste", "overflowing bin", "dumping", "trash", "unclean", "sanitation"], - }, - { - id: "40000000-0000-0000-0000-000000000003", - name: "Water Supply, Sewerage, and Drainage", - departmentCode: "water-sewerage", - departmentName: "Water and Sewerage", - emergency: false, - keywords: ["sewage", "drain", "drainage", "waterlogging", "leak", "pipe", "overflow", "manhole overflow", "water supply"], - }, - { - id: "40000000-0000-0000-0000-000000000004", - name: "Street Lighting and Electrical Infrastructure", - departmentCode: "electrical-street-lighting", - departmentName: "Electrical and Street Lighting", - emergency: false, - keywords: ["streetlight", "pole", "light", "transformer", "wire", "power", "electric", "electrical"], - }, - { - id: "40000000-0000-0000-0000-000000000005", - name: "Public Infrastructure and Amenities", - departmentCode: "municipal-maintenance", - departmentName: "Municipal Maintenance", - emergency: false, - keywords: ["manhole", "bus stop", "bench", "toilet", "park", "amenity", "public facility", "damaged structure"], - }, - { - id: "40000000-0000-0000-0000-000000000006", - name: "Environment and Public Health", - departmentCode: "sanitation-solid-waste", - departmentName: "Sanitation and Solid Waste", - emergency: false, - keywords: ["mosquito", "stagnant water", "dead animal", "fallen tree", "smoke", "public health", "hygiene", "pollution"], - }, - { - id: "40000000-0000-0000-0000-000000000007", - name: "Fire Emergencies", - departmentCode: "disaster-management", - departmentName: "Disaster Management Authority", - emergency: true, - keywords: ["fire", "smoke", "burning", "flame", "blast"], - }, - { - id: "40000000-0000-0000-0000-000000000008", - name: "Flood and Water Disaster", - departmentCode: "disaster-management", - departmentName: "Disaster Management Authority", - emergency: true, - keywords: ["flood", "rescue", "collapse", "landslide", "disaster", "storm", "water disaster"], - }, -]; - -const criticalKeywords = ["fire", "flood", "collapse", "live wire", "electrocution", "blast", "rescue"]; -const highKeywords = ["sewage", "waterlogging", "open manhole", "transformer", "wire", "unsafe", "hazard", "blocked"]; -const sensitiveLocationKeywords = ["school", "hospital", "market", "junction", "bus stop", "bridge", "underpass"]; - -function tokenize(input: string) { - return input - .toLowerCase() - .replace(/[^a-z0-9\s]/g, " ") - .split(/\s+/) - .filter(Boolean); -} - -function unique(values: T[]) { - return [...new Set(values)]; -} - -function jaccardSimilarity(source: string, target: string) { - const sourceTokens = new Set(tokenize(source)); - const targetTokens = new Set(tokenize(target)); - - if (sourceTokens.size === 0 || targetTokens.size === 0) { - return 0; - } - - let intersection = 0; - - for (const token of sourceTokens) { - if (targetTokens.has(token)) { - intersection += 1; - } - } - - return intersection / (sourceTokens.size + targetTokens.size - intersection); -} - -export function analyzeDraft( - input: DraftAnalysisInput, - nearbyComplaints: NearbyComplaintItem[], -): DraftAnalysisResult { - const text = [input.title, input.description, input.addressLine, input.landmark].filter(Boolean).join(" ").toLowerCase(); - const visualSignals = input.visualSignals ?? []; - const matchedKeywords = unique( - [...criticalKeywords, ...highKeywords, ...sensitiveLocationKeywords].filter((keyword) => text.includes(keyword)), - ); - - const domainScored = domainRules - .map((domain) => { - const matchedSignals = domain.keywords.filter((keyword) => text.includes(keyword)); - const visualMatch = visualSignals.find((signal) => signal.label === domain.name); - const score = matchedSignals.length + (visualMatch ? visualMatch.confidence * 2 : 0) + (domain.emergency ? 0.2 : 0); - - return { - domain, - matchedSignals: unique([...matchedSignals, ...(visualMatch?.matchedSignals ?? [])]), - score, - }; - }) - .filter((entry) => entry.score > 0) - .sort((left, right) => right.score - left.score); - - const suggestedDomain: AiDomainSuggestion | null = domainScored[0] - ? { - domainId: domainScored[0].domain.id, - domainName: domainScored[0].domain.name, - confidence: Math.min(0.98, 0.45 + domainScored[0].score * 0.12), - matchedSignals: domainScored[0].matchedSignals, - } - : null; - - const hasCriticalSignal = criticalKeywords.some((keyword) => text.includes(keyword)); - const hasHighSignal = highKeywords.some((keyword) => text.includes(keyword)); - const hasSensitiveLocation = sensitiveLocationKeywords.some((keyword) => text.includes(keyword)); - const duplicateMatches = nearbyComplaints - .map((complaint) => ({ - complaintId: complaint.id, - complaintCode: complaint.complaintCode, - title: complaint.title, - distanceKm: complaint.distanceKm, - similarityScore: jaccardSimilarity(`${input.title ?? ""} ${input.description ?? ""}`, complaint.title), - status: complaint.status, - })) - .filter((complaint) => complaint.distanceKm <= 0.75 && complaint.similarityScore >= 0.15) - .sort((left, right) => { - if (right.similarityScore !== left.similarityScore) { - return right.similarityScore - left.similarityScore; - } - - return left.distanceKm - right.distanceKm; - }) - .slice(0, 4); - - const duplicateRisk = - duplicateMatches.some((item) => item.distanceKm <= 0.35 && item.similarityScore >= 0.45) - ? "high" - : duplicateMatches.length > 0 - ? "medium" - : "low"; - - const suggestedPriority = - hasCriticalSignal || suggestedDomain?.domainName === "Fire Emergencies" || suggestedDomain?.domainName === "Flood and Water Disaster" - ? "P1" - : hasHighSignal || hasSensitiveLocation - ? "P2" - : matchedKeywords.length > 0 - ? "P3" - : "P4"; - - const severityLevel = - suggestedPriority === "P1" - ? "critical" - : suggestedPriority === "P2" - ? "high" - : suggestedPriority === "P3" - ? "medium" - : "low"; - - const emergencyLikelihood = - hasCriticalSignal || suggestedDomain?.domainName === "Fire Emergencies" || suggestedDomain?.domainName === "Flood and Water Disaster" - ? "high" - : hasHighSignal - ? "medium" - : "low"; - - const reasonSummary = unique([ - suggestedDomain - ? `Suggested domain: ${suggestedDomain.domainName} based on keywords like ${suggestedDomain.matchedSignals.join(", ")}.` - : "No strong domain signal detected yet. A manual operator review may still be needed.", - visualSignals.length - ? `Image evidence contributed ${visualSignals.map((signal) => `${signal.label} (${(signal.confidence * 100).toFixed(0)}%)`).join(", ")}.` - : "", - hasSensitiveLocation ? "The location text suggests a sensitive area, so the complaint is treated as higher impact." : "", - duplicateRisk !== "low" ? `Nearby complaint similarity suggests ${duplicateRisk} duplicate risk.` : "", - input.mediaTypes?.length ? `Evidence includes ${input.mediaTypes.join(", ")} support, which improves routing confidence.` : "", - ]).filter(Boolean); - - return { - suggestedDomain, - suggestedPriority, - severityLevel, - emergencyLikelihood, - duplicateRisk, - routeDepartmentCode: suggestedDomain - ? domainRules.find((domain) => domain.id === suggestedDomain.domainId)?.departmentCode ?? null - : null, - routeDepartmentName: suggestedDomain - ? domainRules.find((domain) => domain.id === suggestedDomain.domainId)?.departmentName ?? null - : null, - speechReady: true, - visionReady: true, - reasonSummary, - matchedKeywords, - duplicateMatches, - }; -} - -export function analyzeComplaintForOperators(complaint: ComplaintMapItem | (ComplaintMapItem & { description?: string | null }), nearby: NearbyComplaintItem[]) { - return analyzeDraft( - { - title: complaint.title, - description: "description" in complaint ? complaint.description ?? undefined : undefined, - addressLine: complaint.addressLine ?? undefined, - landmark: complaint.landmark ?? undefined, - latitude: Number(complaint.latitude), - longitude: Number(complaint.longitude), - }, - nearby, - ); -} diff --git a/civic-platform/backend/src/modules/ai/ai.routes.ts b/civic-platform/backend/src/modules/ai/ai.routes.ts deleted file mode 100644 index 8b53dcadfc728ac3cf5c898e0b9807a5f49d8ba9..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/ai/ai.routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Router } from "express"; -import { analyzeDraftService, getComplaintAiInsightsService } from "./ai.service.js"; - -export const aiRouter = Router(); - -aiRouter.post("/analyze-draft", async (req, res) => { - try { - const analysis = await analyzeDraftService(req.body); - res.json({ data: analysis }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to analyze complaint draft", - }); - } -}); - -aiRouter.get("/complaints/:id/insights", async (req, res) => { - try { - const analysis = await getComplaintAiInsightsService(req.params.id); - - if (!analysis) { - res.status(404).json({ error: "Complaint not found" }); - return; - } - - res.json({ data: analysis }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch complaint insights", - }); - } -}); diff --git a/civic-platform/backend/src/modules/ai/ai.service.ts b/civic-platform/backend/src/modules/ai/ai.service.ts deleted file mode 100644 index ee28a3b019b8eca19c4664451f7c3e89752170e8..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/ai/ai.service.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { getComplaintById, listComplaintMedia, listNearbyComplaints } from "../complaints/complaint.repository.js"; -import { analyzeComplaintForOperators, analyzeDraft } from "./ai.engine.js"; -import type { DraftAnalysisInput } from "./ai.types.js"; - -export async function analyzeDraftService(input: DraftAnalysisInput) { - const nearby = - typeof input.latitude === "number" && typeof input.longitude === "number" - ? await listNearbyComplaints({ - latitude: input.latitude, - longitude: input.longitude, - radiusKm: 0.75, - domainId: input.domainId, - publicOnly: true, - }) - : []; - - return analyzeDraft(input, nearby); -} - -export async function getComplaintAiInsightsService(complaintId: string) { - if (!complaintId) { - throw new Error("complaintId is required"); - } - - const complaint = await getComplaintById(complaintId); - - if (!complaint) { - return null; - } - - const nearby = - complaint.latitude && complaint.longitude - ? await listNearbyComplaints({ - latitude: Number(complaint.latitude), - longitude: Number(complaint.longitude), - radiusKm: 0.75, - publicOnly: true, - }) - : []; - - const media = await listComplaintMedia(complaintId); - - const analysis = analyzeComplaintForOperators( - { - id: complaint.id, - complaintCode: complaint.complaintCode, - title: complaint.title, - status: complaint.status, - priorityLevel: complaint.priorityLevel, - isEmergency: complaint.isEmergency, - domainName: complaint.domainName, - departmentName: complaint.departmentName, - wardName: null, - latitude: complaint.latitude ?? "0", - longitude: complaint.longitude ?? "0", - addressLine: complaint.addressLine, - landmark: complaint.landmark, - submittedAt: complaint.submittedAt, - description: complaint.description, - }, - nearby.filter((item) => item.id !== complaint.id), - ); - - return { - ...analysis, - mediaSummary: { - totalEvidence: media.length, - hasImage: media.some((item) => item.mediaType === "image"), - hasVideo: media.some((item) => item.mediaType === "video"), - hasAudio: media.some((item) => item.mediaType === "audio"), - hasResolutionProof: media.some((item) => item.isResolutionProof), - }, - }; -} diff --git a/civic-platform/backend/src/modules/ai/ai.types.ts b/civic-platform/backend/src/modules/ai/ai.types.ts deleted file mode 100644 index e08cd62ee1013fe4da538525e4d41f205da7de22..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/ai/ai.types.ts +++ /dev/null @@ -1,46 +0,0 @@ -export type DraftAnalysisInput = { - title?: string; - description?: string; - addressLine?: string; - landmark?: string; - latitude?: number; - longitude?: number; - domainId?: string; - mediaTypes?: Array<"image" | "video" | "audio">; - visualSignals?: Array<{ - label: string; - confidence: number; - matchedSignals: string[]; - }>; -}; - -export type AiDomainSuggestion = { - domainId: string | null; - domainName: string; - confidence: number; - matchedSignals: string[]; -}; - -export type AiDuplicateMatch = { - complaintId: string; - complaintCode: string; - title: string; - distanceKm: number; - similarityScore: number; - status: string; -}; - -export type DraftAnalysisResult = { - suggestedDomain: AiDomainSuggestion | null; - suggestedPriority: "P1" | "P2" | "P3" | "P4"; - severityLevel: "critical" | "high" | "medium" | "low"; - emergencyLikelihood: "high" | "medium" | "low"; - duplicateRisk: "high" | "medium" | "low"; - routeDepartmentCode: string | null; - routeDepartmentName: string | null; - speechReady: boolean; - visionReady: boolean; - reasonSummary: string[]; - matchedKeywords: string[]; - duplicateMatches: AiDuplicateMatch[]; -}; diff --git a/civic-platform/backend/src/modules/auth/auth.repository.ts b/civic-platform/backend/src/modules/auth/auth.repository.ts deleted file mode 100644 index 86d21754a03677ced9128a9adc088279b13526ed..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/auth/auth.repository.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { db } from "../../db/pool.js"; -import type { AuthUser, RegisterInput } from "./auth.types.js"; - -type AuthUserRow = AuthUser & { - passwordHash: string | null; -}; - -export async function getUserForLogin(email: string): Promise { - const result = await db.query( - ` - SELECT - u.id, - u.full_name AS "fullName", - u.email, - r.name AS role, - u.department_id AS "departmentId", - u.ward_id AS "wardId", - u.password_hash AS "passwordHash" - FROM users u - INNER JOIN roles r ON r.id = u.role_id - WHERE lower(u.email) = lower($1) - AND u.is_active = TRUE - LIMIT 1 - `, - [email], - ); - - return result.rows[0] ?? null; -} - -export async function createCitizenUser(input: RegisterInput): Promise { - const result = await db.query( - ` - INSERT INTO users ( - full_name, - email, - phone, - password_hash, - role_id - ) - SELECT - $1, - lower($2), - $3, - $4, - r.id - FROM roles r - WHERE r.name = 'citizen' - RETURNING - id, - full_name AS "fullName", - email, - 'citizen' AS role, - department_id AS "departmentId", - ward_id AS "wardId", - password_hash AS "passwordHash" - `, - [input.fullName, input.email, input.phone ?? null, input.password], - ); - - const user = result.rows[0]; - - if (!user) { - throw new Error("Citizen role is not configured"); - } - - return user; -} diff --git a/civic-platform/backend/src/modules/auth/auth.routes.ts b/civic-platform/backend/src/modules/auth/auth.routes.ts deleted file mode 100644 index 6619029b72113e256d38573df4e835cd7b77afdf..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/auth/auth.routes.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Router } from "express"; -import { loginService, registerCitizenService } from "./auth.service.js"; - -export const authRouter = Router(); - -authRouter.post("/login", async (req, res) => { - try { - const user = await loginService({ - email: req.body.email, - password: req.body.password, - }); - - res.json({ data: user }); - } catch (error) { - res.status(401).json({ - error: error instanceof Error ? error.message : "Login failed", - }); - } -}); - -authRouter.post("/register", async (req, res) => { - try { - const user = await registerCitizenService({ - fullName: req.body.fullName, - email: req.body.email, - phone: req.body.phone, - password: req.body.password, - }); - - res.status(201).json({ data: user }); - } catch (error) { - const message = error instanceof Error ? error.message : "Registration failed"; - const status = message.includes("already exists") ? 409 : 400; - - res.status(status).json({ error: message }); - } -}); diff --git a/civic-platform/backend/src/modules/auth/auth.service.ts b/civic-platform/backend/src/modules/auth/auth.service.ts deleted file mode 100644 index 3fe0858b09c56d7fe9ebea12bdf6ab82329205b1..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/auth/auth.service.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { env } from "../../config/env.js"; -import { createCitizenUser, getUserForLogin } from "./auth.repository.js"; -import type { LoginInput, RegisterInput } from "./auth.types.js"; - -function validateLoginInput(input: Partial) { - if (!input.email) { - throw new Error("email is required"); - } - - if (!input.password) { - throw new Error("password is required"); - } -} - -function isValidPassword(storedHash: string | null, password: string) { - if (!storedHash) { - return false; - } - - if (storedHash === password) { - return true; - } - - if (storedHash === "dev-placeholder-hash" && password === env.demoAuthPassword) { - return true; - } - - return false; -} - -export async function loginService(input: Partial) { - validateLoginInput(input); - - const user = await getUserForLogin(input.email as string); - - if (!user || !isValidPassword(user.passwordHash, input.password as string)) { - throw new Error("Invalid email or password"); - } - - const { passwordHash: _passwordHash, ...sessionUser } = user; - return sessionUser; -} - -function validateRegisterInput(input: Partial) { - if (!input.fullName?.trim()) { - throw new Error("fullName is required"); - } - - if (!input.email?.trim()) { - throw new Error("email is required"); - } - - if (!input.password) { - throw new Error("password is required"); - } - - if (input.password.length < 8) { - throw new Error("Password must be at least 8 characters"); - } -} - -export async function registerCitizenService(input: Partial) { - validateRegisterInput(input); - - const existingUser = await getUserForLogin(input.email as string); - - if (existingUser) { - throw new Error("An account already exists for this email"); - } - - const user = await createCitizenUser({ - fullName: input.fullName!.trim(), - email: input.email!.trim().toLowerCase(), - phone: input.phone?.trim() || undefined, - password: input.password!, - }); - - const { passwordHash: _passwordHash, ...sessionUser } = user; - return sessionUser; -} diff --git a/civic-platform/backend/src/modules/auth/auth.types.ts b/civic-platform/backend/src/modules/auth/auth.types.ts deleted file mode 100644 index ed678d1d58fc6338aba2ceb105daa573a3675663..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/auth/auth.types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type AuthUser = { - id: string; - fullName: string; - email: string | null; - role: string; - departmentId: string | null; - wardId: string | null; -}; - -export type LoginInput = { - email: string; - password: string; -}; - -export type RegisterInput = { - fullName: string; - email: string; - phone?: string; - password: string; -}; diff --git a/civic-platform/backend/src/modules/complaints/.gitkeep b/civic-platform/backend/src/modules/complaints/.gitkeep deleted file mode 100644 index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/complaints/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/civic-platform/backend/src/modules/complaints/complaint.geo.ts b/civic-platform/backend/src/modules/complaints/complaint.geo.ts deleted file mode 100644 index e1f7925be055214b98d5e185975e2eb64e098e39..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/complaints/complaint.geo.ts +++ /dev/null @@ -1,75 +0,0 @@ -const wardCentroids = [ - { - id: "30000000-0000-0000-0000-000000000001", - code: "ward-4", - name: "Ward 4", - latitude: 17.4012, - longitude: 78.4749, - }, - { - id: "30000000-0000-0000-0000-000000000002", - code: "ward-8", - name: "Ward 8", - latitude: 17.3725, - longitude: 78.5028, - }, - { - id: "30000000-0000-0000-0000-000000000003", - code: "ward-12", - name: "Ward 12", - latitude: 17.3562, - longitude: 78.4728, - }, -] as const; - -function toRadians(value: number) { - return (value * Math.PI) / 180; -} - -export function calculateDistanceKm( - firstLatitude: number, - firstLongitude: number, - secondLatitude: number, - secondLongitude: number, -) { - const earthRadiusKm = 6371; - const latitudeDelta = toRadians(secondLatitude - firstLatitude); - const longitudeDelta = toRadians(secondLongitude - firstLongitude); - const originLatitude = toRadians(firstLatitude); - const targetLatitude = toRadians(secondLatitude); - - const a = - Math.sin(latitudeDelta / 2) ** 2 + - Math.sin(longitudeDelta / 2) ** 2 * Math.cos(originLatitude) * Math.cos(targetLatitude); - - return 2 * earthRadiusKm * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); -} - -export function inferWardFromCoordinates(latitude: number, longitude: number) { - const ranked = wardCentroids - .map((ward) => ({ - ...ward, - distanceKm: calculateDistanceKm(latitude, longitude, ward.latitude, ward.longitude), - })) - .sort((left, right) => left.distanceKm - right.distanceKm); - - const nearest = ranked[0]; - - if (!nearest) { - return null; - } - - return { - id: nearest.id, - code: nearest.code, - name: nearest.name, - distanceKm: nearest.distanceKm, - confidence: - nearest.distanceKm < 1.5 - ? "high" - : nearest.distanceKm < 3 - ? "medium" - : "low", - } as const; -} - diff --git a/civic-platform/backend/src/modules/complaints/complaint.repository.ts b/civic-platform/backend/src/modules/complaints/complaint.repository.ts deleted file mode 100644 index 2990d057b7c5827b3a077aef2ec7f214c7b779c2..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/complaints/complaint.repository.ts +++ /dev/null @@ -1,1902 +0,0 @@ -import type { PoolClient } from "pg"; -import { db } from "../../db/pool.js"; -import type { - AnalyticsSummary, - AssignComplaintInput, - ComplaintNotificationItem, - ComplaintAssignmentItem, - ComplaintNoteItem, - ComplaintDetail, - ComplaintHotspotItem, - DashboardSummary, - ComplaintFeedbackItem, - ComplaintListItem, - ComplaintMapItem, - ComplaintMediaItem, - NearbyComplaintItem, - ComplaintStatusHistoryItem, - CreateComplaintInput, - CreateComplaintNoteInput, - SubmitComplaintFeedbackInput, - UpdateAssignmentStatusInput, - UploadComplaintMediaInput, - UpdateComplaintStatusInput, -} from "./complaint.types.js"; -import { calculateDistanceKm, inferWardFromCoordinates } from "./complaint.geo.js"; -import { clearCache } from "../../lib/cache.js"; -import { writeAuditLog } from "../../lib/audit.js"; - -type DomainMeta = { - id: string; - name: string; - isEmergency: boolean; -}; - -type RoutingMatch = { - departmentId: string; - priorityOverride: string | null; -}; - -const fallbackDepartmentCodeByDomain: Record = { - "Roads and Transportation": "roads-public-works", - "Sanitation and Waste Management": "sanitation-solid-waste", - "Water Supply, Sewerage, and Drainage": "water-sewerage", - "Street Lighting and Electrical Infrastructure": "electrical-street-lighting", - "Public Infrastructure and Amenities": "municipal-maintenance", - "Environment and Public Health": "sanitation-solid-waste", - "Fire Emergencies": "disaster-management", - "Flood and Water Disaster": "disaster-management", - "Structural and Infrastructure Hazard": "disaster-management", - "Electrical Hazard": "disaster-management", - "Disaster and Rescue": "disaster-management", -}; - -const domainBasePriority: Record = { - "Roads and Transportation": "P3", - "Sanitation and Waste Management": "P3", - "Water Supply, Sewerage, and Drainage": "P2", - "Street Lighting and Electrical Infrastructure": "P3", - "Public Infrastructure and Amenities": "P2", - "Environment and Public Health": "P3", - "Fire Emergencies": "P1", - "Flood and Water Disaster": "P1", - "Structural and Infrastructure Hazard": "P1", - "Electrical Hazard": "P1", - "Disaster and Rescue": "P1", -}; - -function buildComplaintCode() { - const now = new Date(); - const year = now.getFullYear(); - const millis = now.getTime().toString().slice(-6); - - return `CP-${year}-${millis}`; -} - -function priorityToScore(priority: string) { - switch (priority) { - case "P1": - return 4; - case "P2": - return 3; - case "P3": - return 2; - default: - return 1; - } -} - -function scoreToPriority(score: number): "P1" | "P2" | "P3" | "P4" { - if (score >= 4) return "P1"; - if (score === 3) return "P2"; - if (score === 2) return "P3"; - return "P4"; -} - -function keywordPriorityBoost(input: CreateComplaintInput) { - const text = `${input.title} ${input.description ?? ""} ${input.addressLine ?? ""} ${input.landmark ?? ""}`.toLowerCase(); - - const criticalKeywords = [ - "fire", - "flood", - "collapse", - "collapsing", - "live wire", - "electrical hazard", - "electrocution", - "blast", - "explosion", - "rescue", - "landslide", - "open manhole", - ]; - - if (criticalKeywords.some((keyword) => text.includes(keyword))) { - return 4; - } - - const highKeywords = [ - "sewage", - "overflow", - "waterlogging", - "blocked drain", - "drainage", - "transformer", - "wire", - "pothole", - "road blockage", - "unsafe", - "hazard", - ]; - - if (highKeywords.some((keyword) => text.includes(keyword))) { - return 3; - } - - const mediumKeywords = ["garbage", "streetlight", "leak", "broken", "damaged", "mosquito", "fallen tree"]; - - if (mediumKeywords.some((keyword) => text.includes(keyword))) { - return 2; - } - - return 0; -} - -function sensitiveLocationBoost(input: CreateComplaintInput) { - const text = `${input.addressLine ?? ""} ${input.landmark ?? ""}`.toLowerCase(); - const sensitiveKeywords = ["school", "hospital", "junction", "bus stop", "market", "highway", "underpass"]; - - return sensitiveKeywords.some((keyword) => text.includes(keyword)) ? 1 : 0; -} - -async function getDomainMeta(client: PoolClient, domainId?: string) { - if (!domainId) { - return null; - } - - const result = await client.query( - ` - SELECT id, name, is_emergency AS "isEmergency" - FROM domains - WHERE id = $1 - LIMIT 1 - `, - [domainId], - ); - - return result.rows[0] ?? null; -} - -async function findRoutingRule(client: PoolClient, input: CreateComplaintInput, isEmergency: boolean) { - const result = await client.query( - ` - SELECT - rr.department_id AS "departmentId", - rr.priority_override AS "priorityOverride" - FROM routing_rules rr - WHERE rr.is_active = TRUE - AND (rr.domain_id IS NULL OR rr.domain_id = $1) - AND (rr.sub_problem_id IS NULL OR rr.sub_problem_id = $2) - AND (rr.ward_id IS NULL OR rr.ward_id = $3) - ORDER BY - CASE WHEN rr.domain_id = $1 THEN 1 ELSE 0 END DESC, - CASE WHEN rr.sub_problem_id = $2 THEN 1 ELSE 0 END DESC, - CASE WHEN rr.ward_id = $3 THEN 1 ELSE 0 END DESC, - CASE WHEN $4 = TRUE AND rr.is_emergency_route = TRUE THEN 1 ELSE 0 END DESC, - rr.created_at ASC - LIMIT 1 - `, - [input.domainId ?? null, input.subProblemId ?? null, input.wardId ?? null, isEmergency], - ); - - return result.rows[0] ?? null; -} - -async function getDepartmentIdByCode(client: PoolClient, code: string) { - const result = await client.query<{ id: string }>( - ` - SELECT id - FROM departments - WHERE code = $1 - LIMIT 1 - `, - [code], - ); - - return result.rows[0]?.id ?? null; -} - -async function findPriorityRule(client: PoolClient, input: CreateComplaintInput) { - const result = await client.query<{ basePriority: string }>( - ` - SELECT base_priority AS "basePriority" - FROM priority_rules - WHERE is_active = TRUE - AND (domain_id IS NULL OR domain_id = $1) - AND (sub_problem_id IS NULL OR sub_problem_id = $2) - ORDER BY - CASE WHEN domain_id = $1 THEN 1 ELSE 0 END DESC, - CASE WHEN sub_problem_id = $2 THEN 1 ELSE 0 END DESC, - created_at ASC - LIMIT 1 - `, - [input.domainId ?? null, input.subProblemId ?? null], - ); - - return result.rows[0]?.basePriority ?? null; -} - -async function deriveWorkflow(client: PoolClient, input: CreateComplaintInput) { - const domain = await getDomainMeta(client, input.domainId); - const isEmergency = Boolean(input.isEmergency || domain?.isEmergency); - const routingRule = await findRoutingRule(client, input, isEmergency); - - let departmentId = routingRule?.departmentId ?? null; - - if (!departmentId && domain?.name) { - const fallbackCode = fallbackDepartmentCodeByDomain[domain.name]; - - if (fallbackCode) { - departmentId = await getDepartmentIdByCode(client, fallbackCode); - } - } - - const baseFromRules = routingRule?.priorityOverride ?? (await findPriorityRule(client, input)); - const basePriority = - (baseFromRules as "P1" | "P2" | "P3" | "P4" | null) ?? - (domain?.name ? domainBasePriority[domain.name] : null) ?? - (isEmergency ? "P1" : "P3"); - - const derivedScore = Math.max( - priorityToScore(basePriority), - keywordPriorityBoost(input), - Math.min(4, priorityToScore(basePriority) + sensitiveLocationBoost(input)), - ); - - const priorityLevel = scoreToPriority(derivedScore); - const status = input.domainId ? "prioritized" : "submitted"; - - return { - departmentId, - isEmergency, - priorityLevel, - status, - domainName: domain?.name ?? null, - }; -} - -async function createNotification( - client: PoolClient, - input: { - userId: string; - complaintId?: string | null; - title: string; - message: string; - notificationType: string; - }, -) { - await client.query( - ` - INSERT INTO notifications ( - user_id, - complaint_id, - title, - message, - notification_type - ) - VALUES ($1, $2, $3, $4, $5) - `, - [input.userId, input.complaintId ?? null, input.title, input.message, input.notificationType], - ); -} - -async function createAuditLog( - client: PoolClient, - input: { - actorUserId?: string | null; - entityType: string; - entityId?: string | null; - action: string; - details?: Record; - }, -) { - await client.query( - ` - INSERT INTO audit_logs ( - actor_user_id, - entity_type, - entity_id, - action, - details - ) - VALUES ($1, $2, $3, $4, $5::jsonb) - `, - [ - input.actorUserId ?? null, - input.entityType, - input.entityId ?? null, - input.action, - JSON.stringify(input.details ?? {}), - ], - ); -} - -export async function createComplaint(input: CreateComplaintInput) { - const complaintCode = buildComplaintCode(); - const client = await db.connect(); - const inferredWard = input.wardId ? null : inferWardFromCoordinates(input.latitude, input.longitude); - const normalizedInput = { - ...input, - wardId: input.wardId ?? inferredWard?.id, - }; - - try { - await client.query("BEGIN"); - - const workflow = await deriveWorkflow(client, normalizedInput); - - const complaintResult = await client.query( - ` - INSERT INTO complaints ( - complaint_code, - citizen_id, - domain_id, - sub_problem_id, - title, - description, - status, - priority_level, - is_emergency, - department_id, - ward_id - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) - RETURNING id, complaint_code, status, priority_level, submitted_at - `, - [ - complaintCode, - normalizedInput.citizenId, - normalizedInput.domainId ?? null, - normalizedInput.subProblemId ?? null, - normalizedInput.title, - normalizedInput.description ?? null, - workflow.status, - workflow.priorityLevel, - workflow.isEmergency, - workflow.departmentId, - normalizedInput.wardId ?? null, - ], - ); - - const complaint = complaintResult.rows[0]; - - await client.query( - ` - INSERT INTO complaint_locations ( - complaint_id, - latitude, - longitude, - address_line, - landmark, - city_name, - state_name, - postal_code, - ward_id - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - `, - [ - complaint.id, - normalizedInput.latitude, - normalizedInput.longitude, - normalizedInput.addressLine ?? null, - normalizedInput.landmark ?? null, - normalizedInput.cityName ?? null, - normalizedInput.stateName ?? null, - normalizedInput.postalCode ?? null, - normalizedInput.wardId ?? null, - ], - ); - - await client.query( - ` - INSERT INTO complaint_status_history ( - complaint_id, - old_status, - new_status, - changed_by, - change_reason - ) - VALUES ($1, $2, $3, $4, $5) - `, - [complaint.id, null, "submitted", input.citizenId, "Complaint submitted by citizen"], - ); - - if (input.domainId) { - await client.query( - ` - INSERT INTO complaint_status_history ( - complaint_id, - old_status, - new_status, - changed_by, - change_reason - ) - VALUES ($1, $2, $3, $4, $5) - `, - [ - complaint.id, - "submitted", - "classified", - input.citizenId, - workflow.domainName ? `Complaint classified under ${workflow.domainName}` : "Complaint classified", - ], - ); - - await client.query( - ` - INSERT INTO complaint_status_history ( - complaint_id, - old_status, - new_status, - changed_by, - change_reason - ) - VALUES ($1, $2, $3, $4, $5) - `, - [ - complaint.id, - "classified", - workflow.status, - input.citizenId, - workflow.departmentId - ? `Priority ${workflow.priorityLevel} and routed to department queue` - : `Priority ${workflow.priorityLevel} assigned`, - ], - ); - } - - await createAuditLog(client, { - actorUserId: normalizedInput.citizenId, - entityType: "complaint", - entityId: complaint.id, - action: "complaint_created", - details: { - complaintCode, - priorityLevel: workflow.priorityLevel, - departmentId: workflow.departmentId, - wardId: normalizedInput.wardId ?? null, - isEmergency: workflow.isEmergency, - }, - }); - - await client.query("COMMIT"); - clearCache(); - return complaint; - } catch (error) { - await client.query("ROLLBACK"); - throw error; - } finally { - client.release(); - } -} - -export async function listComplaints(): Promise { - const result = await db.query( - ` - SELECT - c.id, - c.complaint_code AS "complaintCode", - c.title, - c.status, - c.priority_level AS "priorityLevel", - c.is_emergency AS "isEmergency", - c.submitted_at AS "submittedAt", - d.name AS "domainName", - dept.name AS "departmentName", - cl.address_line AS "addressLine", - cl.landmark - FROM complaints c - LEFT JOIN domains d ON d.id = c.domain_id - LEFT JOIN departments dept ON dept.id = c.department_id - LEFT JOIN complaint_locations cl ON cl.complaint_id = c.id - ORDER BY c.submitted_at DESC - `, - ); - - return result.rows; -} - -export async function listComplaintsByCitizen(citizenId: string): Promise { - const result = await db.query( - ` - SELECT - c.id, - c.complaint_code AS "complaintCode", - c.title, - c.status, - c.priority_level AS "priorityLevel", - c.is_emergency AS "isEmergency", - c.submitted_at AS "submittedAt", - d.name AS "domainName", - dept.name AS "departmentName", - cl.address_line AS "addressLine", - cl.landmark - FROM complaints c - LEFT JOIN domains d ON d.id = c.domain_id - LEFT JOIN departments dept ON dept.id = c.department_id - LEFT JOIN complaint_locations cl ON cl.complaint_id = c.id - WHERE c.citizen_id = $1 - ORDER BY c.submitted_at DESC - `, - [citizenId], - ); - - return result.rows; -} - -export async function listMapComplaints(options?: { - publicOnly?: boolean; - status?: string; - domainId?: string; - priorityLevel?: string; - emergencyOnly?: boolean; -}): Promise { - const result = await db.query( - ` - SELECT - c.id, - c.complaint_code AS "complaintCode", - c.title, - c.status, - c.priority_level AS "priorityLevel", - c.is_emergency AS "isEmergency", - d.name AS "domainName", - dept.name AS "departmentName", - w.name AS "wardName", - cl.latitude::text AS latitude, - cl.longitude::text AS longitude, - cl.address_line AS "addressLine", - cl.landmark, - c.submitted_at AS "submittedAt" - FROM complaints c - LEFT JOIN domains d ON d.id = c.domain_id - LEFT JOIN departments dept ON dept.id = c.department_id - INNER JOIN complaint_locations cl ON cl.complaint_id = c.id - LEFT JOIN wards w ON w.id = COALESCE(c.ward_id, cl.ward_id) - WHERE ($1::boolean = FALSE OR c.status NOT IN ('rejected', 'duplicate', 'on_hold')) - AND ($2::text IS NULL OR c.status = $2) - AND ($3::uuid IS NULL OR c.domain_id = $3) - AND ($4::text IS NULL OR c.priority_level = $4) - AND ($5::boolean = FALSE OR c.is_emergency = TRUE) - ORDER BY c.submitted_at DESC - `, - [ - options?.publicOnly ?? false, - options?.status ?? null, - options?.domainId ?? null, - options?.priorityLevel ?? null, - options?.emergencyOnly ?? false, - ], - ); - - return result.rows; -} - -export async function listPublicHotspots(): Promise { - const complaints = await listMapComplaints({ publicOnly: true }); - const hotspotMap = new Map< - string, - { - latitudeSum: number; - longitudeSum: number; - complaintCount: number; - criticalCount: number; - emergencyCount: number; - domainCounts: Map; - } - >(); - - for (const complaint of complaints) { - const latitude = Number(complaint.latitude); - const longitude = Number(complaint.longitude); - const hotspotKey = `${latitude.toFixed(2)}:${longitude.toFixed(2)}`; - const current = hotspotMap.get(hotspotKey) ?? { - latitudeSum: 0, - longitudeSum: 0, - complaintCount: 0, - criticalCount: 0, - emergencyCount: 0, - domainCounts: new Map(), - }; - - current.latitudeSum += latitude; - current.longitudeSum += longitude; - current.complaintCount += 1; - current.criticalCount += complaint.priorityLevel === "P1" ? 1 : 0; - current.emergencyCount += complaint.isEmergency ? 1 : 0; - - if (complaint.domainName) { - current.domainCounts.set(complaint.domainName, (current.domainCounts.get(complaint.domainName) ?? 0) + 1); - } - - hotspotMap.set(hotspotKey, current); - } - - return [...hotspotMap.entries()] - .map(([hotspotKey, value]) => { - const topDomain = - [...value.domainCounts.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? null; - - return { - hotspotKey, - latitude: value.latitudeSum / value.complaintCount, - longitude: value.longitudeSum / value.complaintCount, - complaintCount: value.complaintCount, - criticalCount: value.criticalCount, - emergencyCount: value.emergencyCount, - topDomain, - }; - }) - .sort((left, right) => right.complaintCount - left.complaintCount) - .slice(0, 12); -} - -export async function listNearbyComplaints(input: { - latitude: number; - longitude: number; - radiusKm: number; - domainId?: string; - publicOnly?: boolean; -}): Promise { - const complaints = await listMapComplaints({ - publicOnly: input.publicOnly ?? true, - domainId: input.domainId, - }); - - return complaints - .map((complaint) => { - const distanceKm = calculateDistanceKm( - input.latitude, - input.longitude, - Number(complaint.latitude), - Number(complaint.longitude), - ); - - return { - id: complaint.id, - complaintCode: complaint.complaintCode, - title: complaint.title, - status: complaint.status, - priorityLevel: complaint.priorityLevel, - domainName: complaint.domainName, - distanceKm, - addressLine: complaint.addressLine, - landmark: complaint.landmark, - }; - }) - .filter((complaint) => complaint.distanceKm <= input.radiusKm) - .sort((left, right) => left.distanceKm - right.distanceKm) - .slice(0, 8); -} - -export async function getDashboardSummary(): Promise { - const [overviewResult, urgentQueueResult, departmentWorkloadResult] = await Promise.all([ - db.query( - ` - SELECT - COUNT(*) FILTER (WHERE status NOT IN ('closed', 'resolved', 'citizen_verified', 'rejected', 'duplicate'))::int AS "openComplaints", - COUNT(*) FILTER (WHERE priority_level = 'P1' AND status NOT IN ('closed', 'resolved', 'citizen_verified', 'rejected', 'duplicate'))::int AS "criticalComplaints", - COUNT(*) FILTER ( - WHERE status NOT IN ('closed', 'resolved', 'citizen_verified', 'rejected', 'duplicate') - AND ( - reopened_count > 0 - OR submitted_at < NOW() - INTERVAL '24 hours' - ) - )::int AS "slaRisk", - COUNT(*) FILTER (WHERE resolved_at >= CURRENT_DATE)::int AS "resolvedToday" - FROM complaints - `, - ), - db.query( - ` - SELECT - c.id, - c.complaint_code AS "complaintCode", - c.title, - d.name AS "domainName", - c.priority_level AS "priorityLevel", - COALESCE(cl.address_line, cl.landmark, dept.name, 'Location pending') AS location, - c.submitted_at AS "submittedAt" - FROM complaints c - LEFT JOIN domains d ON d.id = c.domain_id - LEFT JOIN complaint_locations cl ON cl.complaint_id = c.id - LEFT JOIN departments dept ON dept.id = c.department_id - WHERE c.status NOT IN ('closed', 'resolved', 'citizen_verified', 'rejected', 'duplicate') - ORDER BY - CASE c.priority_level - WHEN 'P1' THEN 1 - WHEN 'P2' THEN 2 - WHEN 'P3' THEN 3 - ELSE 4 - END, - c.submitted_at ASC - LIMIT 5 - `, - ), - db.query( - ` - SELECT - COALESCE(dept.name, 'Unassigned') AS "departmentName", - COUNT(*)::int AS "pendingCount" - FROM complaints c - LEFT JOIN departments dept ON dept.id = c.department_id - WHERE c.status NOT IN ('closed', 'resolved', 'citizen_verified', 'rejected', 'duplicate') - GROUP BY COALESCE(dept.name, 'Unassigned') - ORDER BY "pendingCount" DESC, "departmentName" ASC - LIMIT 6 - `, - ), - ]); - - return { - overview: overviewResult.rows[0] ?? { - openComplaints: 0, - criticalComplaints: 0, - slaRisk: 0, - resolvedToday: 0, - }, - urgentQueue: urgentQueueResult.rows, - departmentWorkload: departmentWorkloadResult.rows, - }; -} - -export async function getAnalyticsSummary(): Promise { - const [trendRows, domainRows, departmentRows, kpiRows] = await Promise.all([ - db.query( - ` - WITH days AS ( - SELECT generate_series(CURRENT_DATE - INTERVAL '6 days', CURRENT_DATE, INTERVAL '1 day')::date AS day - ) - SELECT - to_char(days.day, 'YYYY-MM-DD') AS day, - COUNT(c.id) FILTER (WHERE c.submitted_at::date = days.day)::int AS "reportedCount", - COUNT(c.id) FILTER (WHERE c.resolved_at IS NOT NULL AND c.resolved_at::date = days.day)::int AS "resolvedCount" - FROM days - LEFT JOIN complaints c - ON c.submitted_at::date = days.day - OR (c.resolved_at IS NOT NULL AND c.resolved_at::date = days.day) - GROUP BY days.day - ORDER BY days.day ASC - `, - ), - db.query( - ` - SELECT - COALESCE(d.name, 'Unclassified') AS "domainName", - COUNT(*)::int AS "complaintCount" - FROM complaints c - LEFT JOIN domains d ON d.id = c.domain_id - GROUP BY COALESCE(d.name, 'Unclassified') - ORDER BY "complaintCount" DESC, "domainName" ASC - LIMIT 8 - `, - ), - db.query( - ` - SELECT - COALESCE(dept.name, 'Unassigned') AS "departmentName", - COUNT(*) FILTER (WHERE c.resolved_at IS NOT NULL)::int AS "resolvedCount", - COUNT(*) FILTER (WHERE c.status NOT IN ('closed', 'resolved', 'citizen_verified', 'rejected', 'duplicate'))::int AS "pendingCount", - COALESCE( - ROUND( - AVG( - EXTRACT(EPOCH FROM (c.resolved_at - c.submitted_at)) / 3600 - ) FILTER (WHERE c.resolved_at IS NOT NULL) - , 2), - 0 - )::float AS "averageResolutionHours" - FROM complaints c - LEFT JOIN departments dept ON dept.id = c.department_id - GROUP BY COALESCE(dept.name, 'Unassigned') - ORDER BY "pendingCount" DESC, "resolvedCount" DESC - LIMIT 8 - `, - ), - db.query( - ` - SELECT - COUNT(*) FILTER (WHERE is_emergency = TRUE)::int AS "emergencyCount", - COUNT(*) FILTER (WHERE reopened_count > 0)::int AS "reopenedCount", - COUNT(*) FILTER (WHERE reopened_count > 1)::int AS "repeatComplaintRiskCount", - COUNT(*) FILTER ( - WHERE status NOT IN ('closed', 'resolved', 'citizen_verified', 'rejected', 'duplicate') - AND submitted_at < NOW() - INTERVAL '24 hours' - )::int AS "slaBreaches" - FROM complaints - `, - ), - ]); - - return { - trends: trendRows.rows, - domainBreakdown: domainRows.rows, - departmentPerformance: departmentRows.rows, - kpis: kpiRows.rows[0] ?? { - emergencyCount: 0, - reopenedCount: 0, - repeatComplaintRiskCount: 0, - slaBreaches: 0, - }, - }; -} - -export async function exportAnalyticsCsv() { - const analytics = await getAnalyticsSummary(); - const lines = [ - "section,name,value,secondary", - ...analytics.domainBreakdown.map((row) => `domain,${row.domainName},${row.complaintCount},`), - ...analytics.departmentPerformance.map( - (row) => - `department,${row.departmentName},${row.pendingCount},avg_resolution_hours:${row.averageResolutionHours}`, - ), - ...analytics.trends.map((row) => `trend,${row.day},reported:${row.reportedCount},resolved:${row.resolvedCount}`), - `kpi,emergency_count,${analytics.kpis.emergencyCount},`, - `kpi,reopened_count,${analytics.kpis.reopenedCount},`, - `kpi,repeat_risk_count,${analytics.kpis.repeatComplaintRiskCount},`, - `kpi,sla_breaches,${analytics.kpis.slaBreaches},`, - ]; - - return lines.join("\n"); -} - -export async function getComplaintById(id: string): Promise { - const result = await db.query( - ` - SELECT - c.id, - c.complaint_code AS "complaintCode", - c.title, - c.description, - c.status, - c.priority_level AS "priorityLevel", - c.is_emergency AS "isEmergency", - c.submitted_at AS "submittedAt", - c.department_id AS "departmentId", - c.ward_id AS "wardId", - c.citizen_id AS "citizenId", - d.name AS "domainName", - dept.name AS "departmentName", - cl.latitude, - cl.longitude, - cl.address_line AS "addressLine", - cl.landmark - FROM complaints c - LEFT JOIN domains d ON d.id = c.domain_id - LEFT JOIN departments dept ON dept.id = c.department_id - LEFT JOIN complaint_locations cl ON cl.complaint_id = c.id - WHERE c.id = $1 OR c.complaint_code = $1 - LIMIT 1 - `, - [id], - ); - - return result.rows[0] ?? null; -} - -export async function getComplaintStatusHistory(id: string): Promise { - const result = await db.query( - ` - SELECT - csh.id, - csh.complaint_id AS "complaintId", - csh.old_status AS "oldStatus", - csh.new_status AS "newStatus", - csh.changed_by AS "changedBy", - csh.change_reason AS "changeReason", - csh.created_at AS "createdAt" - FROM complaint_status_history csh - INNER JOIN complaints c ON c.id = csh.complaint_id - WHERE c.id = $1 OR c.complaint_code = $1 - ORDER BY csh.created_at ASC - `, - [id], - ); - - return result.rows; -} - -export async function uploadComplaintMedia(input: UploadComplaintMediaInput): Promise { - const complaintResult = await db.query( - ` - SELECT id - FROM complaints - WHERE id = $1 OR complaint_code = $1 - LIMIT 1 - `, - [input.complaintId], - ); - - const complaint = complaintResult.rows[0]; - - if (!complaint) { - return null; - } - - const result = await db.query( - ` - INSERT INTO complaint_media ( - complaint_id, - uploaded_by, - media_type, - file_path, - file_url, - mime_type, - is_resolution_proof - ) - VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING - id, - complaint_id AS "complaintId", - media_type AS "mediaType", - file_url AS "fileUrl", - mime_type AS "mimeType", - is_resolution_proof AS "isResolutionProof", - created_at AS "createdAt" - `, - [ - complaint.id, - input.uploadedBy ?? null, - input.mediaType, - input.filePath, - input.fileUrl, - input.mimeType ?? null, - input.isResolutionProof ?? false, - ], - ); - - return result.rows[0] ?? null; -} - -export async function listComplaintMedia(id: string): Promise { - const result = await db.query( - ` - SELECT - cm.id, - cm.complaint_id AS "complaintId", - cm.media_type AS "mediaType", - cm.file_url AS "fileUrl", - cm.mime_type AS "mimeType", - cm.is_resolution_proof AS "isResolutionProof", - cm.created_at AS "createdAt" - FROM complaint_media cm - INNER JOIN complaints c ON c.id = cm.complaint_id - WHERE c.id = $1 OR c.complaint_code = $1 - ORDER BY cm.created_at ASC - `, - [id], - ); - - return result.rows; -} - -export async function listComplaintFeedback(id: string): Promise { - const result = await db.query( - ` - SELECT - f.id, - f.complaint_id AS "complaintId", - f.citizen_id AS "citizenId", - f.rating, - f.comment, - f.reopen_requested AS "reopenRequested", - f.created_at AS "createdAt" - FROM feedback f - INNER JOIN complaints c ON c.id = f.complaint_id - WHERE c.id = $1 OR c.complaint_code = $1 - ORDER BY f.created_at DESC - `, - [id], - ); - - return result.rows; -} - -export async function listNotificationsByUser(userId: string): Promise { - const result = await db.query( - ` - SELECT - n.id, - n.user_id AS "userId", - n.complaint_id AS "complaintId", - n.title, - n.message, - n.notification_type AS "notificationType", - n.is_read AS "isRead", - n.created_at AS "createdAt" - FROM notifications n - WHERE n.user_id = $1 - ORDER BY n.is_read ASC, n.created_at DESC - `, - [userId], - ); - - return result.rows; -} - -export async function markNotificationAsRead(notificationId: string, userId: string) { - const result = await db.query( - ` - UPDATE notifications - SET is_read = TRUE - WHERE id = $1 - AND user_id = $2 - RETURNING - id, - user_id AS "userId", - complaint_id AS "complaintId", - title, - message, - notification_type AS "notificationType", - is_read AS "isRead", - created_at AS "createdAt" - `, - [notificationId, userId], - ); - - return result.rows[0] ?? null; -} - -export async function markAllNotificationsAsRead(userId: string) { - const result = await db.query( - ` - UPDATE notifications - SET is_read = TRUE - WHERE user_id = $1 - AND is_read = FALSE - RETURNING id - `, - [userId], - ); - - return { - userId, - updatedCount: result.rowCount ?? 0, - }; -} - -export async function listAssignmentsByOfficer(userId: string): Promise { - const result = await db.query( - ` - SELECT - a.id, - a.complaint_id AS "complaintId", - c.complaint_code AS "complaintCode", - c.title AS "complaintTitle", - a.department_id AS "departmentId", - d.name AS "departmentName", - a.assigned_to_user_id AS "assignedToUserId", - assigned_to.full_name AS "assignedToName", - a.assigned_by_user_id AS "assignedByUserId", - assigned_by.full_name AS "assignedByName", - a.assignment_status AS "assignmentStatus", - a.assigned_at AS "assignedAt", - a.accepted_at AS "acceptedAt", - a.completed_at AS "completedAt", - COUNT(cm.id)::int AS "resolutionProofCount", - a.notes - FROM assignments a - INNER JOIN complaints c ON c.id = a.complaint_id - INNER JOIN departments d ON d.id = a.department_id - LEFT JOIN users assigned_to ON assigned_to.id = a.assigned_to_user_id - LEFT JOIN users assigned_by ON assigned_by.id = a.assigned_by_user_id - LEFT JOIN complaint_media cm - ON cm.complaint_id = a.complaint_id - AND cm.is_resolution_proof = TRUE - WHERE a.assigned_to_user_id = $1 - GROUP BY - a.id, - c.complaint_code, - c.title, - d.name, - assigned_to.full_name, - assigned_by.full_name - ORDER BY - CASE a.assignment_status - WHEN 'assigned' THEN 1 - WHEN 'accepted' THEN 2 - WHEN 'in_progress' THEN 3 - ELSE 4 - END, - a.assigned_at DESC - `, - [userId], - ); - - return result.rows; -} - -export async function listComplaintAssignments(id: string): Promise { - const result = await db.query( - ` - SELECT - a.id, - a.complaint_id AS "complaintId", - c.complaint_code AS "complaintCode", - c.title AS "complaintTitle", - a.department_id AS "departmentId", - d.name AS "departmentName", - a.assigned_to_user_id AS "assignedToUserId", - assigned_to.full_name AS "assignedToName", - a.assigned_by_user_id AS "assignedByUserId", - assigned_by.full_name AS "assignedByName", - a.assignment_status AS "assignmentStatus", - a.assigned_at AS "assignedAt", - a.accepted_at AS "acceptedAt", - a.completed_at AS "completedAt", - COUNT(cm.id)::int AS "resolutionProofCount", - a.notes - FROM assignments a - INNER JOIN complaints c ON c.id = a.complaint_id - INNER JOIN departments d ON d.id = a.department_id - LEFT JOIN users assigned_to ON assigned_to.id = a.assigned_to_user_id - LEFT JOIN users assigned_by ON assigned_by.id = a.assigned_by_user_id - LEFT JOIN complaint_media cm - ON cm.complaint_id = a.complaint_id - AND cm.is_resolution_proof = TRUE - WHERE c.id = $1 OR c.complaint_code = $1 - GROUP BY - a.id, - c.complaint_code, - c.title, - d.name, - assigned_to.full_name, - assigned_by.full_name - ORDER BY a.assigned_at DESC - `, - [id], - ); - - return result.rows; -} - -export async function getAssignmentById(assignmentId: string) { - const result = await db.query( - ` - SELECT - a.id, - a.complaint_id AS "complaintId", - a.department_id AS "departmentId", - a.assigned_to_user_id AS "assignedToUserId", - a.assigned_by_user_id AS "assignedByUserId", - a.assignment_status AS "assignmentStatus", - c.status AS "complaintStatus" - FROM assignments a - INNER JOIN complaints c ON c.id = a.complaint_id - WHERE a.id = $1 - LIMIT 1 - `, - [assignmentId], - ); - - return result.rows[0] ?? null; -} - -export async function complaintHasResolutionProof(complaintId: string) { - const result = await db.query<{ hasProof: boolean }>( - ` - SELECT EXISTS ( - SELECT 1 - FROM complaint_media - WHERE complaint_id = $1 - AND is_resolution_proof = TRUE - ) AS "hasProof" - `, - [complaintId], - ); - - return result.rows[0]?.hasProof ?? false; -} - -export async function hasOfficerAssignment(complaintId: string, officerId: string) { - const result = await db.query<{ hasAssignment: boolean }>( - ` - SELECT EXISTS ( - SELECT 1 - FROM assignments - WHERE complaint_id = $1 - AND assigned_to_user_id = $2 - ) AS "hasAssignment" - `, - [complaintId, officerId], - ); - - return result.rows[0]?.hasAssignment ?? false; -} - -export async function listComplaintNotes(id: string): Promise { - const result = await db.query( - ` - SELECT - cn.id, - cn.complaint_id AS "complaintId", - cn.author_id AS "authorId", - cn.note_type AS "noteType", - cn.note_text AS "noteText", - cn.is_internal AS "isInternal", - cn.created_at AS "createdAt", - u.full_name AS "authorName" - FROM complaint_notes cn - INNER JOIN complaints c ON c.id = cn.complaint_id - LEFT JOIN users u ON u.id = cn.author_id - WHERE c.id = $1 OR c.complaint_code = $1 - ORDER BY cn.created_at DESC - `, - [id], - ); - - return result.rows; -} - -export async function createComplaintNote(input: CreateComplaintNoteInput): Promise { - const complaintResult = await db.query( - ` - SELECT id - FROM complaints - WHERE id = $1 OR complaint_code = $1 - LIMIT 1 - `, - [input.complaintId], - ); - - const complaint = complaintResult.rows[0]; - - if (!complaint) { - return null; - } - - const result = await db.query( - ` - INSERT INTO complaint_notes ( - complaint_id, - author_id, - note_type, - note_text, - is_internal - ) - VALUES ($1, $2, $3, $4, $5) - RETURNING - id, - complaint_id AS "complaintId", - author_id AS "authorId", - note_type AS "noteType", - note_text AS "noteText", - is_internal AS "isInternal", - created_at AS "createdAt" - `, - [ - complaint.id, - input.authorId, - input.noteType, - input.noteText, - input.isInternal ?? true, - ], - ); - - const note = result.rows[0]; - - if (!note) { - return null; - } - - const authorResult = await db.query<{ fullName: string }>( - ` - SELECT full_name AS "fullName" - FROM users - WHERE id = $1 - LIMIT 1 - `, - [input.authorId], - ); - - await writeAuditLog({ - actorUserId: input.authorId, - entityType: "complaint_note", - entityId: note.id, - action: "complaint_note_created", - details: { - complaintId: complaint.id, - noteType: input.noteType, - isInternal: input.isInternal ?? true, - }, - }); - clearCache(); - - return { - ...note, - authorName: authorResult.rows[0]?.fullName ?? null, - }; -} - -export async function submitComplaintFeedback(input: SubmitComplaintFeedbackInput): Promise { - const client = await db.connect(); - - try { - await client.query("BEGIN"); - - const complaintResult = await client.query( - ` - SELECT id, status - FROM complaints - WHERE id = $1 OR complaint_code = $1 - LIMIT 1 - `, - [input.complaintId], - ); - - const complaint = complaintResult.rows[0]; - - if (!complaint) { - await client.query("ROLLBACK"); - return null; - } - - const feedbackResult = await client.query( - ` - INSERT INTO feedback ( - complaint_id, - citizen_id, - rating, - comment, - reopen_requested - ) - VALUES ($1, $2, $3, $4, $5) - RETURNING - id, - complaint_id AS "complaintId", - citizen_id AS "citizenId", - rating, - comment, - reopen_requested AS "reopenRequested", - created_at AS "createdAt" - `, - [ - complaint.id, - input.citizenId, - input.rating ?? null, - input.comment ?? null, - input.reopenRequested ?? false, - ], - ); - - if (input.reopenRequested) { - await client.query( - ` - UPDATE complaints - SET - status = 'reopened', - updated_at = NOW(), - reopened_count = reopened_count + 1 - WHERE id = $1 - `, - [complaint.id], - ); - - await client.query( - ` - INSERT INTO complaint_status_history ( - complaint_id, - old_status, - new_status, - changed_by, - change_reason - ) - VALUES ($1, $2, 'reopened', $3, $4) - `, - [ - complaint.id, - complaint.status, - input.citizenId, - input.comment?.trim() || "Complaint reopened by citizen after follow-up feedback", - ], - ); - - const ownerResult = await client.query<{ assignedByUserId: string | null }>( - ` - SELECT assigned_by_user_id AS "assignedByUserId" - FROM assignments - WHERE complaint_id = $1 - ORDER BY assigned_at DESC - LIMIT 1 - `, - [complaint.id], - ); - - if (ownerResult.rows[0]?.assignedByUserId) { - await createNotification(client, { - userId: ownerResult.rows[0].assignedByUserId, - complaintId: complaint.id, - title: "Complaint reopened by citizen", - message: input.comment?.trim() || "A citizen reopened the complaint after follow-up.", - notificationType: "complaint_reopened", - }); - } - } - - await createAuditLog(client, { - actorUserId: input.citizenId, - entityType: "feedback", - entityId: feedbackResult.rows[0]?.id ?? null, - action: input.reopenRequested ? "complaint_reopened_by_feedback" : "complaint_feedback_submitted", - details: { - complaintId: complaint.id, - rating: input.rating ?? null, - }, - }); - - await client.query("COMMIT"); - clearCache(); - return feedbackResult.rows[0] ?? null; - } catch (error) { - await client.query("ROLLBACK"); - throw error; - } finally { - client.release(); - } -} - -export async function updateComplaintStatus(input: UpdateComplaintStatusInput) { - const currentResult = await db.query( - ` - SELECT id, status - FROM complaints - WHERE id = $1 OR complaint_code = $1 - LIMIT 1 - `, - [input.complaintId], - ); - - const currentComplaint = currentResult.rows[0]; - - if (!currentComplaint) { - return null; - } - - const updatedResult = await db.query( - ` - UPDATE complaints - SET - status = $2, - updated_at = NOW(), - resolved_at = CASE WHEN $2 = 'resolved' THEN NOW() ELSE resolved_at END, - closed_at = CASE WHEN $2 = 'closed' THEN NOW() ELSE closed_at END, - reopened_count = CASE WHEN $2 = 'reopened' THEN reopened_count + 1 ELSE reopened_count END - WHERE id = $1 - RETURNING id, complaint_code AS "complaintCode", status, updated_at AS "updatedAt" - `, - [currentComplaint.id, input.newStatus], - ); - - await db.query( - ` - INSERT INTO complaint_status_history ( - complaint_id, - old_status, - new_status, - changed_by, - change_reason - ) - VALUES ($1, $2, $3, $4, $5) - `, - [ - currentComplaint.id, - currentComplaint.status, - input.newStatus, - input.changedBy, - input.reason ?? null, - ], - ); - - await writeAuditLog({ - actorUserId: input.changedBy, - entityType: "complaint", - entityId: currentComplaint.id, - action: "complaint_status_updated", - details: { - newStatus: input.newStatus, - oldStatus: currentComplaint.status, - }, - }); - clearCache(); - - return updatedResult.rows[0]; -} - -export async function updateAssignmentStatus(input: UpdateAssignmentStatusInput) { - const client = await db.connect(); - - try { - await client.query("BEGIN"); - - const assignmentResult = await client.query( - ` - SELECT - a.id, - a.complaint_id AS "complaintId", - a.assignment_status AS "assignmentStatus", - a.assigned_to_user_id AS "assignedToUserId", - a.assigned_by_user_id AS "assignedByUserId", - c.status AS "complaintStatus", - c.citizen_id AS "citizenId", - c.complaint_code AS "complaintCode" - FROM assignments a - INNER JOIN complaints c ON c.id = a.complaint_id - WHERE a.id = $1 - LIMIT 1 - `, - [input.assignmentId], - ); - - const assignment = assignmentResult.rows[0]; - - if (!assignment) { - await client.query("ROLLBACK"); - return null; - } - - await client.query( - ` - UPDATE assignments - SET - assignment_status = $2, - accepted_at = CASE WHEN $2 = 'accepted' AND accepted_at IS NULL THEN NOW() ELSE accepted_at END, - completed_at = CASE WHEN $2 = 'completed' THEN NOW() ELSE completed_at END, - notes = COALESCE($3, notes) - WHERE id = $1 - `, - [input.assignmentId, input.newStatus, input.notes ?? null], - ); - - let complaintStatus = assignment.complaintStatus as string; - if (input.newStatus === "accepted") complaintStatus = "accepted"; - if (input.newStatus === "in_progress") complaintStatus = "in_progress"; - if (input.newStatus === "completed") complaintStatus = "resolved"; - - await client.query( - ` - UPDATE complaints - SET - status = $2, - updated_at = NOW(), - resolved_at = CASE WHEN $2 = 'resolved' THEN NOW() ELSE resolved_at END - WHERE id = $1 - `, - [assignment.complaintId, complaintStatus], - ); - - await client.query( - ` - INSERT INTO complaint_status_history ( - complaint_id, - old_status, - new_status, - changed_by, - change_reason - ) - VALUES ($1, $2, $3, $4, $5) - `, - [ - assignment.complaintId, - assignment.complaintStatus, - complaintStatus, - input.changedByUserId, - input.notes ?? - (input.newStatus === "completed" - ? "Field officer marked the assignment as completed" - : `Assignment updated to ${input.newStatus}`), - ], - ); - - await createNotification(client, { - userId: assignment.citizenId, - complaintId: assignment.complaintId, - title: - input.newStatus === "completed" - ? "Complaint resolved by field team" - : input.newStatus === "in_progress" - ? "Field work started" - : "Assignment accepted", - message: - input.newStatus === "completed" - ? `Complaint ${assignment.complaintCode} was marked resolved and is ready for your review.` - : input.newStatus === "in_progress" - ? `Field work has started for complaint ${assignment.complaintCode}.` - : `A field officer accepted complaint ${assignment.complaintCode}.`, - notificationType: - input.newStatus === "completed" - ? "complaint_resolved" - : input.newStatus === "in_progress" - ? "field_work_started" - : "assignment_accepted", - }); - - if (assignment.assignedByUserId) { - await createNotification(client, { - userId: assignment.assignedByUserId, - complaintId: assignment.complaintId, - title: - input.newStatus === "completed" - ? "Field team completed work" - : input.newStatus === "in_progress" - ? "Field team started work" - : input.newStatus === "reassigned" - ? "Assignment was reassigned" - : "Field team accepted assignment", - message: - input.newStatus === "completed" - ? `The field team completed complaint ${assignment.complaintCode}. Citizen verification can begin.` - : input.newStatus === "in_progress" - ? `A field officer started work on complaint ${assignment.complaintCode}.` - : input.newStatus === "reassigned" - ? `Complaint ${assignment.complaintCode} was flagged for reassignment.` - : `A field officer accepted complaint ${assignment.complaintCode}.`, - notificationType: - input.newStatus === "completed" - ? "field_work_completed" - : input.newStatus === "in_progress" - ? "field_work_started_internal" - : input.newStatus === "reassigned" - ? "assignment_reassigned" - : "assignment_accepted_internal", - }); - } - - await createAuditLog(client, { - actorUserId: input.changedByUserId, - entityType: "assignment", - entityId: assignment.id, - action: "assignment_status_updated", - details: { - complaintId: assignment.complaintId, - oldStatus: assignment.assignmentStatus, - newStatus: input.newStatus, - complaintStatus, - }, - }); - - await client.query("COMMIT"); - clearCache(); - - return { - id: assignment.id, - complaintId: assignment.complaintId, - assignmentStatus: input.newStatus, - complaintStatus, - }; - } catch (error) { - await client.query("ROLLBACK"); - throw error; - } finally { - client.release(); - } -} - -export async function verifyComplaintByCitizen(input: { - complaintId: string; - citizenId: string; - comment?: string; -}) { - const client = await db.connect(); - - try { - await client.query("BEGIN"); - - const complaintResult = await client.query( - ` - SELECT id, status, complaint_code AS "complaintCode" - FROM complaints - WHERE (id = $1 OR complaint_code = $1) AND citizen_id = $2 - LIMIT 1 - `, - [input.complaintId, input.citizenId], - ); - - const complaint = complaintResult.rows[0]; - - if (!complaint) { - await client.query("ROLLBACK"); - return null; - } - - await client.query( - ` - UPDATE complaints - SET - status = 'citizen_verified', - updated_at = NOW() - WHERE id = $1 - `, - [complaint.id], - ); - - await client.query( - ` - INSERT INTO complaint_status_history ( - complaint_id, - old_status, - new_status, - changed_by, - change_reason - ) - VALUES ($1, $2, 'citizen_verified', $3, $4) - `, - [ - complaint.id, - complaint.status, - input.citizenId, - input.comment?.trim() || "Citizen confirmed that the issue is resolved", - ], - ); - - const ownerResult = await client.query<{ assignedByUserId: string | null }>( - ` - SELECT assigned_by_user_id AS "assignedByUserId" - FROM assignments - WHERE complaint_id = $1 - ORDER BY assigned_at DESC - LIMIT 1 - `, - [complaint.id], - ); - - if (ownerResult.rows[0]?.assignedByUserId) { - await createNotification(client, { - userId: ownerResult.rows[0].assignedByUserId, - complaintId: complaint.id, - title: "Citizen verified resolution", - message: `Complaint ${complaint.complaintCode} was verified by the citizen.`, - notificationType: "citizen_verified", - }); - } - - await createAuditLog(client, { - actorUserId: input.citizenId, - entityType: "complaint", - entityId: complaint.id, - action: "complaint_verified_by_citizen", - details: { - oldStatus: complaint.status, - }, - }); - - await client.query("COMMIT"); - clearCache(); - - return { - complaintId: complaint.id, - status: "citizen_verified", - }; - } catch (error) { - await client.query("ROLLBACK"); - throw error; - } finally { - client.release(); - } -} - -export async function assignComplaint(input: AssignComplaintInput) { - const client = await db.connect(); - - try { - await client.query("BEGIN"); - - const complaintResult = await client.query( - ` - SELECT id, status, citizen_id AS "citizenId", complaint_code AS "complaintCode" - FROM complaints - WHERE id = $1 OR complaint_code = $1 - LIMIT 1 - `, - [input.complaintId], - ); - - const complaint = complaintResult.rows[0]; - - if (!complaint) { - await client.query("ROLLBACK"); - return null; - } - - const assignmentResult = await client.query( - ` - INSERT INTO assignments ( - complaint_id, - department_id, - assigned_to_user_id, - assigned_by_user_id, - assignment_status, - notes - ) - VALUES ($1, $2, $3, $4, 'assigned', $5) - RETURNING id, complaint_id AS "complaintId", department_id AS "departmentId", assigned_at AS "assignedAt" - `, - [ - complaint.id, - input.departmentId, - input.assignedToUserId ?? null, - input.assignedByUserId, - input.notes ?? null, - ], - ); - - await client.query( - ` - UPDATE complaints - SET - department_id = $2, - status = 'assigned', - updated_at = NOW() - WHERE id = $1 - `, - [complaint.id, input.departmentId], - ); - - await client.query( - ` - INSERT INTO complaint_status_history ( - complaint_id, - old_status, - new_status, - changed_by, - change_reason - ) - VALUES ($1, $2, 'assigned', $3, $4) - `, - [ - complaint.id, - complaint.status, - input.assignedByUserId, - input.notes ?? "Complaint assigned to department", - ], - ); - - await createNotification(client, { - userId: complaint.citizenId, - complaintId: complaint.id, - title: "Complaint assigned", - message: `Complaint ${complaint.complaintCode} has been assigned to a department team.`, - notificationType: "complaint_assigned", - }); - - if (input.assignedToUserId) { - await createNotification(client, { - userId: input.assignedToUserId, - complaintId: complaint.id, - title: "New field assignment", - message: `You were assigned complaint ${complaint.complaintCode}.`, - notificationType: "field_assignment", - }); - } - - await createAuditLog(client, { - actorUserId: input.assignedByUserId, - entityType: "assignment", - entityId: assignmentResult.rows[0]?.id ?? null, - action: "complaint_assigned", - details: { - complaintId: complaint.id, - departmentId: input.departmentId, - assignedToUserId: input.assignedToUserId ?? null, - }, - }); - - await client.query("COMMIT"); - clearCache(); - return assignmentResult.rows[0]; - } catch (error) { - await client.query("ROLLBACK"); - throw error; - } finally { - client.release(); - } -} diff --git a/civic-platform/backend/src/modules/complaints/complaint.routes.ts b/civic-platform/backend/src/modules/complaints/complaint.routes.ts deleted file mode 100644 index 54deea81d237a1fe74c01b80d40bbba7bde4f493..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/complaints/complaint.routes.ts +++ /dev/null @@ -1,466 +0,0 @@ -import { randomUUID } from "node:crypto"; -import fs from "node:fs"; -import path from "node:path"; -import { Router } from "express"; -import multer from "multer"; -import { - assignComplaintService, - createComplaintService, - createComplaintNoteService, - getDashboardSummaryService, - getAnalyticsSummaryService, - getMapComplaintsService, - getNearbyComplaintsService, - markAllNotificationsReadService, - markNotificationReadService, - getNotificationsService, - getOfficerAssignmentsService, - getPublicHotspotsService, - exportAnalyticsCsvService, - getComplaintAssignmentsService, - getComplaintDetailService, - getComplaintFeedbackService, - getComplaintMediaService, - getComplaintNotesService, - getComplaintStatusHistoryService, - listCitizenComplaintsService, - listComplaintsService, - submitComplaintFeedbackService, - updateAssignmentStatusService, - uploadComplaintMediaService, - updateComplaintStatusService, - verifyComplaintByCitizenService, -} from "./complaint.service.js"; - -export const complaintRouter = Router(); - -const uploadsRoot = path.resolve(process.cwd(), "uploads", "complaints"); -fs.mkdirSync(uploadsRoot, { recursive: true }); - -const upload = multer({ - storage: multer.diskStorage({ - destination: (_req, _file, callback) => { - callback(null, uploadsRoot); - }, - filename: (_req, file, callback) => { - const safeExtension = path.extname(file.originalname) || ""; - callback(null, `${Date.now()}-${randomUUID()}${safeExtension}`); - }, - }), - limits: { - fileSize: 25 * 1024 * 1024, - }, -}); - -function singleValue(value: string | string[] | undefined) { - return Array.isArray(value) ? value[0] : value; -} - -function normalizeMediaType(value: string | undefined) { - if (value === "image" || value === "video" || value === "audio") { - return value; - } - - return undefined; -} - -complaintRouter.get("/", async (_req, res) => { - try { - const complaints = await listComplaintsService(); - res.json({ data: complaints }); - } catch (error) { - res.status(500).json({ - error: error instanceof Error ? error.message : "Failed to fetch complaints", - }); - } -}); - -complaintRouter.get("/summary/dashboard", async (_req, res) => { - try { - const summary = await getDashboardSummaryService(); - res.json({ data: summary }); - } catch (error) { - res.status(500).json({ - error: error instanceof Error ? error.message : "Failed to fetch dashboard summary", - }); - } -}); - -complaintRouter.get("/summary/analytics", async (_req, res) => { - try { - const summary = await getAnalyticsSummaryService(); - res.json({ data: summary }); - } catch (error) { - res.status(500).json({ - error: error instanceof Error ? error.message : "Failed to fetch analytics summary", - }); - } -}); - -complaintRouter.get("/summary/export.csv", async (_req, res) => { - try { - const csv = await exportAnalyticsCsvService(); - res.setHeader("Content-Type", "text/csv; charset=utf-8"); - res.setHeader("Content-Disposition", "attachment; filename=\"civicpulse-analytics.csv\""); - res.send(csv); - } catch (error) { - res.status(500).json({ - error: error instanceof Error ? error.message : "Failed to export analytics", - }); - } -}); - -complaintRouter.get("/map", async (req, res) => { - try { - const complaints = await getMapComplaintsService({ - publicOnly: req.query.publicOnly === "true", - status: typeof req.query.status === "string" ? req.query.status : undefined, - domainId: typeof req.query.domainId === "string" ? req.query.domainId : undefined, - priorityLevel: typeof req.query.priorityLevel === "string" ? req.query.priorityLevel : undefined, - emergencyOnly: req.query.emergencyOnly === "true", - }); - - res.json({ data: complaints }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch map complaints", - }); - } -}); - -complaintRouter.get("/public/hotspots", async (_req, res) => { - try { - const hotspots = await getPublicHotspotsService(); - res.json({ data: hotspots }); - } catch (error) { - res.status(500).json({ - error: error instanceof Error ? error.message : "Failed to fetch hotspots", - }); - } -}); - -complaintRouter.get("/nearby", async (req, res) => { - try { - const nearby = await getNearbyComplaintsService({ - latitude: typeof req.query.latitude === "string" ? Number(req.query.latitude) : undefined, - longitude: typeof req.query.longitude === "string" ? Number(req.query.longitude) : undefined, - radiusKm: typeof req.query.radiusKm === "string" ? Number(req.query.radiusKm) : undefined, - domainId: typeof req.query.domainId === "string" ? req.query.domainId : undefined, - publicOnly: req.query.publicOnly !== "false", - }); - - res.json({ data: nearby }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch nearby complaints", - }); - } -}); - -complaintRouter.get("/citizen/:citizenId", async (req, res) => { - try { - const complaints = await listCitizenComplaintsService(req.params.citizenId); - res.json({ data: complaints }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch citizen complaints", - }); - } -}); - -complaintRouter.get("/officer/:officerId/assignments", async (req, res) => { - try { - const assignments = await getOfficerAssignmentsService(req.params.officerId); - res.json({ data: assignments }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch officer assignments", - }); - } -}); - -complaintRouter.get("/notifications/user/:userId", async (req, res) => { - try { - const notifications = await getNotificationsService(req.params.userId); - res.json({ data: notifications }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch notifications", - }); - } -}); - -complaintRouter.patch("/notifications/:notificationId/read", async (req, res) => { - try { - const notification = await markNotificationReadService(req.params.notificationId, req.body.userId); - - if (!notification) { - res.status(404).json({ error: "Notification not found" }); - return; - } - - res.json({ data: notification }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to update notification", - }); - } -}); - -complaintRouter.post("/notifications/user/:userId/read-all", async (req, res) => { - try { - const result = await markAllNotificationsReadService(req.params.userId); - res.json({ data: result }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to update notifications", - }); - } -}); - -complaintRouter.get("/:id", async (req, res) => { - try { - const complaint = await getComplaintDetailService(req.params.id); - - if (!complaint) { - res.status(404).json({ error: "Complaint not found" }); - return; - } - - res.json({ data: complaint }); - } catch (error) { - res.status(500).json({ - error: error instanceof Error ? error.message : "Failed to fetch complaint detail", - }); - } -}); - -complaintRouter.get("/:id/history", async (req, res) => { - try { - const history = await getComplaintStatusHistoryService(req.params.id); - res.json({ data: history }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch complaint history", - }); - } -}); - -complaintRouter.get("/:id/media", async (req, res) => { - try { - const media = await getComplaintMediaService(req.params.id); - res.json({ data: media }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch complaint media", - }); - } -}); - -complaintRouter.get("/:id/feedback", async (req, res) => { - try { - const feedback = await getComplaintFeedbackService(req.params.id); - res.json({ data: feedback }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch complaint feedback", - }); - } -}); - -complaintRouter.get("/:id/assignments", async (req, res) => { - try { - const assignments = await getComplaintAssignmentsService(req.params.id); - res.json({ data: assignments }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch complaint assignments", - }); - } -}); - -complaintRouter.get("/:id/notes", async (req, res) => { - try { - const notes = await getComplaintNotesService(req.params.id); - res.json({ data: notes }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch complaint notes", - }); - } -}); - -complaintRouter.post("/", async (req, res) => { - try { - const complaint = await createComplaintService(req.body); - res.status(201).json({ data: complaint }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to create complaint", - }); - } -}); - -complaintRouter.post("/:id/notes", async (req, res) => { - try { - const note = await createComplaintNoteService({ - complaintId: req.params.id, - authorId: req.body.authorId, - noteType: req.body.noteType, - noteText: req.body.noteText, - isInternal: req.body.isInternal, - }); - - if (!note) { - res.status(404).json({ error: "Complaint not found" }); - return; - } - - res.status(201).json({ data: note }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to create complaint note", - }); - } -}); - -complaintRouter.patch("/:id/status", async (req, res) => { - try { - const complaint = await updateComplaintStatusService({ - complaintId: req.params.id, - newStatus: req.body.newStatus, - changedBy: req.body.changedBy, - reason: req.body.reason, - }); - - if (!complaint) { - res.status(404).json({ error: "Complaint not found" }); - return; - } - - res.json({ data: complaint }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to update complaint status", - }); - } -}); - -complaintRouter.post("/:id/assign", async (req, res) => { - try { - const assignment = await assignComplaintService({ - complaintId: req.params.id, - departmentId: req.body.departmentId, - assignedToUserId: req.body.assignedToUserId, - assignedByUserId: req.body.assignedByUserId, - notes: req.body.notes, - }); - - if (!assignment) { - res.status(404).json({ error: "Complaint not found" }); - return; - } - - res.status(201).json({ data: assignment }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to assign complaint", - }); - } -}); - -complaintRouter.patch("/assignments/:assignmentId/status", async (req, res) => { - try { - const assignment = await updateAssignmentStatusService({ - assignmentId: req.params.assignmentId, - newStatus: req.body.newStatus, - changedByUserId: req.body.changedByUserId, - notes: req.body.notes, - }); - - if (!assignment) { - res.status(404).json({ error: "Assignment not found" }); - return; - } - - res.json({ data: assignment }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to update assignment status", - }); - } -}); - -complaintRouter.post("/:id/media", upload.single("file"), async (req, res) => { - try { - if (!req.file) { - res.status(400).json({ error: "File is required" }); - return; - } - - const media = await uploadComplaintMediaService({ - complaintId: String(req.params.id), - uploadedBy: singleValue(req.body.uploadedBy), - mediaType: normalizeMediaType(singleValue(req.body.mediaType)), - filePath: req.file.path, - fileUrl: `${req.protocol}://${req.get("host")}/uploads/complaints/${req.file.filename}`, - mimeType: req.file.mimetype, - isResolutionProof: singleValue(req.body.isResolutionProof) === "true", - }); - - if (!media) { - res.status(404).json({ error: "Complaint not found" }); - return; - } - - res.status(201).json({ data: media }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to upload complaint media", - }); - } -}); - -complaintRouter.post("/:id/feedback", async (req, res) => { - try { - const feedback = await submitComplaintFeedbackService({ - complaintId: req.params.id, - citizenId: req.body.citizenId, - rating: req.body.rating, - comment: req.body.comment, - reopenRequested: req.body.reopenRequested, - }); - - if (!feedback) { - res.status(404).json({ error: "Complaint not found" }); - return; - } - - res.status(201).json({ data: feedback }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to submit complaint feedback", - }); - } -}); - -complaintRouter.post("/:id/verify", async (req, res) => { - try { - const verification = await verifyComplaintByCitizenService({ - complaintId: req.params.id, - citizenId: req.body.citizenId, - comment: req.body.comment, - }); - - if (!verification) { - res.status(404).json({ error: "Complaint not found" }); - return; - } - - res.status(201).json({ data: verification }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to verify complaint", - }); - } -}); diff --git a/civic-platform/backend/src/modules/complaints/complaint.service.ts b/civic-platform/backend/src/modules/complaints/complaint.service.ts deleted file mode 100644 index 6402f37bc91f5f142f8b5a7896679c0ed0771360..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/complaints/complaint.service.ts +++ /dev/null @@ -1,590 +0,0 @@ -import type { - AnalyticsSummary, - AssignComplaintInput, - ComplaintHotspotItem, - ComplaintMapItem, - ComplaintNotificationItem, - CreateComplaintNoteInput, - CreateComplaintInput, - NearbyComplaintItem, - SubmitComplaintFeedbackInput, - UpdateAssignmentStatusInput, - UpdateComplaintStatusInput, - UploadComplaintMediaInput, -} from "./complaint.types.js"; -import { - assignComplaint, - complaintHasResolutionProof, - createComplaintNote, - createComplaint, - exportAnalyticsCsv, - getAssignmentById, - getAnalyticsSummary, - getDashboardSummary, - listAssignmentsByOfficer, - listMapComplaints, - listNearbyComplaints, - listNotificationsByUser, - listComplaintAssignments, - getComplaintById, - listComplaintFeedback, - listPublicHotspots, - listComplaintMedia, - listComplaintNotes, - getComplaintStatusHistory, - listComplaints, - listComplaintsByCitizen, - markAllNotificationsAsRead, - markNotificationAsRead, - submitComplaintFeedback, - hasOfficerAssignment, - updateAssignmentStatus, - uploadComplaintMedia, - updateComplaintStatus, - verifyComplaintByCitizen, -} from "./complaint.repository.js"; -import { getUserContext } from "../users/user.repository.js"; -import { sanitizeText } from "../../lib/security.js"; -import { withCache } from "../../lib/cache.js"; - -function validateCreateComplaintInput(input: Partial) { - const missing: string[] = []; - - if (!input.citizenId) missing.push("citizenId"); - if (!input.title) missing.push("title"); - if (typeof input.latitude !== "number") missing.push("latitude"); - if (typeof input.longitude !== "number") missing.push("longitude"); - - if (missing.length > 0) { - throw new Error(`Missing required complaint fields: ${missing.join(", ")}`); - } -} - -function isOperatorRole(role: string) { - return role === "department_operator" || role === "municipal_admin"; -} - -async function requireUserContext(userId: string) { - const user = await getUserContext(userId); - - if (!user) { - throw new Error("User not found"); - } - - return user; -} - -export async function createComplaintService(input: Partial) { - validateCreateComplaintInput(input); - - const citizen = await requireUserContext(input.citizenId as string); - - if (citizen.role !== "citizen") { - throw new Error("Only citizens can create complaints"); - } - - return createComplaint({ - ...(input as CreateComplaintInput), - title: sanitizeText(input.title, 220) ?? "", - description: sanitizeText(input.description, 2000), - addressLine: sanitizeText(input.addressLine, 220), - landmark: sanitizeText(input.landmark, 220), - cityName: sanitizeText(input.cityName, 120), - stateName: sanitizeText(input.stateName, 120), - postalCode: sanitizeText(input.postalCode, 20), - }); -} - -export async function listComplaintsService() { - return listComplaints(); -} - -export async function getDashboardSummaryService() { - return withCache("analytics:dashboard", 15000, () => getDashboardSummary()); -} - -export async function getMapComplaintsService(input: { - publicOnly?: boolean; - status?: string; - domainId?: string; - priorityLevel?: string; - emergencyOnly?: boolean; -}): Promise { - return listMapComplaints(input); -} - -export async function getPublicHotspotsService(): Promise { - return withCache("analytics:hotspots", 15000, () => listPublicHotspots()); -} - -export async function getAnalyticsSummaryService(): Promise { - return withCache("analytics:summary", 20000, () => getAnalyticsSummary()); -} - -export async function exportAnalyticsCsvService() { - return withCache("analytics:csv", 20000, () => exportAnalyticsCsv()); -} - -export async function getNearbyComplaintsService(input: { - latitude?: number; - longitude?: number; - radiusKm?: number; - domainId?: string; - publicOnly?: boolean; -}): Promise { - if (typeof input.latitude !== "number" || Number.isNaN(input.latitude)) { - throw new Error("latitude is required"); - } - - if (typeof input.longitude !== "number" || Number.isNaN(input.longitude)) { - throw new Error("longitude is required"); - } - - return listNearbyComplaints({ - latitude: input.latitude, - longitude: input.longitude, - radiusKm: input.radiusKm ?? 0.75, - domainId: input.domainId, - publicOnly: input.publicOnly ?? true, - }); -} - -export async function listCitizenComplaintsService(citizenId: string) { - if (!citizenId) { - throw new Error("citizenId is required"); - } - - return listComplaintsByCitizen(citizenId); -} - -export async function getComplaintDetailService(id: string) { - if (!id) { - throw new Error("Complaint id is required"); - } - - return getComplaintById(id); -} - -export async function getComplaintStatusHistoryService(id: string) { - if (!id) { - throw new Error("Complaint id is required"); - } - - return getComplaintStatusHistory(id); -} - -export async function getComplaintMediaService(id: string) { - if (!id) { - throw new Error("Complaint id is required"); - } - - return listComplaintMedia(id); -} - -export async function getComplaintFeedbackService(id: string) { - if (!id) { - throw new Error("Complaint id is required"); - } - - return listComplaintFeedback(id); -} - -export async function getComplaintAssignmentsService(id: string) { - if (!id) { - throw new Error("Complaint id is required"); - } - - return listComplaintAssignments(id); -} - -export async function getOfficerAssignmentsService(userId: string) { - if (!userId) { - throw new Error("userId is required"); - } - - return listAssignmentsByOfficer(userId); -} - -export async function getNotificationsService(userId: string): Promise { - if (!userId) { - throw new Error("userId is required"); - } - - return listNotificationsByUser(userId); -} - -export async function markNotificationReadService(notificationId: string, userId: string) { - if (!notificationId) { - throw new Error("notificationId is required"); - } - - if (!userId) { - throw new Error("userId is required"); - } - - return markNotificationAsRead(notificationId, userId); -} - -export async function markAllNotificationsReadService(userId: string) { - if (!userId) { - throw new Error("userId is required"); - } - - return markAllNotificationsAsRead(userId); -} - -export async function getComplaintNotesService(id: string) { - if (!id) { - throw new Error("Complaint id is required"); - } - - return listComplaintNotes(id); -} - -const allowedStatuses = new Set([ - "submitted", - "validated", - "classified", - "prioritized", - "assigned", - "accepted", - "in_progress", - "resolved", - "citizen_verified", - "closed", - "escalated", - "reopened", - "duplicate", - "rejected", - "on_hold", -]); - -const allowedTransitions: Record = { - submitted: ["validated", "escalated", "rejected"], - validated: ["classified", "prioritized", "assigned", "escalated"], - classified: ["prioritized", "assigned"], - prioritized: ["assigned", "escalated", "on_hold"], - assigned: ["accepted", "in_progress", "escalated", "on_hold"], - accepted: ["in_progress", "escalated", "on_hold"], - in_progress: ["resolved", "escalated", "on_hold"], - resolved: ["citizen_verified", "closed", "reopened"], - citizen_verified: ["closed"], - reopened: ["assigned", "in_progress", "escalated"], - escalated: ["assigned", "in_progress", "resolved"], - on_hold: ["assigned", "in_progress", "escalated"], - rejected: ["reopened"], - duplicate: ["reopened"], - closed: ["reopened"], -}; - -export async function updateComplaintStatusService(input: Partial) { - if (!input.complaintId) { - throw new Error("complaintId is required"); - } - - if (!input.changedBy) { - throw new Error("changedBy is required"); - } - - if (!input.newStatus || !allowedStatuses.has(input.newStatus)) { - throw new Error("newStatus is invalid"); - } - - const actor = await requireUserContext(input.changedBy); - - if (!isOperatorRole(actor.role)) { - throw new Error("Only operators and municipal admins can update complaint status"); - } - - const complaint = await getComplaintById(input.complaintId); - - if (!complaint) { - return null; - } - - const nextStatuses = allowedTransitions[complaint.status] ?? []; - - if (!nextStatuses.includes(input.newStatus)) { - throw new Error(`Cannot move complaint from ${complaint.status} to ${input.newStatus}`); - } - - return updateComplaintStatus(input as UpdateComplaintStatusInput); -} - -export async function assignComplaintService(input: Partial) { - if (!input.complaintId) { - throw new Error("complaintId is required"); - } - - if (!input.departmentId) { - throw new Error("departmentId is required"); - } - - if (!input.assignedByUserId) { - throw new Error("assignedByUserId is required"); - } - - const actor = await requireUserContext(input.assignedByUserId); - - if (!isOperatorRole(actor.role)) { - throw new Error("Only operators and municipal admins can assign complaints"); - } - - if (actor.role === "department_operator" && actor.departmentId !== input.departmentId) { - throw new Error("Department operators can only assign complaints within their own department"); - } - - if (input.assignedToUserId) { - const assignee = await requireUserContext(input.assignedToUserId); - - if (assignee.role !== "field_officer") { - throw new Error("Assigned user must be a field officer"); - } - - if (assignee.departmentId !== input.departmentId) { - throw new Error("Field officer must belong to the selected department"); - } - } - - return assignComplaint(input as AssignComplaintInput); -} - -const allowedAssignmentStatuses = new Set(["accepted", "in_progress", "completed", "reassigned"]); - -export async function updateAssignmentStatusService(input: Partial) { - if (!input.assignmentId) { - throw new Error("assignmentId is required"); - } - - if (!input.changedByUserId) { - throw new Error("changedByUserId is required"); - } - - if (!input.newStatus || !allowedAssignmentStatuses.has(input.newStatus)) { - throw new Error("newStatus is invalid"); - } - - const actor = await requireUserContext(input.changedByUserId); - const assignment = await getAssignmentById(input.assignmentId); - - if (!assignment) { - return null; - } - - if (actor.role === "field_officer") { - if (assignment.assignedToUserId !== actor.id) { - throw new Error("Field officers can only update assignments assigned to them"); - } - - if (input.newStatus === "reassigned") { - throw new Error("Field officers cannot reassign tasks"); - } - - if (input.newStatus === "completed") { - const hasProof = await complaintHasResolutionProof(assignment.complaintId); - - if (!hasProof) { - throw new Error("Upload at least one resolution proof before marking the task as completed"); - } - } - } else if (isOperatorRole(actor.role)) { - if (actor.role === "department_operator" && actor.departmentId !== assignment.departmentId) { - throw new Error("Department operators can only manage assignments within their department"); - } - - if (input.newStatus !== "reassigned") { - throw new Error("Operators can only use this action to reassign work"); - } - } else { - throw new Error("Only field officers, operators, and municipal admins can update assignments"); - } - - return updateAssignmentStatus(input as UpdateAssignmentStatusInput); -} - -const allowedMediaTypes = new Set(["image", "video", "audio"]); - -export async function uploadComplaintMediaService(input: Partial) { - if (!input.complaintId) { - throw new Error("complaintId is required"); - } - - if (!input.filePath) { - throw new Error("filePath is required"); - } - - if (!input.fileUrl) { - throw new Error("fileUrl is required"); - } - - if (!input.mediaType || !allowedMediaTypes.has(input.mediaType)) { - throw new Error("mediaType is invalid"); - } - - if (input.mimeType) { - const mimeAllowed = - (input.mediaType === "image" && input.mimeType.startsWith("image/")) || - (input.mediaType === "video" && input.mimeType.startsWith("video/")) || - (input.mediaType === "audio" && input.mimeType.startsWith("audio/")); - - if (!mimeAllowed) { - throw new Error("Uploaded file type does not match the selected media type"); - } - } - - if (input.uploadedBy) { - const actor = await requireUserContext(input.uploadedBy); - - if (input.isResolutionProof) { - if (actor.role === "citizen") { - throw new Error("Citizens cannot upload resolution proof"); - } - - if (actor.role === "field_officer") { - const hasAssignment = await hasOfficerAssignment(input.complaintId, actor.id); - - if (!hasAssignment) { - throw new Error("Field officers can only upload proof for complaints assigned to them"); - } - } - } - } - - return uploadComplaintMedia(input as UploadComplaintMediaInput); -} - -export async function submitComplaintFeedbackService(input: Partial) { - if (!input.complaintId) { - throw new Error("complaintId is required"); - } - - if (!input.citizenId) { - throw new Error("citizenId is required"); - } - - if (input.rating !== undefined && (input.rating < 1 || input.rating > 5)) { - throw new Error("rating must be between 1 and 5"); - } - - const citizen = await requireUserContext(input.citizenId); - - if (citizen.role !== "citizen") { - throw new Error("Only citizens can submit complaint feedback"); - } - - const complaint = await getComplaintById(input.complaintId); - - if (!complaint || complaint.citizenId !== input.citizenId) { - throw new Error("Complaint does not belong to this citizen"); - } - - if (input.reopenRequested && !["resolved", "closed", "citizen_verified"].includes(complaint.status)) { - throw new Error("Complaints can only be reopened after they have been resolved or closed"); - } - - return submitComplaintFeedback({ - ...(input as SubmitComplaintFeedbackInput), - comment: sanitizeText(input.comment, 1200), - }); -} - -export async function verifyComplaintByCitizenService(input: { - complaintId?: string; - citizenId?: string; - comment?: string; -}) { - if (!input.complaintId) { - throw new Error("complaintId is required"); - } - - if (!input.citizenId) { - throw new Error("citizenId is required"); - } - - const citizen = await requireUserContext(input.citizenId); - - if (citizen.role !== "citizen") { - throw new Error("Only citizens can verify resolved complaints"); - } - - const complaint = await getComplaintById(input.complaintId); - - if (!complaint || complaint.citizenId !== input.citizenId) { - throw new Error("Complaint does not belong to this citizen"); - } - - if (complaint.status !== "resolved") { - throw new Error("Only resolved complaints can be verified by the citizen"); - } - - return verifyComplaintByCitizen({ - complaintId: input.complaintId, - citizenId: input.citizenId, - comment: sanitizeText(input.comment, 1200), - }); -} - -const allowedNoteTypes = new Set(["operator_note", "field_note", "citizen_note", "system_note"]); - -export async function createComplaintNoteService(input: Partial) { - if (!input.complaintId) { - throw new Error("complaintId is required"); - } - - if (!input.authorId) { - throw new Error("authorId is required"); - } - - if (!input.noteText?.trim()) { - throw new Error("noteText is required"); - } - - if (!input.noteType || !allowedNoteTypes.has(input.noteType)) { - throw new Error("noteType is invalid"); - } - - const author = await requireUserContext(input.authorId); - const complaint = await getComplaintById(input.complaintId); - - if (!complaint) { - return null; - } - - if (input.noteType === "system_note") { - throw new Error("System notes cannot be created manually"); - } - - if (input.noteType === "citizen_note") { - if (author.role !== "citizen" || complaint.citizenId !== author.id) { - throw new Error("Only the reporting citizen can add a citizen note"); - } - } - - if (input.noteType === "operator_note" && !isOperatorRole(author.role)) { - throw new Error("Only operators and municipal admins can add operator notes"); - } - - if (input.noteType === "field_note") { - if (author.role !== "field_officer") { - throw new Error("Only field officers can add field notes"); - } - - const hasAssignment = await hasOfficerAssignment(input.complaintId, author.id); - - if (!hasAssignment) { - throw new Error("Field notes can only be added to complaints assigned to the field officer"); - } - } - - if ((input.isInternal ?? true) && author.role === "citizen") { - throw new Error("Citizens cannot create internal notes"); - } - - return createComplaintNote({ - complaintId: input.complaintId, - authorId: input.authorId, - noteText: sanitizeText(input.noteText, 2000) ?? "", - noteType: input.noteType as CreateComplaintNoteInput["noteType"], - isInternal: input.isInternal ?? true, - }); -} diff --git a/civic-platform/backend/src/modules/complaints/complaint.types.ts b/civic-platform/backend/src/modules/complaints/complaint.types.ts deleted file mode 100644 index 60f9e5b22ba967b1fe587b295bb832fdb9317976..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/complaints/complaint.types.ts +++ /dev/null @@ -1,245 +0,0 @@ -export type CreateComplaintInput = { - citizenId: string; - title: string; - description?: string; - domainId?: string; - subProblemId?: string; - isEmergency?: boolean; - wardId?: string; - latitude: number; - longitude: number; - addressLine?: string; - landmark?: string; - cityName?: string; - stateName?: string; - postalCode?: string; -}; - -export type ComplaintListItem = { - id: string; - complaintCode: string; - title: string; - status: string; - priorityLevel: string; - isEmergency: boolean; - submittedAt: string; - domainName: string | null; - departmentName: string | null; - addressLine: string | null; - landmark: string | null; -}; - -export type ComplaintDetail = ComplaintListItem & { - description: string | null; - departmentId: string | null; - wardId: string | null; - citizenId: string; - latitude: string | null; - longitude: string | null; - addressLine: string | null; - landmark: string | null; -}; - -export type ComplaintStatusHistoryItem = { - id: string; - complaintId: string; - oldStatus: string | null; - newStatus: string; - changedBy: string | null; - changeReason: string | null; - createdAt: string; -}; - -export type UpdateComplaintStatusInput = { - complaintId: string; - newStatus: string; - changedBy: string; - reason?: string; -}; - -export type AssignComplaintInput = { - complaintId: string; - departmentId: string; - assignedToUserId?: string; - assignedByUserId: string; - notes?: string; -}; - -export type ComplaintAssignmentItem = { - id: string; - complaintId: string; - complaintCode: string | null; - complaintTitle: string | null; - departmentId: string; - departmentName: string; - assignedToUserId: string | null; - assignedToName: string | null; - assignedByUserId: string | null; - assignedByName: string | null; - assignmentStatus: string; - assignedAt: string; - acceptedAt: string | null; - completedAt: string | null; - resolutionProofCount: number; - notes: string | null; -}; - -export type UpdateAssignmentStatusInput = { - assignmentId: string; - newStatus: "accepted" | "in_progress" | "completed" | "reassigned"; - changedByUserId: string; - notes?: string; -}; - -export type UploadComplaintMediaInput = { - complaintId: string; - uploadedBy?: string; - mediaType: "image" | "video" | "audio"; - filePath: string; - fileUrl: string; - mimeType?: string; - isResolutionProof?: boolean; -}; - -export type ComplaintMediaItem = { - id: string; - complaintId: string; - mediaType: "image" | "video" | "audio"; - fileUrl: string | null; - mimeType: string | null; - isResolutionProof: boolean; - createdAt: string; -}; - -export type ComplaintMapItem = { - id: string; - complaintCode: string; - title: string; - status: string; - priorityLevel: string; - isEmergency: boolean; - domainName: string | null; - departmentName: string | null; - wardName: string | null; - latitude: string; - longitude: string; - addressLine: string | null; - landmark: string | null; - submittedAt: string; -}; - -export type ComplaintHotspotItem = { - hotspotKey: string; - latitude: number; - longitude: number; - complaintCount: number; - criticalCount: number; - emergencyCount: number; - topDomain: string | null; -}; - -export type NearbyComplaintItem = { - id: string; - complaintCode: string; - title: string; - status: string; - priorityLevel: string; - domainName: string | null; - distanceKm: number; - addressLine: string | null; - landmark: string | null; -}; - -export type SubmitComplaintFeedbackInput = { - complaintId: string; - citizenId: string; - rating?: number; - comment?: string; - reopenRequested?: boolean; -}; - -export type ComplaintFeedbackItem = { - id: string; - complaintId: string; - citizenId: string; - rating: number | null; - comment: string | null; - reopenRequested: boolean; - createdAt: string; -}; - -export type ComplaintNotificationItem = { - id: string; - userId: string; - complaintId: string | null; - title: string; - message: string; - notificationType: string; - isRead: boolean; - createdAt: string; -}; - -export type DashboardSummary = { - overview: { - openComplaints: number; - criticalComplaints: number; - slaRisk: number; - resolvedToday: number; - }; - urgentQueue: Array<{ - id: string; - complaintCode: string; - title: string; - domainName: string | null; - priorityLevel: string; - location: string | null; - submittedAt: string; - }>; - departmentWorkload: Array<{ - departmentName: string; - pendingCount: number; - }>; -}; - -export type AnalyticsSummary = { - trends: Array<{ - day: string; - reportedCount: number; - resolvedCount: number; - }>; - domainBreakdown: Array<{ - domainName: string; - complaintCount: number; - }>; - departmentPerformance: Array<{ - departmentName: string; - resolvedCount: number; - pendingCount: number; - averageResolutionHours: number; - }>; - kpis: { - emergencyCount: number; - reopenedCount: number; - repeatComplaintRiskCount: number; - slaBreaches: number; - }; -}; - -export type ComplaintNoteItem = { - id: string; - complaintId: string; - authorId: string | null; - noteType: "operator_note" | "field_note" | "citizen_note" | "system_note"; - noteText: string; - isInternal: boolean; - createdAt: string; - authorName: string | null; -}; - -export type CreateComplaintNoteInput = { - complaintId: string; - authorId: string; - noteType: "operator_note" | "field_note" | "citizen_note" | "system_note"; - noteText: string; - isInternal?: boolean; -}; diff --git a/civic-platform/backend/src/modules/departments/.gitkeep b/civic-platform/backend/src/modules/departments/.gitkeep deleted file mode 100644 index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/departments/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/civic-platform/backend/src/modules/departments/department.repository.ts b/civic-platform/backend/src/modules/departments/department.repository.ts deleted file mode 100644 index d978b6daad648b7f1c301a0028afb612a9ca654e..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/departments/department.repository.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { db } from "../../db/pool.js"; - -export async function listDepartments() { - const result = await db.query( - ` - SELECT - id, - name, - code, - description, - is_emergency AS "isEmergency", - is_active AS "isActive" - FROM departments - WHERE is_active = TRUE - ORDER BY is_emergency DESC, name ASC - `, - ); - - return result.rows; -} diff --git a/civic-platform/backend/src/modules/departments/department.routes.ts b/civic-platform/backend/src/modules/departments/department.routes.ts deleted file mode 100644 index e8e5fa1a4a4ffdbc09cb663eb846efc49b7cd4ca..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/departments/department.routes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Router } from "express"; -import { listDepartmentsService } from "./department.service.js"; - -export const departmentRouter = Router(); - -departmentRouter.get("/", async (_req, res) => { - try { - const departments = await listDepartmentsService(); - res.json({ data: departments }); - } catch (error) { - res.status(500).json({ - error: error instanceof Error ? error.message : "Failed to fetch departments", - }); - } -}); diff --git a/civic-platform/backend/src/modules/departments/department.service.ts b/civic-platform/backend/src/modules/departments/department.service.ts deleted file mode 100644 index 487c3090f07055c02a81538065cb5f61cf9c000d..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/departments/department.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { listDepartments } from "./department.repository.js"; - -export async function listDepartmentsService() { - return listDepartments(); -} diff --git a/civic-platform/backend/src/modules/domains/domain.repository.ts b/civic-platform/backend/src/modules/domains/domain.repository.ts deleted file mode 100644 index 1efea4ae8acfbb0a70ae05e3e79da68130a0a357..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/domains/domain.repository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { db } from "../../db/pool.js"; - -export async function listDomains() { - const result = await db.query( - ` - SELECT - id, - name, - description, - is_emergency AS "isEmergency", - is_active AS "isActive" - FROM domains - WHERE is_active = TRUE - ORDER BY is_emergency ASC, name ASC - `, - ); - - return result.rows; -} diff --git a/civic-platform/backend/src/modules/domains/domain.routes.ts b/civic-platform/backend/src/modules/domains/domain.routes.ts deleted file mode 100644 index 322ad21fd7062b14e06da7a860a07e2bcff3f8af..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/domains/domain.routes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Router } from "express"; -import { listDomainsService } from "./domain.service.js"; - -export const domainRouter = Router(); - -domainRouter.get("/", async (_req, res) => { - try { - const domains = await listDomainsService(); - res.json({ data: domains }); - } catch (error) { - res.status(500).json({ - error: error instanceof Error ? error.message : "Failed to fetch domains", - }); - } -}); diff --git a/civic-platform/backend/src/modules/domains/domain.service.ts b/civic-platform/backend/src/modules/domains/domain.service.ts deleted file mode 100644 index b6cc12ddac141929d3f0bba43a326439b1158c86..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/domains/domain.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { listDomains } from "./domain.repository.js"; - -export async function listDomainsService() { - return listDomains(); -} diff --git a/civic-platform/backend/src/modules/notifications/.gitkeep b/civic-platform/backend/src/modules/notifications/.gitkeep deleted file mode 100644 index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/notifications/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/civic-platform/backend/src/modules/routing/.gitkeep b/civic-platform/backend/src/modules/routing/.gitkeep deleted file mode 100644 index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/routing/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/civic-platform/backend/src/modules/users/.gitkeep b/civic-platform/backend/src/modules/users/.gitkeep deleted file mode 100644 index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/users/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/civic-platform/backend/src/modules/users/user.repository.ts b/civic-platform/backend/src/modules/users/user.repository.ts deleted file mode 100644 index 69747abc2262e8821b1fd122982a2b94ad5cf292..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/users/user.repository.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { db } from "../../db/pool.js"; -import type { UserContext } from "./user.types.js"; - -export async function getUserContext(userId: string): Promise { - const result = await db.query( - ` - SELECT - u.id, - u.full_name AS "fullName", - u.email, - r.name AS role, - u.department_id AS "departmentId", - d.name AS "departmentName", - u.ward_id AS "wardId" - FROM users u - INNER JOIN roles r ON r.id = u.role_id - LEFT JOIN departments d ON d.id = u.department_id - WHERE u.id = $1 - AND u.is_active = TRUE - LIMIT 1 - `, - [userId], - ); - - return result.rows[0] ?? null; -} - -export async function listFieldOfficers(departmentId?: string): Promise { - const result = await db.query( - ` - SELECT - u.id, - u.full_name AS "fullName", - u.email, - r.name AS role, - u.department_id AS "departmentId", - d.name AS "departmentName", - u.ward_id AS "wardId" - FROM users u - INNER JOIN roles r ON r.id = u.role_id - LEFT JOIN departments d ON d.id = u.department_id - WHERE r.name = 'field_officer' - AND u.is_active = TRUE - AND ($1::uuid IS NULL OR u.department_id = $1::uuid) - ORDER BY u.full_name ASC - `, - [departmentId ?? null], - ); - - return result.rows; -} - diff --git a/civic-platform/backend/src/modules/users/user.routes.ts b/civic-platform/backend/src/modules/users/user.routes.ts deleted file mode 100644 index c1faea0a41d64918bfacf76f8428b04031535bdc..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/users/user.routes.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Router } from "express"; -import { getUserContextService, listFieldOfficersService } from "./user.service.js"; - -export const userRouter = Router(); - -userRouter.get("/field-officers", async (req, res) => { - try { - const officers = await listFieldOfficersService( - typeof req.query.departmentId === "string" ? req.query.departmentId : undefined, - ); - - res.json({ data: officers }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch field officers", - }); - } -}); - -userRouter.get("/:id", async (req, res) => { - try { - const user = await getUserContextService(req.params.id); - - if (!user) { - res.status(404).json({ error: "User not found" }); - return; - } - - res.json({ data: user }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Failed to fetch user", - }); - } -}); diff --git a/civic-platform/backend/src/modules/users/user.service.ts b/civic-platform/backend/src/modules/users/user.service.ts deleted file mode 100644 index 76dfb4f1c50d689eb3bc3dd1f9db251253b72f30..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/users/user.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getUserContext, listFieldOfficers } from "./user.repository.js"; - -export async function getUserContextService(userId: string) { - if (!userId) { - throw new Error("userId is required"); - } - - return getUserContext(userId); -} - -export async function listFieldOfficersService(departmentId?: string) { - return listFieldOfficers(departmentId); -} - diff --git a/civic-platform/backend/src/modules/users/user.types.ts b/civic-platform/backend/src/modules/users/user.types.ts deleted file mode 100644 index 78919fb1013a7c18235a9229f9cf29027450128e..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/modules/users/user.types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type UserContext = { - id: string; - fullName: string; - email: string | null; - role: "citizen" | "department_operator" | "municipal_admin" | "field_officer"; - departmentId: string | null; - departmentName: string | null; - wardId: string | null; -}; - diff --git a/civic-platform/backend/src/routes/health.ts b/civic-platform/backend/src/routes/health.ts deleted file mode 100644 index f22b63dcefca007432c5fb26f6f81f9633854122..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/routes/health.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Router } from "express"; -import { db } from "../db/pool.js"; - -export const healthRouter = Router(); - -const startedAt = Date.now(); - -healthRouter.get("/", async (_req, res) => { - try { - await db.query("SELECT 1"); - - res.json({ - status: "ok", - service: "civicpulse-backend", - timestamp: new Date().toISOString(), - uptimeSeconds: Math.round((Date.now() - startedAt) / 1000), - database: "connected", - }); - } catch { - res.status(503).json({ - status: "degraded", - service: "civicpulse-backend", - timestamp: new Date().toISOString(), - uptimeSeconds: Math.round((Date.now() - startedAt) / 1000), - database: "unavailable", - }); - } -}); diff --git a/civic-platform/backend/src/scripts/init-db.ts b/civic-platform/backend/src/scripts/init-db.ts deleted file mode 100644 index 8323b01446ece1c5a00cffdb5cdd5fb96a9a1901..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/scripts/init-db.ts +++ /dev/null @@ -1,88 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { db } from "../db/pool.js"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const sqlDir = path.resolve(__dirname, "../../sql"); - -async function markLegacyMigrations() { - const rolesCheck = await db.query<{ exists: string | null }>( - `SELECT to_regclass('public.roles')::text AS exists`, - ); - - if (rolesCheck.rows[0]?.exists) { - await db.query( - ` - INSERT INTO schema_migrations (file_name) - VALUES ('001_initial_schema.sql') - ON CONFLICT (file_name) DO NOTHING - `, - ); - } - - const seedCheck = await db.query<{ hasSeed: boolean }>( - ` - SELECT EXISTS (SELECT 1 FROM domains LIMIT 1) - AND EXISTS (SELECT 1 FROM departments LIMIT 1) - AND EXISTS (SELECT 1 FROM users LIMIT 1) AS "hasSeed" - `, - ); - - if (seedCheck.rows[0]?.hasSeed) { - await db.query( - ` - INSERT INTO schema_migrations (file_name) - VALUES ('002_seed_core_data.sql') - ON CONFLICT (file_name) DO NOTHING - `, - ); - } -} - -async function run() { - const files = ["001_initial_schema.sql", "002_seed_core_data.sql", "003_performance_indexes.sql"]; - - await db.query(` - CREATE TABLE IF NOT EXISTS schema_migrations ( - id BIGSERIAL PRIMARY KEY, - file_name TEXT NOT NULL UNIQUE, - applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ) - `); - - await markLegacyMigrations(); - - for (const file of files) { - const existing = await db.query<{ file_name: string }>( - ` - SELECT file_name - FROM schema_migrations - WHERE file_name = $1 - LIMIT 1 - `, - [file], - ); - - if (existing.rows[0]) { - console.log(`Skipped ${file}`); - continue; - } - - const fullPath = path.join(sqlDir, file); - const sql = await fs.readFile(fullPath, "utf8"); - await db.query(sql); - await db.query(`INSERT INTO schema_migrations (file_name) VALUES ($1)`, [file]); - console.log(`Applied ${file}`); - } - - await db.end(); -} - -run().catch(async (error) => { - console.error("Database initialization failed:"); - console.error(error instanceof Error ? error.message : error); - await db.end().catch(() => undefined); - process.exit(1); -}); diff --git a/civic-platform/backend/src/scripts/seed-db.ts b/civic-platform/backend/src/scripts/seed-db.ts deleted file mode 100644 index b1b76632b551547d337c8b275a6104bf9372f3ca..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/scripts/seed-db.ts +++ /dev/null @@ -1,23 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { db } from "../db/pool.js"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const sqlDir = path.resolve(__dirname, "../../sql"); - -async function run() { - const fullPath = path.join(sqlDir, "002_seed_core_data.sql"); - const sql = await fs.readFile(fullPath, "utf8"); - await db.query(sql); - console.log("Applied 002_seed_core_data.sql"); - await db.end(); -} - -run().catch(async (error) => { - console.error("Database seed failed:"); - console.error(error instanceof Error ? error.message : error); - await db.end().catch(() => undefined); - process.exit(1); -}); diff --git a/civic-platform/backend/src/scripts/smoke-test.ts b/civic-platform/backend/src/scripts/smoke-test.ts deleted file mode 100644 index c494b975d3b8e6818ea6beac0502d93e76509b83..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/scripts/smoke-test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import process from "node:process"; - -const baseUrl = process.env.API_BASE_URL ?? "http://localhost:4000/api"; - -type SmokeResult = { - name: string; - ok: boolean; - status?: number; - details?: string; -}; - -async function checkJson(path: string, name: string) { - const response = await fetch(`${baseUrl}${path}`); - - if (!response.ok) { - return { - name, - ok: false, - status: response.status, - details: await response.text(), - } satisfies SmokeResult; - } - - await response.json(); - - return { - name, - ok: true, - status: response.status, - } satisfies SmokeResult; -} - -async function checkText(path: string, name: string) { - const response = await fetch(`${baseUrl}${path}`); - - return { - name, - ok: response.ok, - status: response.status, - details: response.ok ? "ok" : await response.text(), - } satisfies SmokeResult; -} - -async function main() { - const checks = await Promise.all([ - checkJson("/health", "Health"), - checkJson("/departments", "Departments"), - checkJson("/domains", "Domains"), - checkJson("/complaints/summary/dashboard", "Dashboard summary"), - checkJson("/complaints/summary/analytics", "Analytics summary"), - checkText("/complaints/summary/export.csv", "Analytics export"), - ]); - - const failed = checks.filter((check) => !check.ok); - - for (const check of checks) { - console.log( - `${check.ok ? "PASS" : "FAIL"} ${check.name}${check.status ? ` (${check.status})` : ""}${ - check.details && !check.ok ? ` - ${check.details}` : "" - }`, - ); - } - - if (failed.length > 0) { - process.exitCode = 1; - } -} - -main().catch((error) => { - console.error("Smoke test failed:", error); - process.exitCode = 1; -}); diff --git a/civic-platform/backend/src/server.ts b/civic-platform/backend/src/server.ts deleted file mode 100644 index d71f5c14ebfde0ad1290ece580ed7c3adedbfc99..0000000000000000000000000000000000000000 --- a/civic-platform/backend/src/server.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createApp } from "./app.js"; -import { env } from "./config/env.js"; - -const app = createApp(); - -app.listen(env.port, () => { - console.log(`CivicPulse backend listening on port ${env.port}`); -}); diff --git a/civic-platform/backend/tsconfig.json b/civic-platform/backend/tsconfig.json deleted file mode 100644 index c4462c78c1c622cbb1e0a30d40b2ffde1a03ce83..0000000000000000000000000000000000000000 --- a/civic-platform/backend/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} diff --git a/civic-platform/build.log b/civic-platform/build.log deleted file mode 100644 index a4d5c59e48468ac33cb8deb84b833a8a4fbdd922..0000000000000000000000000000000000000000 Binary files a/civic-platform/build.log and /dev/null differ diff --git a/civic-platform/build_utf8.log b/civic-platform/build_utf8.log deleted file mode 100644 index 9964e2277b08fc7f3c29254f6c0f70f6a180c74d..0000000000000000000000000000000000000000 --- a/civic-platform/build_utf8.log +++ /dev/null @@ -1,46 +0,0 @@ - -> civic-platform@0.1.0 build -> next build - - Γû▓ Next.js 15.5.14 - - Creating an optimized production build ... -node.exe : Failed to -compile. -At C:\Program -Files\nodejs\npm.ps1:29 -char:3 -+ & $NODE_EXE -$NPM_CLI_JS $args -+ ~~~~~~~~~~~~~~~~~~~~~~ -~~~~~~~ - + CategoryInfo - : NotSpecified: (F - ailed to compile.:Str - ing) [], RemoteExcept -ion - + FullyQualifiedError - Id : NativeCommandErr - or - - -./components/issue-map-boa -rd.tsx -Module not found: Package -path . is not exported -from package C:\Users\Prav -een\Documents\New project\ -civic-platform\node_module -s\react-map-gl (see -exports field in C:\Users\ -Praveen\Documents\New proj -ect\civic-platform\node_mo -dules\react-map-gl\package -.json) - -https://nextjs.org/docs/me -ssages/module-not-found - - -> Build failed because of -webpack errors diff --git a/civic-platform/components/admin-assign-action.tsx b/civic-platform/components/admin-assign-action.tsx deleted file mode 100644 index f413e826e4f351c2f7e5351d19078201700dd8a7..0000000000000000000000000000000000000000 --- a/civic-platform/components/admin-assign-action.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client"; - -import { FormEvent, useEffect, useMemo, useState, useTransition } from "react"; -import { useRouter } from "next/navigation"; -import { LoaderCircle } from "lucide-react"; -import { assignComplaint, getDepartments, getFieldOfficers, type DepartmentOption, type UserOption } from "@/lib/api"; - -export function AdminAssignAction({ - complaintId, - suggestedDepartment, - operatorId, -}: { - complaintId: string; - suggestedDepartment?: string; - operatorId: string; -}) { - const router = useRouter(); - const [departments, setDepartments] = useState([]); - const [officers, setOfficers] = useState([]); - const [selectedDepartment, setSelectedDepartment] = useState(""); - const [selectedOfficer, setSelectedOfficer] = useState(""); - const [notes, setNotes] = useState(""); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); - const [isPending, startTransition] = useTransition(); - - const resolvedSuggestion = useMemo(() => suggestedDepartment ?? "Department suggestion pending", [suggestedDepartment]); - - useEffect(() => { - async function loadDepartments() { - try { - const result = await getDepartments(); - setDepartments(result); - - if (result.length > 0) { - const suggestedMatch = result.find((department) => department.name === suggestedDepartment); - setSelectedDepartment(suggestedMatch?.id ?? result[0].id); - } - } catch (loadError) { - setError(loadError instanceof Error ? loadError.message : "Failed to load departments"); - } - } - - void loadDepartments(); - }, [suggestedDepartment]); - - useEffect(() => { - async function loadOfficers() { - if (!selectedDepartment) { - setOfficers([]); - setSelectedOfficer(""); - return; - } - - try { - const result = await getFieldOfficers(selectedDepartment); - setOfficers(result); - setSelectedOfficer((current) => (result.some((officer) => officer.id === current) ? current : "")); - } catch (loadError) { - setOfficers([]); - setSelectedOfficer(""); - setError(loadError instanceof Error ? loadError.message : "Failed to load field officers"); - } - } - - void loadOfficers(); - }, [selectedDepartment]); - - function handleSubmit(event: FormEvent) { - event.preventDefault(); - setError(null); - setSuccess(null); - - startTransition(async () => { - try { - await assignComplaint({ - complaintId, - departmentId: selectedDepartment, - assignedToUserId: selectedOfficer || undefined, - assignedByUserId: operatorId, - notes: notes.trim() || undefined, - }); - - setSuccess(selectedOfficer ? "Complaint assigned to the department and field officer." : "Complaint assigned successfully."); - router.refresh(); - } catch (assignError) { - setError(assignError instanceof Error ? assignError.message : "Failed to assign complaint"); - } - }); - } - - return ( -
-
- Suggested department: {resolvedSuggestion} -
- - - - - -