'use client'; import { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter, } from '@/components/ui/card'; import { AlertCircle, Activity, MapPin as MapPinIcon, Milestone, BarChart3, AlertTriangle, Zap, PencilLine, CheckCircle2, Settings2, } from 'lucide-react'; import { StatsCard } from '@/components/dashboard/stats-card'; import { FilterSelector } from '@/components/dashboard/filter-selector'; import { DetectionDonutChart } from '@/components/dashboard/detection-donut-chart'; import { ChainageBarChart } from '@/components/dashboard/chainage-bar-chart'; import { DashboardMap } from '@/components/dashboard/dashboard-map'; import { PageHeader } from '@/components/page-header'; import { projectService } from '@/services/api'; import { Project } from '@/types'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { DashboardSkeleton } from '@/components/dashboard/dashboard-skeleton'; import { PoweredBy } from '@/components/powered-by'; import { toast } from 'sonner'; import { Reveal, StaggerContainer, StaggerItem } from '@/components/ui/reveal'; 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; chainages: { [key: string]: { chainage_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; chainageData: 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, chainageData: [], }; } let total_defected_sign_board = 0; let total_pothole = 0; let total_road_crack = 0; let total_damaged_road_marking = 0; let total_good_sign_board = 0; let total_road_damage = 0; const chainageData: DetectionStats['chainageData'] = []; for (const pkg of Object.values(summary.packages || {})) { for (const [chnName, chn] of Object.entries(pkg.chainages || {})) { // Changed 'locName, loc' to 'chnName, chn' and 'pkg.locations' to 'pkg.chainages' let chnDefectedSignboard = 0; // Changed 'locDefectedSignboard' to 'chnDefectedSignboard' let chnPothole = 0; // Changed 'locPothole' to 'chnPothole' let chnRoadCrack = 0; // Changed 'locRoadCrack' to 'chnRoadCrack' let chnDamagedRoadMarking = 0; // Changed 'locDamagedRoadMarking' to 'chnDamagedRoadMarking' let chnGoodSignboard = 0; // Changed 'locGoodSignboard' to 'chnGoodSignboard' for (const detection of chn.detections || []) { // Changed 'loc.detections' to 'chn.detections' const type = detection.type.toLowerCase(); if (type === 'defected_sign_board') { chnDefectedSignboard++; total_defected_sign_board++; } else if (type === 'pothole') { chnPothole++; total_pothole++; } else if (type === 'road_crack') { chnRoadCrack++; total_road_crack++; } else if (type === 'damaged_road_marking') { chnDamagedRoadMarking++; total_damaged_road_marking++; } else if (type === 'good_sign_board') { chnGoodSignboard++; total_good_sign_board++; } } const chnTotalDamage = // Changed 'locTotalDamage' to 'chnTotalDamage' chnDefectedSignboard + chnPothole + chnRoadCrack + chnDamagedRoadMarking; if ( chnDefectedSignboard > 0 || chnPothole > 0 || chnRoadCrack > 0 || chnDamagedRoadMarking > 0 || chnGoodSignboard > 0 ) { const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName; chainageData.push({ // Changed 'locationData.push' to 'chainageData.push' name: shortName, defected_sign_board: chnDefectedSignboard, pothole: chnPothole, road_crack: chnRoadCrack, damaged_road_marking: chnDamagedRoadMarking, good_sign_board: chnGoodSignboard, total: chnDefectedSignboard + chnPothole + chnRoadCrack + chnDamagedRoadMarking + chnGoodSignboard, }); } } } total_road_damage = total_defected_sign_board + total_pothole + total_road_crack + total_damaged_road_marking; return { totalDefectedSignboard: total_defected_sign_board, totalPothole: total_pothole, totalRoadCrack: total_road_crack, totalDamagedRoadMarking: total_damaged_road_marking, totalGoodSignboard: total_good_sign_board, totalRoadDamage: total_road_damage, chainageData, }; } export default function DashboardPage() { const [isLoading, setIsLoading] = useState(true); const [projects, setProjects] = useState([]); const [selectedProjectId, setSelectedProjectId] = useState(null); const [projectSummary, setProjectSummary] = useState(null); const [stats, setStats] = useState({ totalDefectedSignboard: 0, totalPothole: 0, totalRoadCrack: 0, totalDamagedRoadMarking: 0, totalGoodSignboard: 0, totalRoadDamage: 0, chainageData: [], }); // Load projects on mount useEffect(() => { const loadProjects = async () => { try { setIsLoading(true); const projectsData = await projectService.getProjects(); setProjects(projectsData.items); if (projectsData.items.length > 0) { setSelectedProjectId(projectsData.items[0].id); } } catch (err) { console.error('Failed to load projects:', err); toast.error('Failed to load projects', { description: '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 chainages from selected package const [selectedPackageId, setSelectedPackageId] = useState(null); const [selectedChainageId, setSelectedChainageId] = useState(null); // Changed 'selectedLocationId' to 'selectedChainageId' const chainages = projectSummary && selectedPackageId && selectedPackageId !== 'all' ? Object.keys(projectSummary.packages[selectedPackageId]?.chainages || {}).map((chnName) => ({ id: chnName, name: chnName, })) : []; // Load project summary when project changes useEffect(() => { if (!selectedProjectId) { setProjectSummary(null); return; } const loadProjectSummary = async () => { try { setIsLoading(true); const summary: ProjectSummary = await projectService.getProjectSummary(selectedProjectId); setProjectSummary(summary); } catch (err) { console.error('Failed to load project summary:', err); toast.error('Failed to load project summary'); setProjectSummary(null); } finally { setTimeout(() => { setIsLoading(false); }, 1500); } }; loadProjectSummary(); }, [selectedProjectId]); // Reset package and chainage when project changes useEffect(() => { setSelectedPackageId(null); setSelectedChainageId(null); // Changed 'setSelectedLocationId' to 'setSelectedChainageId' }, [selectedProjectId]); // Reset chainage when package changes useEffect(() => { setSelectedChainageId(null); }, [selectedPackageId]); // Filter stats based on selections useEffect(() => { if (!projectSummary) { setStats({ totalDefectedSignboard: 0, totalPothole: 0, totalRoadCrack: 0, totalDamagedRoadMarking: 0, totalGoodSignboard: 0, totalRoadDamage: 0, chainageData: [], // Changed 'locationData' to 'chainageData' }); return; } let totalDefectedSignboard = 0; let totalPothole = 0; let totalRoadCrack = 0; let totalDamagedRoadMarking = 0; let totalGoodSignboard = 0; const chainageData: DetectionStats['chainageData'] = []; const packagesToProcess = selectedPackageId && selectedPackageId !== 'all' ? { [selectedPackageId]: projectSummary.packages[selectedPackageId] } : projectSummary.packages || {}; for (const [pkgName, pkg] of Object.entries(packagesToProcess)) { const chainagesToProcess = selectedChainageId && selectedChainageId !== 'all' ? { [selectedChainageId]: pkg.chainages[selectedChainageId] } : pkg.chainages || {}; for (const [chnName, chn] of Object.entries(chainagesToProcess)) { if (!chn) continue; let chnDefectedSignboard = 0; let chnPothole = 0; let chnRoadCrack = 0; let chnDamagedRoadMarking = 0; let chnGoodSignboard = 0; for (const detection of chn.detections || []) { const type = detection.type.toLowerCase(); if (type === 'defected_sign_board') { chnDefectedSignboard++; totalDefectedSignboard++; } else if (type === 'pothole') { chnPothole++; totalPothole++; } else if (type === 'road_crack') { chnRoadCrack++; totalRoadCrack++; } else if (type === 'damaged_road_marking') { chnDamagedRoadMarking++; totalDamagedRoadMarking++; } else if (type === 'good_sign_board') { chnGoodSignboard++; totalGoodSignboard++; } } if ( chnDefectedSignboard > 0 || chnPothole > 0 || chnRoadCrack > 0 || chnDamagedRoadMarking > 0 || chnGoodSignboard > 0 ) { const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName; chainageData.push({ name: shortName, defected_sign_board: chnDefectedSignboard, pothole: chnPothole, road_crack: chnRoadCrack, damaged_road_marking: chnDamagedRoadMarking, good_sign_board: chnGoodSignboard, total: chnDefectedSignboard + chnPothole + chnRoadCrack + chnDamagedRoadMarking + chnGoodSignboard, }); } } } setStats({ totalDefectedSignboard, totalPothole, totalRoadCrack, totalDamagedRoadMarking, totalGoodSignboard, totalRoadDamage: totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking, chainageData, }); }, [projectSummary, selectedPackageId, selectedChainageId]); return ( <> {/* Main Content */}
{/* Header */}

Filter Analysis

Refine your view by project, package, or chainage

{isLoading && !projectSummary ? ( ) : ( <> {/* Stats Cards - Top Row */} {/* Charts Row - Side by Side */}
Detection Distribution Breakdown of all detected road conditions
Total detections: {stats.totalRoadDamage + stats.totalGoodSignboard}
Severity by Chainage Detections grouped by road segment
Analysis based on latest processed sequence
{/* Map - Full Width Below Charts */}
)}
); }