refactor: refactor the color schema
This commit is contained in:
590
src/app/(modules)/dashboard/page.tsx
Normal file
590
src/app/(modules)/dashboard/page.tsx
Normal file
@@ -0,0 +1,590 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
AlertCircle,
|
||||
Activity,
|
||||
MapPin as MapPinIcon,
|
||||
BarChart3,
|
||||
AlertTriangle,
|
||||
Zap,
|
||||
PencilLine,
|
||||
CheckCircle2,
|
||||
} from "lucide-react";
|
||||
import { StatsCard } from "@/components/dashboard/stats-card";
|
||||
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector";
|
||||
import { FilterSelector } from "@/components/dashboard/filter-selector";
|
||||
import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart";
|
||||
import { LocationBarChart } from "@/components/dashboard/location-bar-chart";
|
||||
import { DashboardMap } from "@/components/dashboard/dashboard-map";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { fetchProjects, type Project } from "@/lib/api";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Filter } from "lucide-react";
|
||||
import { DashboardSkeleton } from "@/components/dashboard/dashboard-skeleton"
|
||||
import { PoweredBy } from "@/components/powered-by";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
interface ProjectSummary {
|
||||
project: {
|
||||
id: string;
|
||||
name: string;
|
||||
corridor_name: string | null;
|
||||
state: string | null;
|
||||
};
|
||||
packages: {
|
||||
[key: string]: {
|
||||
package_id: string;
|
||||
region: string | null;
|
||||
locations: {
|
||||
[key: string]: {
|
||||
location_id: string;
|
||||
chainage: string | null;
|
||||
detection_count: number;
|
||||
detections: Array<{
|
||||
id: number;
|
||||
type: string;
|
||||
class: string;
|
||||
confidence: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface DetectionStats {
|
||||
totalDefectedSignboard: number;
|
||||
totalPothole: number;
|
||||
totalRoadCrack: number;
|
||||
totalDamagedRoadMarking: number;
|
||||
totalGoodSignboard: number;
|
||||
totalRoadDamage: number;
|
||||
locationData: Array<{
|
||||
name: string;
|
||||
defected_sign_board: number;
|
||||
pothole: number;
|
||||
road_crack: number;
|
||||
damaged_road_marking: number;
|
||||
good_sign_board: number;
|
||||
total: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
||||
if (!summary) {
|
||||
return {
|
||||
totalDefectedSignboard: 0,
|
||||
totalPothole: 0,
|
||||
totalRoadCrack: 0,
|
||||
totalDamagedRoadMarking: 0,
|
||||
totalGoodSignboard: 0,
|
||||
totalRoadDamage: 0,
|
||||
locationData: [],
|
||||
};
|
||||
}
|
||||
|
||||
let totalDefectedSignboard = 0;
|
||||
let totalPothole = 0;
|
||||
let totalRoadCrack = 0;
|
||||
let totalDamagedRoadMarking = 0;
|
||||
let totalGoodSignboard = 0;
|
||||
let totalRoadDamage = 0;
|
||||
const locationData: DetectionStats["locationData"] = [];
|
||||
|
||||
for (const pkg of Object.values(summary.packages || {})) {
|
||||
for (const [locName, loc] of Object.entries(pkg.locations || {})) {
|
||||
let locDefectedSignboard = 0;
|
||||
let locPothole = 0;
|
||||
let locRoadCrack = 0;
|
||||
let locDamagedRoadMarking = 0;
|
||||
let locGoodSignboard = 0;
|
||||
|
||||
for (const detection of loc.detections || []) {
|
||||
const type = detection.type.toLowerCase();
|
||||
if (type === "defected_sign_board") {
|
||||
locDefectedSignboard++;
|
||||
totalDefectedSignboard++;
|
||||
} else if (type === "pothole") {
|
||||
locPothole++;
|
||||
totalPothole++;
|
||||
} else if (type === "road_crack") {
|
||||
locRoadCrack++;
|
||||
totalRoadCrack++;
|
||||
} else if (type === "damaged_road_marking") {
|
||||
locDamagedRoadMarking++;
|
||||
totalDamagedRoadMarking++;
|
||||
} else if (type === "good_sign_board") {
|
||||
locGoodSignboard++;
|
||||
totalGoodSignboard++;
|
||||
}
|
||||
}
|
||||
|
||||
const locTotalDamage =
|
||||
locDefectedSignboard +
|
||||
locPothole +
|
||||
locRoadCrack +
|
||||
locDamagedRoadMarking;
|
||||
totalRoadDamage +=
|
||||
locDefectedSignboard > 0 ||
|
||||
locPothole > 0 ||
|
||||
locRoadCrack > 0 ||
|
||||
locDamagedRoadMarking > 0
|
||||
? 1
|
||||
: 0; // This logic might need refinement based on "total_road_damage" definition
|
||||
|
||||
if (
|
||||
locDefectedSignboard > 0 ||
|
||||
locPothole > 0 ||
|
||||
locRoadCrack > 0 ||
|
||||
locDamagedRoadMarking > 0 ||
|
||||
locGoodSignboard > 0
|
||||
) {
|
||||
const shortName =
|
||||
locName.length > 20 ? locName.substring(0, 20) + "..." : locName;
|
||||
locationData.push({
|
||||
name: shortName,
|
||||
defected_sign_board: locDefectedSignboard,
|
||||
pothole: locPothole,
|
||||
road_crack: locRoadCrack,
|
||||
damaged_road_marking: locDamagedRoadMarking,
|
||||
good_sign_board: locGoodSignboard,
|
||||
total:
|
||||
locDefectedSignboard +
|
||||
locPothole +
|
||||
locRoadCrack +
|
||||
locDamagedRoadMarking +
|
||||
locGoodSignboard,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate totalRoadDamage based on backends unique count
|
||||
// But since we are aggregating from locations, we just sum them up or use a simpler metric
|
||||
// The user's JSON shows "total_road_damage": 9 which is sum of 2+6+0+1
|
||||
totalRoadDamage =
|
||||
totalDefectedSignboard +
|
||||
totalPothole +
|
||||
totalRoadCrack +
|
||||
totalDamagedRoadMarking;
|
||||
|
||||
return {
|
||||
totalDefectedSignboard,
|
||||
totalPothole,
|
||||
totalRoadCrack,
|
||||
totalDamagedRoadMarking,
|
||||
totalGoodSignboard,
|
||||
totalRoadDamage,
|
||||
locationData,
|
||||
};
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(
|
||||
null,
|
||||
);
|
||||
const [stats, setStats] = useState<DetectionStats>({
|
||||
totalDefectedSignboard: 0,
|
||||
totalPothole: 0,
|
||||
totalRoadCrack: 0,
|
||||
totalDamagedRoadMarking: 0,
|
||||
totalGoodSignboard: 0,
|
||||
totalRoadDamage: 0,
|
||||
locationData: [],
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load projects on mount
|
||||
useEffect(() => {
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const projectsData = await fetchProjects();
|
||||
setProjects(projectsData);
|
||||
|
||||
if (projectsData.length > 0) {
|
||||
setSelectedProjectId(projectsData[0].id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load projects:", err);
|
||||
setError(
|
||||
"Failed to load projects. Please check if the backend is running.",
|
||||
);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
loadProjects();
|
||||
}, []);
|
||||
|
||||
const selectedProject = projects.find((p) => p.id === selectedProjectId);
|
||||
|
||||
// Extract packages from project summary
|
||||
const packages = projectSummary
|
||||
? Object.keys(projectSummary.packages || {}).map((pkgName) => ({
|
||||
id: pkgName,
|
||||
name: pkgName,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Extract locations from selected package
|
||||
const [selectedPackageId, setSelectedPackageId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedLocationId, setSelectedLocationId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const locations =
|
||||
projectSummary && selectedPackageId && selectedPackageId !== "all"
|
||||
? Object.keys(
|
||||
projectSummary.packages[selectedPackageId]?.locations || {},
|
||||
).map((locName) => ({
|
||||
id: locName,
|
||||
name: locName,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Load project summary when project changes
|
||||
useEffect(() => {
|
||||
if (!selectedProjectId) {
|
||||
setProjectSummary(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadProjectSummary = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(
|
||||
`${API_URL}/summary/projects/${selectedProjectId}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"ngrok-skip-browser-warning": "true",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const summary: ProjectSummary = await response.json();
|
||||
setProjectSummary(summary);
|
||||
} catch (err) {
|
||||
console.error("Failed to load project summary:", err);
|
||||
setError("Failed to load project summary.");
|
||||
setProjectSummary(null);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
loadProjectSummary();
|
||||
}, [selectedProjectId]);
|
||||
|
||||
// Reset package and location when project changes
|
||||
useEffect(() => {
|
||||
setSelectedPackageId(null);
|
||||
setSelectedLocationId(null);
|
||||
}, [selectedProjectId]);
|
||||
|
||||
// Reset location when package changes
|
||||
useEffect(() => {
|
||||
setSelectedLocationId(null);
|
||||
}, [selectedPackageId]);
|
||||
|
||||
// Filter stats based on selections
|
||||
useEffect(() => {
|
||||
if (!projectSummary) {
|
||||
setStats({
|
||||
totalDefectedSignboard: 0,
|
||||
totalPothole: 0,
|
||||
totalRoadCrack: 0,
|
||||
totalDamagedRoadMarking: 0,
|
||||
totalGoodSignboard: 0,
|
||||
totalRoadDamage: 0,
|
||||
locationData: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let totalDefectedSignboard = 0;
|
||||
let totalPothole = 0;
|
||||
let totalRoadCrack = 0;
|
||||
let totalDamagedRoadMarking = 0;
|
||||
let totalGoodSignboard = 0;
|
||||
const locationData: DetectionStats["locationData"] = [];
|
||||
|
||||
const packagesToProcess =
|
||||
selectedPackageId && selectedPackageId !== "all"
|
||||
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
|
||||
: projectSummary.packages || {};
|
||||
|
||||
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
|
||||
const locationsToProcess =
|
||||
selectedLocationId && selectedLocationId !== "all"
|
||||
? { [selectedLocationId]: pkg.locations[selectedLocationId] }
|
||||
: pkg.locations || {};
|
||||
|
||||
for (const [locName, loc] of Object.entries(locationsToProcess)) {
|
||||
if (!loc) continue;
|
||||
|
||||
let locDefectedSignboard = 0;
|
||||
let locPothole = 0;
|
||||
let locRoadCrack = 0;
|
||||
let locDamagedRoadMarking = 0;
|
||||
let locGoodSignboard = 0;
|
||||
|
||||
for (const detection of loc.detections || []) {
|
||||
const type = detection.type.toLowerCase();
|
||||
if (type === "defected_sign_board") {
|
||||
locDefectedSignboard++;
|
||||
totalDefectedSignboard++;
|
||||
} else if (type === "pothole") {
|
||||
locPothole++;
|
||||
totalPothole++;
|
||||
} else if (type === "road_crack") {
|
||||
locRoadCrack++;
|
||||
totalRoadCrack++;
|
||||
} else if (type === "damaged_road_marking") {
|
||||
locDamagedRoadMarking++;
|
||||
totalDamagedRoadMarking++;
|
||||
} else if (type === "good_sign_board") {
|
||||
locGoodSignboard++;
|
||||
totalGoodSignboard++;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
locDefectedSignboard > 0 ||
|
||||
locPothole > 0 ||
|
||||
locRoadCrack > 0 ||
|
||||
locDamagedRoadMarking > 0 ||
|
||||
locGoodSignboard > 0
|
||||
) {
|
||||
const shortName =
|
||||
locName.length > 20 ? locName.substring(0, 20) + "..." : locName;
|
||||
locationData.push({
|
||||
name: shortName,
|
||||
defected_sign_board: locDefectedSignboard,
|
||||
pothole: locPothole,
|
||||
road_crack: locRoadCrack,
|
||||
damaged_road_marking: locDamagedRoadMarking,
|
||||
good_sign_board: locGoodSignboard,
|
||||
total:
|
||||
locDefectedSignboard +
|
||||
locPothole +
|
||||
locRoadCrack +
|
||||
locDamagedRoadMarking +
|
||||
locGoodSignboard,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setStats({
|
||||
totalDefectedSignboard,
|
||||
totalPothole,
|
||||
totalRoadCrack,
|
||||
totalDamagedRoadMarking,
|
||||
totalGoodSignboard,
|
||||
totalRoadDamage:
|
||||
totalDefectedSignboard +
|
||||
totalPothole +
|
||||
totalRoadCrack +
|
||||
totalDamagedRoadMarking,
|
||||
locationData,
|
||||
});
|
||||
}, [projectSummary, selectedPackageId, selectedLocationId]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||
{/* Main Content */}
|
||||
<main className="min-h-screen">
|
||||
<div className="p-4 px-8 max-w-340 mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
description="A Comprehensive Overview Of Your Road Infrastructure Analysis"
|
||||
icon={BarChart3}
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="gap-2 font-semibold">
|
||||
<Filter className="h-4 w-4" />
|
||||
<span>Filters</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[320px] p-0 overflow-hidden" align="end">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<h3 className="font-bold text-sm flex items-center gap-2 text-foreground">
|
||||
<Filter className="h-4 w-4 text-primary" />
|
||||
Filter Analysis
|
||||
</h3>
|
||||
<p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or location</p>
|
||||
</div>
|
||||
<div>
|
||||
<FilterSelector
|
||||
projects={projects}
|
||||
selectedProjectId={selectedProjectId}
|
||||
selectedPackageId={selectedPackageId}
|
||||
selectedLocationId={selectedLocationId}
|
||||
onProjectChange={setSelectedProjectId}
|
||||
onPackageChange={setSelectedPackageId}
|
||||
onLocationChange={setSelectedLocationId}
|
||||
packages={packages}
|
||||
locations={locations}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mb-4 flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 text-sm">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && !projectSummary ? (
|
||||
<DashboardSkeleton />
|
||||
) : (
|
||||
<>
|
||||
{/* Stats Cards - Top Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<StatsCard
|
||||
title="Total Road Damage"
|
||||
subtitle="Combined damage detections"
|
||||
value={stats.totalRoadDamage}
|
||||
icon={Activity}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Potholes"
|
||||
subtitle="Surface depressions"
|
||||
value={stats.totalPothole}
|
||||
icon={AlertCircle}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Defected Signboards"
|
||||
subtitle="Damaged traffic signs"
|
||||
value={stats.totalDefectedSignboard}
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Road Cracks"
|
||||
subtitle="Surface fissures"
|
||||
value={stats.totalRoadCrack}
|
||||
icon={Zap}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Damaged Markings"
|
||||
subtitle="Worn road lines"
|
||||
value={stats.totalDamagedRoadMarking}
|
||||
icon={PencilLine}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Good Signboards"
|
||||
subtitle="Informational markers"
|
||||
value={stats.totalGoodSignboard}
|
||||
icon={CheckCircle2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts Row - Side by Side */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
{/* Left Chart - Detection Distribution */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||
<div className="p-2 rounded-md bg-secondary text-secondary-foreground">
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
</div>
|
||||
<span className="text-xl">
|
||||
Detection Distribution
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
<DetectionDonutChart
|
||||
defectedSignboard={stats.totalDefectedSignboard}
|
||||
pothole={stats.totalPothole}
|
||||
roadCrack={stats.totalRoadCrack}
|
||||
damagedRoadMarking={stats.totalDamagedRoadMarking}
|
||||
goodSignboard={stats.totalGoodSignboard}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Right Chart - Location Bar Chart */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||
<div className="p-2 rounded-md bg-secondary text-secondary-foreground">
|
||||
<MapPinIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<span className="text-xl">Detections by Location</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
<LocationBarChart
|
||||
data={stats.locationData.map((loc) => ({
|
||||
name: loc.name,
|
||||
defected_sign_board: loc.defected_sign_board,
|
||||
pothole: loc.pothole,
|
||||
road_crack: loc.road_crack,
|
||||
damaged_road_marking: loc.damaged_road_marking,
|
||||
good_sign_board: loc.good_sign_board,
|
||||
total: loc.total,
|
||||
}))}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Map - Full Width Below Charts */}
|
||||
<div>
|
||||
<DashboardMap
|
||||
selectedProjectId={selectedProjectId}
|
||||
selectedPackageId={selectedPackageId}
|
||||
selectedLocationId={selectedLocationId}
|
||||
projectSummary={projectSummary}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<PoweredBy />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user