From 049294b1c704485aa7e2d2c676900fae38a2a2d1 Mon Sep 17 00:00:00 2001 From: Dhara Pandya Date: Tue, 24 Feb 2026 18:36:40 +0530 Subject: [PATCH] feat: implement live grievance map --- backend/grievance_service.py | 14 ++++ backend/routers/grievances.py | 39 ++++++++++- backend/utils.py | 5 ++ frontend/src/App.jsx | 14 +++- frontend/src/api/grievances.js | 45 ++++++++---- frontend/src/main.jsx | 1 + frontend/src/views/GrievanceMap.jsx | 102 ++++++++++++++++++++++++++++ frontend/src/views/Home.jsx | 12 +++- frontend/src/views/ReportForm.jsx | 15 ++-- 9 files changed, 224 insertions(+), 23 deletions(-) create mode 100644 frontend/src/views/GrievanceMap.jsx diff --git a/backend/grievance_service.py b/backend/grievance_service.py index 849f9837..db99e326 100644 --- a/backend/grievance_service.py +++ b/backend/grievance_service.py @@ -90,6 +90,20 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) - longitude = location_data.get('longitude') if isinstance(location_data, dict) else None address = location_data.get('address') if isinstance(location_data, dict) else None + # Validate geo coordinates + if latitude is not None and longitude is not None: + try: + latitude = float(latitude) + longitude = float(longitude) + + if not (-90 <- latitude <=90): + raise ValueError("Invalid latitude range") + if not(-180 <= longitude <=180): + raise ValueError("Invalid longitude range") + except(ValueError, TypeError): + print("Invalid latitude/longitude provided") + latitude = None + longitude = None # Create grievance grievance = Grievance( unique_id=unique_id, diff --git a/backend/routers/grievances.py b/backend/routers/grievances.py index 6de629f2..cd48ffbe 100644 --- a/backend/routers/grievances.py +++ b/backend/routers/grievances.py @@ -85,7 +85,44 @@ def get_grievances( except Exception as e: logger.error(f"Error getting grievances: {e}", exc_info=True) raise HTTPException(status_code=500, detail="Failed to retrieve grievances") - +@router.get("/api/grievances/map") +def get_grievances_for_map( + status: Optional[str] = None, + db: Session = Depends(get_db) +): + """ + Optimized endpoint for map rendering. + Returns only geo + minimal fields. + """ + query = db.query( + Grievance.id, + Grievance.category, + Grievance.severity, + Grievance.status, + Grievance.latitude, + Grievance.longitude, + Grievance.assigned_authority + ).filter( + Grievance.latitude.isnot(None), + Grievance.longitude.isnot(None) + ) + if status: + query = query.filter(Grievance.status == status) + results = query.all() + + return [ + { + "id": g.id, + "category": g.category, + "severity": g.severity.value, + "status": g.status.value, + "latitude": g.latitude, + "longitude": g.longitude, + "authority": g.assigned_authority, + + } + for g in results + ] @router.get("/api/grievances/{grievance_id}", response_model=GrievanceSummaryResponse) def get_grievance(grievance_id: int, db: Session = Depends(get_db)): """Get detailed grievance information with escalation history""" diff --git a/backend/utils.py b/backend/utils.py index eaaf0d48..62ae410f 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -8,8 +8,12 @@ import shutil import logging import io + import secrets import string + +import uuid + from typing import Optional from backend.cache import user_upload_cache @@ -325,3 +329,4 @@ def generate_reference_id() -> str: timestamp = datetime.now().strftime('%Y%m%d-%H%M%S') random_suffix = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4)) return f"VOICE-{timestamp}-{random_suffix}" + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 686b5cfd..95b22655 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -8,6 +8,7 @@ import Footer from './components/Footer'; import FloatingButtonsManager from './components/FloatingButtonsManager'; import LoadingSpinner from './components/LoadingSpinner'; import { DarkModeProvider, useDarkMode } from './contexts/DarkModeContext'; +import GrievanceMap from "./views/GrievanceMap"; // Lazy Load Views const Landing = React.lazy(() => import('./views/Landing')); @@ -69,7 +70,9 @@ function AppContent() { // Safe navigation helper const navigateToView = useCallback((view) => { - const validViews = ['home', 'map', 'report', 'action', 'mh-rep', 'pothole', 'garbage', 'vandalism', 'flood', 'infrastructure', 'parking', 'streetlight', 'fire', 'animal', 'blocked', 'tree', 'pest', 'smart-scan', 'grievance-analysis', 'noise', 'safety-check', 'insight', 'my-reports', 'grievance', 'login', 'signup']; + + const validViews = ['home', 'map', 'report', 'action', 'mh-rep', 'pothole', 'garbage', 'vandalism', 'flood', 'infrastructure', 'parking', 'streetlight', 'fire', 'animal', 'blocked', 'tree', 'pest', 'smart-scan', 'grievance-analysis', 'noise', 'safety-check', 'my-reports', 'grievance', 'login', 'signup','grievance-map']; + if (validViews.includes(view)) { navigate(view === 'home' ? '/' : `/${view}`); } else { @@ -352,12 +355,21 @@ function AppContent() { } /> + } /> + + + + + + } /> } /> + diff --git a/frontend/src/api/grievances.js b/frontend/src/api/grievances.js index 628b9c51..87accac2 100644 --- a/frontend/src/api/grievances.js +++ b/frontend/src/api/grievances.js @@ -3,30 +3,52 @@ const API_BASE = import.meta.env.VITE_API_URL || ''; export const grievancesApi = { - // Get list of grievances with escalation history + + // Create grievance + async create(data) { + const response = await fetch(`${API_BASE}/api/grievances`,{ + method: 'POST', + headers :{ + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if(!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }, + //Get list of grievances async getAll(params = {}) { const queryParams = new URLSearchParams(); - if (params.status) queryParams.append('status', params.status); - if (params.category) queryParams.append('category', params.category); - if (params.limit) queryParams.append('limit', params.limit); - if (params.offset) queryParams.append('offset', params.offset); + if(params.status) queryParams.append('status', params.status); + if(params.category) queryParams.append('category', params.category); + if(params.limit) queryParams.append('limit', params.limit); + if(params.offset) queryParams.append('offset', params.offset); const response = await fetch(`${API_BASE}/api/grievances?${queryParams}`); - if (!response.ok) { + if(!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }, - - // Get single grievance with escalation history + //Get single grievance async getById(grievanceId) { const response = await fetch(`${API_BASE}/api/grievances/${grievanceId}`); - if (!response.ok) { + if(!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }, - + //Get map data + async getMapData() { + const response = await fetch(`${API_BASE}/api/grievances/map`); + if(!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }, + // Get escalation statistics async getStats() { const response = await fetch(`${API_BASE}/api/escalation-stats`); @@ -40,9 +62,6 @@ export const grievancesApi = { async escalate(grievanceId, reason) { const response = await fetch(`${API_BASE}/api/grievances/${grievanceId}/escalate?reason=${encodeURIComponent(reason)}`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 446f8b07..7cd0fd70 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -4,6 +4,7 @@ import './index.css' import App from './App.jsx' import './i18n' // Initialize i18n import './offlineQueue' // Initialize offline queue listeners +import "leaflet/dist/leaflet.css"; createRoot(document.getElementById('root')).render( diff --git a/frontend/src/views/GrievanceMap.jsx b/frontend/src/views/GrievanceMap.jsx new file mode 100644 index 00000000..6f424c42 --- /dev/null +++ b/frontend/src/views/GrievanceMap.jsx @@ -0,0 +1,102 @@ +import React, {useEffect, useState} from "react"; +import {MapContainer, TileLayer, Marker, Popup} from "react-leaflet"; +import MarkerClusterGroup from "react-leaflet-cluster"; +import L from "leaflet"; +import { grievancesApi} from "../api"; +import "leaflet/dist/leaflet.css"; + +const GrievanceMap = () => { + const [grievances , setGrievances] = useState([]); + + useEffect(() => { + loadMapData(); + }, []); + + const loadMapData = async () => { + try { + const response = await grievancesApi.getMapData(); + const data = Array.isArray(response)?response: response.data; + console.log("Map data:", data); + + setGrievances(data); + } catch(err) { + console.error("Error loading map data:", err); + } + }; + const getColor = (status) => { + switch (status?.toLowerCase()) { + case "open": return "red"; + case "in_progress": return "orange"; + case "resolved": return "green"; + case "escalated": return "purple"; + default: return "blue"; + } + }; + const markerIcon = (status) => + L.divIcon({ + className: "custom-marker", + html: `
`, + iconSize: [16,16], + iconAnchor: [8, 8], + }); + return ( +
+ + + + {grievances.map((g)=> + g.latitude && g.longitude ? ( + + +
+

{g.category}

+

+ + Status: + + {g.status} +

+

+ + Severity: + + {g.severity} +

+

+ + Authority: + + {g.assigned_authority} +

+ + +
+
+
+ ): null + )} +
+
+
+ ); +}; +export default GrievanceMap; \ No newline at end of file diff --git a/frontend/src/views/Home.jsx b/frontend/src/views/Home.jsx index c63b9d97..c3cff278 100644 --- a/frontend/src/views/Home.jsx +++ b/frontend/src/views/Home.jsx @@ -2,7 +2,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; -import { AnimatePresence, motion } from 'framer-motion'; + +import { motion, AnimatePresence } from 'framer-motion'; + import { AlertTriangle, MapPin, Search, Activity, Camera, Trash2, ThumbsUp, Brush, Droplets, Zap, Truck, Flame, Dog, XCircle, Lightbulb, TreeDeciduous, Bug, @@ -253,7 +255,13 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote, loa whileInView={{ opacity: 1, scale: 1 }} viewport={{ once: true }} transition={{ delay: itemIdx * 0.05 }} - onClick={() => setView(item.id)} + onClick={() => { + if(item.id === "map"){ + navigate("/grievance-map"); + } else { + setView(item.id) + } + }} className="group bg-white/50 dark:bg-gray-900/50 backdrop-blur-xl rounded-[2rem] border border-white/50 dark:border-gray-800/50 p-8 flex flex-col items-start gap-6 hover:shadow-2xl hover:bg-white dark:hover:bg-gray-800 transition-all duration-300 h-56" >
diff --git a/frontend/src/views/ReportForm.jsx b/frontend/src/views/ReportForm.jsx index 8ee29018..116fc673 100644 --- a/frontend/src/views/ReportForm.jsx +++ b/frontend/src/views/ReportForm.jsx @@ -269,11 +269,13 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { + const lat = position.coords.latitude; + const lng = position.coords.longitude; setFormData(prev => ({ ...prev, - latitude: position.coords.latitude, - longitude: position.coords.longitude, - location: `Lat: ${position.coords.latitude.toFixed(4)}, Long: ${position.coords.longitude.toFixed(4)}` + latitude: lat, + longitude: lng, + location: `Lat: ${lat.toFixed(4)}, Long: ${lng.toFixed(4)}` })); setGettingLocation(false); }, @@ -283,10 +285,11 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = setGettingLocation(false); } ); - } else { - setError("Geolocation is not supported by this browser."); - setGettingLocation(false); } + // else { + // setError("Geolocation is not supported by this browser."); + // setGettingLocation(false); + // } }; const checkNearbyIssues = async () => {