Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Web/frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,14 @@
border-right-color: var(--border);
}
}

.severity-badge {
padding: 4px 12px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
}
.severity-badge.low { background: #d1fae5; color: #065f46; }
.severity-badge.medium { background: #fef3c7; color: #92400e; }
.severity-badge.high { background: #fee2e2; color: #991b1b; }
.severity-badge.critical { background: #7f1d1d; color: white; }
19 changes: 19 additions & 0 deletions Web/frontend/src/components/ReportCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ function ReportCard({ report, actions }) {
</span>
</div>

{/* Add this block somewhere visible, e.g. after description */}
<div className="mt-3 flex flex-wrap gap-3">
{report.severity_label && (
<span
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold ${
report.severity_label === "Critical" ? "bg-red-100 text-red-800" :
report.severity_label === "High" ? "bg-orange-100 text-orange-800" :
report.severity_label === "Medium" ? "bg-yellow-100 text-yellow-800" :
"bg-green-100 text-green-800"
}`}
>
Severity: {report.severity_label} ({report.severity_score || "?"})
</span>
)}
<span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold bg-blue-100 text-blue-800">
Escalation Level: {report.escalation_level || 1}
</span>
</div>

<div className="mt-5 grid gap-3 text-sm text-slate-600 sm:grid-cols-3">
<div className="rounded-2xl bg-slate-50 p-3">
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">Reported By</p>
Expand Down
233 changes: 175 additions & 58 deletions Web/frontend/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// src/pages/Dashboard.jsx
import { useEffect, useState } from "react";
import {
Bar,
BarChart,
Expand All @@ -15,12 +17,18 @@ import MapView from "../components/MapView";
import ReportCard from "../components/ReportCard";
import StatsPanel from "../components/StatsPanel";
import StatusAlert from "../components/StatusAlert";
import {
fetchReports,
acknowledgeReport,
resolveReport,
} from "../services/api";

// Keep your mock data only for charts (you can replace these later with real aggregated data)
import {
encroachmentTrend,
mapMarkers,
monthlyMonitoring,
portalStats,
reportCards,
} from "../data/mockData";

function ChartPanel({ title, subtitle, children }) {
Expand All @@ -36,6 +44,60 @@ function ChartPanel({ title, subtitle, children }) {
}

function Dashboard() {
const [reports, setReports] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

// Fetch real reports from backend
useEffect(() => {
const loadReports = async () => {
try {
setLoading(true);
const data = await fetchReports();
setReports(data || []);
setError(null);
} catch (err) {
console.error("Failed to load reports:", err);
setError("Failed to load reports from server. Showing demo data.");
// Fallback to mock only if you want (optional)
// setReports(reportCards);
} finally {
setLoading(false);
}
};

loadReports();

// Optional: refresh every 30 seconds for "live" feel
const interval = setInterval(loadReports, 30000);
return () => clearInterval(interval);
}, []);

// Handler for Acknowledge button
const handleAcknowledge = async (reportId) => {
try {
await acknowledgeReport(reportId);
// Refresh list after action
const updated = await fetchReports();
setReports(updated);
} catch (err) {
alert("Failed to acknowledge report");
console.error(err);
}
};

// Handler for Resolve button
const handleResolve = async (reportId) => {
try {
await resolveReport(reportId);
const updated = await fetchReports();
setReports(updated);
} catch (err) {
alert("Failed to resolve report");
console.error(err);
}
};

return (
<div className="space-y-6">
<section className="rounded-[32px] border border-sky-900/10 bg-gradient-to-r from-sky-950 via-sky-900 to-emerald-800 p-7 text-white shadow-[0_30px_80px_-40px_rgba(15,23,42,0.75)]">
Expand All @@ -59,66 +121,121 @@ function Dashboard() {
tone="error"
/>

<section className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<ChartPanel
title="Encroachment Trend"
subtitle="Monthly comparison of detected encroachments and resolved actions."
>
<div className="h-80 min-w-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={encroachmentTrend}>
<CartesianGrid strokeDasharray="3 3" stroke="#cbd5e1" />
<XAxis dataKey="month" stroke="#475569" />
<YAxis stroke="#475569" />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="detected" stroke="#0369a1" strokeWidth={3} />
<Line type="monotone" dataKey="resolved" stroke="#0f766e" strokeWidth={3} />
</LineChart>
</ResponsiveContainer>
</div>
</ChartPanel>

<ChartPanel
title="Monthly Monitoring Load"
subtitle="Survey scan volume against alert creation across the current cycle."
>
<div className="h-80 min-w-0">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={monthlyMonitoring}>
<CartesianGrid strokeDasharray="3 3" stroke="#cbd5e1" />
<XAxis dataKey="period" stroke="#475569" />
<YAxis stroke="#475569" />
<Tooltip />
<Legend />
<Bar dataKey="scans" fill="#0f766e" radius={[8, 8, 0, 0]} />
<Bar dataKey="alerts" fill="#0f172a" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</ChartPanel>
</section>
{error && (
<StatusAlert title="Connection Issue" message={error} tone="error" />
)}

<section className="grid gap-6 xl:grid-cols-[1.15fr_0.85fr]">
<div>
<div className="mb-4">
<p className="section-kicker">Geospatial View</p>
<h2 className="section-title">Critical encroachment locations</h2>
</div>
<MapView center={[11.4, 78.1]} markers={mapMarkers} />
{loading ? (
<div className="text-center py-12">
<LoadingSpinner label="Loading real-time reports..." />
</div>
<div className="space-y-4">
<div className="mb-4">
<p className="section-kicker">Recent Cases</p>
<h2 className="section-title">Latest report activity</h2>
</div>
{reportCards.slice(0, 2).map((report) => (
<ReportCard key={report.id} report={report} />
))}
</div>
</section>
) : (
<>
<section className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<ChartPanel
title="Encroachment Trend"
subtitle="Monthly comparison of detected encroachments and resolved actions."
>
<div className="h-80 min-w-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={encroachmentTrend}>
<CartesianGrid strokeDasharray="3 3" stroke="#cbd5e1" />
<XAxis dataKey="month" stroke="#475569" />
<YAxis stroke="#475569" />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="detected" stroke="#0369a1" strokeWidth={3} />
<Line type="monotone" dataKey="resolved" stroke="#0f766e" strokeWidth={3} />
</LineChart>
</ResponsiveContainer>
</div>
</ChartPanel>

<ChartPanel
title="Monthly Monitoring Load"
subtitle="Survey scan volume against alert creation across the current cycle."
>
<div className="h-80 min-w-0">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={monthlyMonitoring}>
<CartesianGrid strokeDasharray="3 3" stroke="#cbd5e1" />
<XAxis dataKey="period" stroke="#475569" />
<YAxis stroke="#475569" />
<Tooltip />
<Legend />
<Bar dataKey="scans" fill="#0f766e" radius={[8, 8, 0, 0]} />
<Bar dataKey="alerts" fill="#0f172a" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</ChartPanel>
</section>

<section className="grid gap-6 xl:grid-cols-[1.15fr_0.85fr]">
<div>
<div className="mb-4">
<p className="section-kicker">Geospatial View</p>
<h2 className="section-title">Critical encroachment locations</h2>
</div>
<MapView center={[11.4, 78.1]} markers={mapMarkers} />
</div>

<div className="space-y-4">
<div className="mb-4">
<p className="section-kicker">Recent Cases</p>
<h2 className="section-title">Latest report activity</h2>
</div>

{reports.length === 0 ? (
<div className="text-center py-8 text-slate-500">
No recent reports yet. Submit one to see live data.
</div>
) : (
reports.slice(0, 4).map((report) => ( // show latest 4
<ReportCard
key={report.id}
report={{
...report,
// Map backend fields to what ReportCard expects
category: report.source?.toUpperCase() || "Unknown",
location: report.location_name || `${report.latitude?.toFixed(4)}, ${report.longitude?.toFixed(4)}`,
description: report.description,
status: report.status?.charAt(0).toUpperCase() + report.status?.slice(1),
reportedBy: report.user_id ? "User" : "System (Satellite)",
date: new Date(report.created_at).toLocaleDateString(),
photo: report.image_url ? "View" : null,
// Extra fields for new UI
severity_label: report.severity_label,
severity_score: report.severity_score,
escalation_level: report.escalation_level || 1,
}}
actions={
report.status === "pending" && (
<div className="flex gap-3 mt-4">
<button
onClick={() => handleAcknowledge(report.id)}
className="btn-primary px-5 py-2 text-sm"
>
Acknowledge
</button>
<button
onClick={() => handleResolve(report.id)}
className="btn-secondary px-5 py-2 text-sm"
>
Resolve
</button>
</div>
)
}
/>
))
)}
</div>
</section>
</>
)}
</div>
);
}

export default Dashboard;
export default Dashboard;
21 changes: 21 additions & 0 deletions Web/frontend/src/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import axios from "axios";
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || "http://localhost:5000/api",
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});

api.interceptors.request.use((config) => {
const token = localStorage.getItem("supabase.auth.token"); // or from your auth context
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

export async function fetchReports() {
Expand All @@ -15,6 +26,16 @@ export async function submitEncroachmentReport(payload) {
return data;
}

export const acknowledgeReport = async (id) => {
const { data } = await api.put(`/api/reports/${id}/acknowledge`);
return data;
};

export const resolveReport = async (id) => {
const { data } = await api.put(`/api/reports/${id}/resolve`);
return data;
};

export async function fetchWaterbodies() {
const { data } = await api.get("/waterbodies");
return data;
Expand Down
Loading