From 30fef7ddccc2d1f9628336f715a28ad1c0500c13 Mon Sep 17 00:00:00 2001 From: Arman Date: Thu, 5 Feb 2026 14:33:12 +0530 Subject: [PATCH 1/3] resolved the issue about Emergency and High-Severity Grievance Fast-Track System. --- backend/models.py | 1 + backend/routers/grievances.py | 14 +- backend/routers/issues.py | 7 +- backend/schemas.py | 5 +- backend/sla_config_service.py | 11 + backend/tasks.py | 11 +- frontend/src/views/Home.jsx | 411 ++++++++-------- frontend/src/views/ReportForm.jsx | 766 +++++++++++++++--------------- 8 files changed, 637 insertions(+), 589 deletions(-) diff --git a/backend/models.py b/backend/models.py index 14b34ba9..af2bc63a 100644 --- a/backend/models.py +++ b/backend/models.py @@ -135,6 +135,7 @@ class Issue(Base): longitude = Column(Float, nullable=True, index=True) location = Column(String, nullable=True) action_plan = Column(JSONEncodedDict, nullable=True) + severity = Column(Enum(SeverityLevel), default=SeverityLevel.MEDIUM, index=True) class PushSubscription(Base): __tablename__ = "push_subscriptions" diff --git a/backend/routers/grievances.py b/backend/routers/grievances.py index 360ec17e..e6fa8134 100644 --- a/backend/routers/grievances.py +++ b/backend/routers/grievances.py @@ -1,13 +1,13 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy.orm import Session, joinedload -from sqlalchemy import func +from sqlalchemy import func, case from typing import List, Optional import os import json import logging from backend.database import get_db -from backend.models import Grievance, EscalationAudit +from backend.models import Grievance, EscalationAudit, SeverityLevel from backend.schemas import ( GrievanceSummaryResponse, EscalationAuditResponse, EscalationStatsResponse, ResponsibilityMapResponse @@ -38,6 +38,16 @@ def get_grievances( if category: query = query.filter(Grievance.category == category) + # Priority Queue Logic: Sort by Severity (Critical > High > Medium > Low) then by Date (Oldest first for resolution) + severity_order = case( + (Grievance.severity == SeverityLevel.CRITICAL, 1), + (Grievance.severity == SeverityLevel.HIGH, 2), + (Grievance.severity == SeverityLevel.MEDIUM, 3), + (Grievance.severity == SeverityLevel.LOW, 4), + else_=5 + ) + query = query.order_by(severity_order.asc(), Grievance.created_at.asc()) + grievances = query.offset(offset).limit(limit).all() # Convert to response format diff --git a/backend/routers/issues.py b/backend/routers/issues.py index 8a06f3b0..7390eb45 100644 --- a/backend/routers/issues.py +++ b/backend/routers/issues.py @@ -41,6 +41,7 @@ async def create_issue( background_tasks: BackgroundTasks, description: str = Form(..., min_length=10, max_length=1000), category: str = Form(..., pattern=f"^({'|'.join([cat.value for cat in IssueCategory])})$"), + severity: str = Form('medium', pattern="^(low|medium|high|critical)$"), language: str = Form('en'), user_email: str = Form(None), latitude: float = Form(None, ge=-90, le=90), @@ -158,7 +159,8 @@ async def create_issue( latitude=latitude, longitude=longitude, location=location, - action_plan=None + action_plan=None, + severity=severity ) # Offload blocking DB operations to threadpool @@ -505,7 +507,8 @@ def get_recent_issues( upvotes=i.upvotes if i.upvotes is not None else 0, location=i.location, latitude=i.latitude, - longitude=i.longitude + longitude=i.longitude, + severity=i.severity.value if hasattr(i.severity, 'value') else i.severity # action_plan is deferred and excluded ).model_dump(mode='json')) diff --git a/backend/schemas.py b/backend/schemas.py index 9d5c915d..e9e23e17 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -41,12 +41,11 @@ class IssueResponse(BaseModel): location: Optional[str] = None latitude: Optional[float] = None longitude: Optional[float] = None + severity: Optional[str] = "medium" action_plan: Optional[Any] = None model_config = ConfigDict(from_attributes=True) -class IssueResponse(IssueSummaryResponse): - action_plan: Optional[Dict[str, Any]] = Field(None, description="Generated action plan") class IssueCreateRequest(BaseModel): description: str = Field(..., min_length=10, max_length=1000, description="Issue description") @@ -55,6 +54,7 @@ class IssueCreateRequest(BaseModel): latitude: Optional[float] = Field(None, ge=-90, le=90, description="Latitude coordinate") longitude: Optional[float] = Field(None, ge=-180, le=180, description="Longitude coordinate") location: Optional[str] = Field(None, max_length=200, description="Location description") + severity: Optional[str] = Field("medium", pattern="^(low|medium|high|critical)$", description="Severity level") @field_validator('description') @classmethod @@ -157,6 +157,7 @@ class NearbyIssueResponse(BaseModel): upvotes: int = Field(..., description="Number of upvotes") created_at: datetime = Field(..., description="Issue creation timestamp") status: str = Field(..., description="Issue status") + severity: str = Field(default="medium", description="Issue severity") class DeduplicationCheckResponse(BaseModel): diff --git a/backend/sla_config_service.py b/backend/sla_config_service.py index ff3afcbb..21f43e2a 100644 --- a/backend/sla_config_service.py +++ b/backend/sla_config_service.py @@ -83,6 +83,17 @@ def get_sla_hours(self, severity: SeverityLevel, jurisdiction_level: Jurisdictio return sla_config.sla_hours # Return default + # Return default based on severity if available + severity_defaults = { + SeverityLevel.CRITICAL: 6, + SeverityLevel.HIGH: 24, + SeverityLevel.MEDIUM: 48, + SeverityLevel.LOW: 72 + } + + if severity in severity_defaults: + return severity_defaults[severity] + return self.default_sla_hours finally: diff --git a/backend/tasks.py b/backend/tasks.py index af8b3286..8de89745 100644 --- a/backend/tasks.py +++ b/backend/tasks.py @@ -59,7 +59,16 @@ async def create_grievance_from_issue_background(issue_id: int): 'vandalism': 'medium' } - severity = severity_mapping.get(issue.category.lower(), 'medium') + # Prefer issue's own severity if set, otherwise fallback to mapping + if hasattr(issue, 'severity') and issue.severity: + severity = issue.severity.value if hasattr(issue.severity, 'value') else issue.severity + else: + severity = severity_mapping.get(issue.category.lower(), 'medium') + + # Check for immediate escalation triggers (e.g. Critical severity) + if severity == 'critical': + logger.warning(f"CRITICAL ISSUE REPORTED: {issue.id} - {issue.description[:50]}...") + # Here we could trigger immediate SMS/Call alerts or specialized notifications # Create grievance data grievance_data = { diff --git a/frontend/src/views/Home.jsx b/frontend/src/views/Home.jsx index d99e7b59..22c3725c 100644 --- a/frontend/src/views/Home.jsx +++ b/frontend/src/views/Home.jsx @@ -147,163 +147,162 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote, loa {totalImpact} {t('home.issuesSolved')} - - - - - {/* Quick Actions Grid */} -
- - - - - - - - - - - - - - - {/* New Western Style Features */} - - - - - - - - - - - - - - - -
+ + + + {/* Quick Actions Grid */} +
+ + + + + + + + + + + + + + + {/* New Western Style Features */} + + + + + + + + + + + + + + + +
{/* Smart Scanner CTA */} - + {/* Categorized Features */}
- {categories.map((cat, idx) => ( -
-
- {cat.icon} -

{cat.title}

-
-
- {cat.items.map((item) => ( - - ))} + { + categories.map((cat, idx) => ( +
+
+ {cat.icon} +

{cat.title}

+
+
+ {cat.items.map((item) => ( + + ))} +
-
- ))} + )) + }
{/* Additional Tools */} @@ -390,7 +391,8 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote, loa
{recentIssues.length > 0 ? ( recentIssues.map((issue) => ( -
+
{issue.category} + {(issue.severity === 'high' || issue.severity === 'critical') && ( + + Urgent + + )} {new Date(issue.created_at).toLocaleDateString()} @@ -455,30 +462,32 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote, loa
)}
-
+
{/* Scroll to Top Button - Appears on scroll */} {/* Scroll to Top Button - Portal to Body */} - {createPortal( - - {showScrollTop && ( - - - - )} - , - document.body - )} + { + createPortal( + + {showScrollTop && ( + + + + )} + , + document.body + ) + } ); }; diff --git a/frontend/src/views/ReportForm.jsx b/frontend/src/views/ReportForm.jsx index cf5eb9c7..608c061e 100644 --- a/frontend/src/views/ReportForm.jsx +++ b/frontend/src/views/ReportForm.jsx @@ -19,7 +19,8 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = image: null, latitude: null, longitude: null, - location: '' + location: '', + severity: 'medium' }); const [gettingLocation, setGettingLocation] = useState(false); const [severity, setSeverity] = useState(null); @@ -53,50 +54,50 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = }, []); const analyzeUrgency = async () => { - if (!formData.description || formData.description.length < 5) return; - setAnalyzingUrgency(true); - try { - const response = await fetch(`${API_URL}/api/analyze-urgency`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ description: formData.description }), - }); - if (response.ok) { - const data = await response.json(); - setUrgencyAnalysis(data); - } - } catch (e) { - console.error("Urgency analysis failed", e); - } finally { - setAnalyzingUrgency(false); + if (!formData.description || formData.description.length < 5) return; + setAnalyzingUrgency(true); + try { + const response = await fetch(`${API_URL}/api/analyze-urgency`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ description: formData.description }), + }); + if (response.ok) { + const data = await response.json(); + setUrgencyAnalysis(data); } + } catch (e) { + console.error("Urgency analysis failed", e); + } finally { + setAnalyzingUrgency(false); + } }; const autoDescribe = async () => { - if (!formData.image) return; - setDescribing(true); + if (!formData.image) return; + setDescribing(true); - const uploadData = new FormData(); - uploadData.append('image', formData.image); + const uploadData = new FormData(); + uploadData.append('image', formData.image); - try { - const response = await fetch(`${API_URL}/api/generate-description`, { - method: 'POST', - body: uploadData - }); - if (response.ok) { - const data = await response.json(); - if (data.description) { - setFormData(prev => ({...prev, description: data.description})); - } - } - } catch (e) { - console.error("Auto description failed", e); - } finally { - setDescribing(false); + try { + const response = await fetch(`${API_URL}/api/generate-description`, { + method: 'POST', + body: uploadData + }); + if (response.ok) { + const data = await response.json(); + if (data.description) { + setFormData(prev => ({ ...prev, description: data.description })); + } } + } catch (e) { + console.error("Auto description failed", e); + } finally { + setDescribing(false); + } }; const analyzeImage = async (file) => { @@ -109,87 +110,90 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = uploadData.append('image', file); try { - const response = await fetch(`${API_URL}/api/detect-severity`, { - method: 'POST', - body: uploadData - }); - if (response.ok) { - const data = await response.json(); - setSeverity(data); - } else { - const errorData = await response.json().catch(() => ({ detail: 'Unknown error' })); - setAnalysisErrors(prev => ({ ...prev, severity: errorData.detail || 'Analysis failed' })); + const response = await fetch(`${API_URL}/api/detect-severity`, { + method: 'POST', + body: uploadData + }); + if (response.ok) { + const data = await response.json(); + setSeverity(data); + if (data.level) { + setFormData(prev => ({ ...prev, severity: data.level.toLowerCase() })); } + } else { + const errorData = await response.json().catch(() => ({ detail: 'Unknown error' })); + setAnalysisErrors(prev => ({ ...prev, severity: errorData.detail || 'Analysis failed' })); + } } catch (e) { - console.error("Severity analysis failed", e); - setAnalysisErrors(prev => ({ ...prev, severity: 'Network error - please try again' })); + console.error("Severity analysis failed", e); + setAnalysisErrors(prev => ({ ...prev, severity: 'Network error - please try again' })); } finally { - setAnalyzing(false); + setAnalyzing(false); } }; const analyzeDepth = async () => { - if (!formData.image) return; - setAnalyzingDepth(true); - setDepthMap(null); + if (!formData.image) return; + setAnalyzingDepth(true); + setDepthMap(null); - const uploadData = new FormData(); - uploadData.append('image', formData.image); + const uploadData = new FormData(); + uploadData.append('image', formData.image); - try { - const data = await detectorsApi.depth(uploadData); - if (data && data.depth_map) { - setDepthMap(data.depth_map); - } - } catch (e) { - console.error("Depth analysis failed", e); - } finally { - setAnalyzingDepth(false); + try { + const data = await detectorsApi.depth(uploadData); + if (data && data.depth_map) { + setDepthMap(data.depth_map); } + } catch (e) { + console.error("Depth analysis failed", e); + } finally { + setAnalyzingDepth(false); + } }; const mapSmartScanToCategory = (label) => { - const map = { - 'pothole': 'road', - 'garbage': 'garbage', - 'flooded street': 'water', - 'fire accident': 'road', - 'fallen tree': 'road', - 'stray animal': 'road', - 'blocked road': 'road', - 'broken streetlight': 'streetlight', - 'illegal parking': 'road', - 'graffiti vandalism': 'college_infra', - 'normal street': 'road' - }; - return map[label] || 'road'; + const map = { + 'pothole': 'road', + 'garbage': 'garbage', + 'flooded street': 'water', + 'fire accident': 'road', + 'fallen tree': 'road', + 'stray animal': 'road', + 'blocked road': 'road', + 'broken streetlight': 'streetlight', + 'illegal parking': 'road', + 'graffiti vandalism': 'college_infra', + 'normal street': 'road' + }; + return map[label] || 'road'; }; const analyzeSmartScan = async (file) => { - if (!file) return; - setAnalyzingSmartScan(true); - setSmartCategory(null); - setAnalysisErrors(prev => ({ ...prev, smartScan: null })); + if (!file) return; + setAnalyzingSmartScan(true); + setSmartCategory(null); + setAnalysisErrors(prev => ({ ...prev, smartScan: null })); - const uploadData = new FormData(); - uploadData.append('image', file); + const uploadData = new FormData(); + uploadData.append('image', file); - try { - const data = await detectorsApi.smartScan(uploadData); - if (data && data.category && data.category !== 'unknown') { - const mappedCategory = mapSmartScanToCategory(data.category); - setSmartCategory({ - original: data.category, - mapped: mappedCategory, - confidence: data.confidence - }); - } - } catch (e) { - console.error("Smart scan failed", e); - setAnalysisErrors(prev => ({ ...prev, smartScan: 'Smart scan failed - continuing with manual selection' })); - } finally { - setAnalyzingSmartScan(false); + try { + const data = await detectorsApi.smartScan(uploadData); + if (data && data.category && data.category !== 'unknown') { + const mappedCategory = mapSmartScanToCategory(data.category); + setSmartCategory({ + original: data.category, + mapped: mappedCategory, + confidence: data.confidence + }); } + } catch (e) { + console.error("Smart scan failed", e); + setAnalysisErrors(prev => ({ ...prev, smartScan: 'Smart scan failed - continuing with manual selection' })); + } finally { + setAnalyzingSmartScan(false); + } }; const compressImage = (file, maxWidth = 1024, maxHeight = 1024, quality = 0.8) => { @@ -242,7 +246,7 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = }); } - setFormData({...formData, image: processedFile}); + setFormData({ ...formData, image: processedFile }); // Analyze in parallel but with error handling await Promise.allSettled([ @@ -252,7 +256,7 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = } catch (error) { console.error('Image processing failed:', error); // Fallback to original file - setFormData({...formData, image: file}); + setFormData({ ...formData, image: file }); await Promise.allSettled([ analyzeImage(file), analyzeSmartScan(file) @@ -290,21 +294,21 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = const checkNearbyIssues = async () => { if (!formData.latitude || !formData.longitude) { - getLocation(); // Try to get location first - return; + getLocation(); // Try to get location first + return; } setCheckingNearby(true); try { - const response = await fetch(`${API_URL}/api/issues/nearby?latitude=${formData.latitude}&longitude=${formData.longitude}&radius=50`); - if (response.ok) { - const data = await response.json(); - setNearbyIssues(data); - setShowNearbyModal(true); - } + const response = await fetch(`${API_URL}/api/issues/nearby?latitude=${formData.latitude}&longitude=${formData.longitude}&radius=50`); + if (response.ok) { + const data = await response.json(); + setNearbyIssues(data); + setShowNearbyModal(true); + } } catch (e) { - console.error("Failed to check nearby issues", e); + console.error("Failed to check nearby issues", e); } finally { - setCheckingNearby(false); + setCheckingNearby(false); } }; @@ -353,10 +357,14 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = if (formData.image) { payload.append('image', formData.image); } - // Append severity info if available + // Append severity info + payload.append('severity', formData.severity); + if (severity) { - payload.append('severity_level', severity.level); - payload.append('severity_score', severity.confidence); + // We still send detailed AI analysis if key matches what backend expects, or for logging + // But the main field is 'severity' + payload.append('ai_severity_level', severity.level); + payload.append('severity_score', severity.confidence); } try { @@ -397,280 +405,276 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = return (
-

Report an Issue

-
-
- - setFormData({ ...formData, category: e.target.value })} + > + + + + + + + + {analyzingSmartScan && ( +
+ + AI is analyzing image for category... +
+ )} + {analysisErrors.smartScan && ( +
+ + {analysisErrors.smartScan} +
+ )} + {smartCategory && ( +
setFormData({ ...formData, category: smartCategory.mapped })} + className="mt-2 bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-100 p-2 rounded-lg cursor-pointer hover:bg-purple-100 transition flex items-center justify-between group" > - - - - - - - - {analyzingSmartScan && ( -
- - AI is analyzing image for category... -
- )} - {analysisErrors.smartScan && ( -
- - {analysisErrors.smartScan} -
- )} - {smartCategory && ( -
setFormData({...formData, category: smartCategory.mapped})} - className="mt-2 bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-100 p-2 rounded-lg cursor-pointer hover:bg-purple-100 transition flex items-center justify-between group" - > -
- -
-

AI Suggestion

-

{smartCategory.original}

-
-
-
- Apply -
+
+ +
+

AI Suggestion

+

{smartCategory.original}

- )} +
+
+ Apply +
+
+ )} +
+ +
+ + +
+ +
+ +
+