refactor: remove static routes and stop multiple token api call in sse

This commit is contained in:
2026-06-23 22:48:34 +05:30
parent b282fdf56d
commit 5e4cf61d6e
25 changed files with 129 additions and 2086 deletions

View File

@@ -1,678 +0,0 @@
'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,
Droplets,
} 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, projectSummaryService } 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 { 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;
totalDrainIssue: number;
totalDefectiveCulvert: number;
totalRoadDamage: number;
chainageData: Array<{
name: string;
defected_sign_board: number;
pothole: number;
road_crack: number;
damaged_road_marking: number;
// good_sign_board: number;
drain_issue: number;
defective_culvert: number;
total: number;
}>;
}
function calculateStats(summary: ProjectSummary | null): DetectionStats {
if (!summary) {
return {
totalDefectedSignboard: 0,
totalPothole: 0,
totalRoadCrack: 0,
totalDamagedRoadMarking: 0,
totalGoodSignboard: 0,
totalDrainIssue: 0,
totalDefectiveCulvert: 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_drain_issue = 0;
let total_defective_culvert = 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 || {})) {
let chnDefectedSignboard = 0;
let chnPothole = 0;
let chnRoadCrack = 0;
let chnDamagedRoadMarking = 0;
let chnGoodSignboard = 0;
let chnDrainIssue = 0;
let chnDefectiveCulvert = 0;
for (const detection of 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++;
} else if (type === 'drain_issue') {
chnDrainIssue++;
total_drain_issue++;
} else if (type === 'defective_culvert') {
chnDefectiveCulvert++;
total_defective_culvert++;
}
}
if (
chnDefectedSignboard > 0 ||
chnPothole > 0 ||
chnRoadCrack > 0 ||
chnDamagedRoadMarking > 0 ||
chnGoodSignboard > 0 ||
chnDrainIssue > 0 ||
chnDefectiveCulvert > 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,
drain_issue: chnDrainIssue,
defective_culvert: chnDefectiveCulvert,
total:
chnDefectedSignboard +
chnPothole +
chnRoadCrack +
chnDamagedRoadMarking +
// chnGoodSignboard +
chnDrainIssue +
chnDefectiveCulvert,
});
}
}
}
total_road_damage =
total_defected_sign_board +
total_pothole +
total_road_crack +
total_damaged_road_marking +
total_drain_issue +
total_defective_culvert;
return {
totalDefectedSignboard: total_defected_sign_board,
totalPothole: total_pothole,
totalRoadCrack: total_road_crack,
totalDamagedRoadMarking: total_damaged_road_marking,
totalGoodSignboard: total_good_sign_board,
totalDrainIssue: total_drain_issue,
totalDefectiveCulvert: total_defective_culvert,
totalRoadDamage: total_road_damage,
chainageData,
};
}
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,
totalDrainIssue: 0,
totalDefectiveCulvert: 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[projectsData.items.length - 1].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<string | null>(
null,
);
const [selectedChainageId, setSelectedChainageId] = useState<string | null>(
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;
}
setProjectSummary(null);
const loadProjectSummary = async () => {
try {
setIsLoading(true);
const summary =
await projectSummaryService.getProjectSummary<ProjectSummary>(
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);
}, [selectedProjectId]);
// Auto-select last package when project summary is loaded
useEffect(() => {
if (projectSummary && selectedPackageId === null) {
const packageIds = Object.keys(projectSummary.packages || {});
if (packageIds.length > 0) {
setSelectedPackageId(packageIds[packageIds.length - 1]);
}
}
}, [projectSummary, selectedPackageId]);
// Reset chainage when package changes
useEffect(() => {
setSelectedChainageId(null);
}, [selectedPackageId]);
// Auto-select last chainage when package is selected
useEffect(() => {
if (
projectSummary &&
selectedPackageId &&
selectedPackageId !== 'all' &&
selectedChainageId === null
) {
const chainageIds = Object.keys(
projectSummary.packages[selectedPackageId]?.chainages || {},
);
if (chainageIds.length > 0) {
setSelectedChainageId(chainageIds[chainageIds.length - 1]);
}
}
}, [projectSummary, selectedPackageId, selectedChainageId]);
// Filter stats based on selections
useEffect(() => {
if (!projectSummary) {
setStats({
totalDefectedSignboard: 0,
totalPothole: 0,
totalRoadCrack: 0,
totalDamagedRoadMarking: 0,
totalGoodSignboard: 0,
totalDrainIssue: 0,
totalDefectiveCulvert: 0,
totalRoadDamage: 0,
chainageData: [], // Changed 'locationData' to 'chainageData'
});
return;
}
let totalDefectedSignboard = 0;
let totalPothole = 0;
let totalRoadCrack = 0;
let totalDamagedRoadMarking = 0;
let totalGoodSignboard = 0;
let totalDrainIssue = 0;
let totalDefectiveCulvert = 0;
const chainageData: DetectionStats['chainageData'] = [];
const packagesToProcess =
selectedPackageId && selectedPackageId !== 'all'
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
: projectSummary.packages || {};
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
if (!pkg) continue;
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;
let chnDrainIssue = 0;
let chnDefectiveCulvert = 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++;
} else if (type === 'drain_issue') {
chnDrainIssue++;
totalDrainIssue++;
} else if (type === 'defective_culvert') {
chnDefectiveCulvert++;
totalDefectiveCulvert++;
}
}
if (
chnDefectedSignboard > 0 ||
chnPothole > 0 ||
chnRoadCrack > 0 ||
chnDamagedRoadMarking > 0 ||
chnGoodSignboard > 0 ||
chnDrainIssue > 0 ||
chnDefectiveCulvert > 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,
drain_issue: chnDrainIssue,
defective_culvert: chnDefectiveCulvert,
total:
chnDefectedSignboard +
chnPothole +
chnRoadCrack +
chnDamagedRoadMarking +
// chnGoodSignboard +
chnDrainIssue +
chnDefectiveCulvert,
});
}
}
}
setStats({
totalDefectedSignboard,
totalPothole,
totalRoadCrack,
totalDamagedRoadMarking,
totalGoodSignboard,
totalDrainIssue,
totalDefectiveCulvert,
totalRoadDamage:
totalDefectedSignboard +
totalPothole +
totalRoadCrack +
totalDamagedRoadMarking +
totalDrainIssue +
totalDefectiveCulvert,
chainageData,
});
}, [projectSummary, selectedPackageId, selectedChainageId]);
return (
<>
{/* Main Content */}
<main>
<div>
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<PageHeader
title="Dashboard"
description="A Comprehensive Overview Of Your Road Infrastructure Analysis"
icon={BarChart3}
/>
<Reveal direction="down" delay={0.2}>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="gap-2 font-semibold">
<Settings2 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">
<Milestone 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 segment
</p>
</div>
<div>
<FilterSelector
projects={projects}
selectedProjectId={selectedProjectId}
selectedPackageId={selectedPackageId}
selectedChainageId={selectedChainageId} // Changed 'selectedLocationId' to 'selectedChainageId'
onProjectChange={setSelectedProjectId}
onPackageChange={setSelectedPackageId}
onChainageChange={setSelectedChainageId} // Changed 'onLocationChange' to 'onChainageChange'
packages={packages}
chainages={chainages} // Changed 'locations' to 'chainages'
isLoading={isLoading}
/>
</div>
</PopoverContent>
</Popover>
</Reveal>
</div>
{isLoading && !projectSummary ? (
<DashboardSkeleton />
) : (
<>
{/* Stats Cards - Top Row */}
<StaggerContainer className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
<StaggerItem>
<StatsCard
title="Total Road Damage"
subtitle="Combined damage detections"
value={stats.totalRoadDamage}
icon={Activity}
/>
</StaggerItem>
<StaggerItem>
<StatsCard
title="Potholes"
subtitle="Surface depressions"
value={stats.totalPothole}
icon={AlertCircle}
/>
</StaggerItem>
<StaggerItem>
<StatsCard
title="Defected Signboards"
subtitle="Damaged traffic signs"
value={stats.totalDefectedSignboard}
icon={AlertTriangle}
/>
</StaggerItem>
<StaggerItem>
<StatsCard
title="Road Cracks"
subtitle="Surface fissures"
value={stats.totalRoadCrack}
icon={Zap}
/>
</StaggerItem>
<StaggerItem>
<StatsCard
title="Damaged Markings"
subtitle="Worn road lines"
value={stats.totalDamagedRoadMarking}
icon={PencilLine}
/>
</StaggerItem>
<StaggerItem>
<StatsCard
title="Drain Issues"
subtitle="Clogged or broken drainage"
value={stats.totalDrainIssue}
icon={Droplets}
/>
</StaggerItem>
{/* <StaggerItem>
<StatsCard
title="Good Signboards"
subtitle="Informational markers"
value={stats.totalGoodSignboard}
icon={CheckCircle2}
/>
</StaggerItem> */}
</StaggerContainer>
{/* Charts Row - Side by Side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
<Reveal delay={0.2} direction="left" className="flex flex-col">
<Card className="flex-1 h-full">
<CardHeader className="items-center pb-0">
<CardTitle>Detection Distribution</CardTitle>
<CardDescription>
Breakdown of all detected road conditions
</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">
<DetectionDonutChart
defectedSignboard={stats.totalDefectedSignboard}
pothole={stats.totalPothole}
roadCrack={stats.totalRoadCrack}
damagedRoadMarking={stats.totalDamagedRoadMarking}
// goodSignboard={stats.totalGoodSignboard}
drainIssue={stats.totalDrainIssue}
defectiveCulvert={stats.totalDefectiveCulvert}
/>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="leading-none text-muted-foreground">
Total detections:{' '}
{stats.totalRoadDamage +
// stats.totalGoodSignboard +
stats.totalDrainIssue +
stats.totalDefectiveCulvert}
</div>
</CardFooter>
</Card>
</Reveal>
<Reveal delay={0.3} direction="right" className="flex flex-col">
<Card className="flex-1 h-full">
<CardHeader className="items-center pb-0">
<CardTitle className="text-base font-bold">
Severity by Segment
</CardTitle>
<CardDescription className="text-xs">
Detections grouped by road segment
</CardDescription>
</CardHeader>
<CardContent className="h-[300px]">
<ChainageBarChart
data={stats.chainageData || []}
isLoading={isLoading}
/>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="leading-none text-muted-foreground">
Analysis based on latest processed sequence
</div>
</CardFooter>
</Card>
</Reveal>
</div>
{/* Map - Full Width Below Charts */}
<Reveal delay={0.4} direction="up">
<div>
<DashboardMap
selectedProjectId={selectedProjectId}
selectedPackageId={selectedPackageId}
selectedChainageId={selectedChainageId} // Changed 'selectedLocationId' to 'selectedChainageId'
projectSummary={projectSummary}
/>
</div>
</Reveal>
</>
)}
</div>
</main>
</>
);
}

View File

@@ -1,6 +1,18 @@
import { redirect } from 'next/navigation';
import { ROUTES } from '@/utils/routes';
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { AppLoader } from '@/components/AppLoader';
import { useAppStore } from '@/store/app.store';
export default function HomePage() {
redirect(ROUTES.DASHBOARD);
const router = useRouter();
const defaultRoute = useAppStore((state) => state.defaultRoute);
useEffect(() => {
router.replace(defaultRoute ?? '/access');
}, [defaultRoute, router]);
return <AppLoader label="Opening workspace" />;
}

View File

@@ -1,6 +1,6 @@
'use client';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { API_ROUTES } from '@/constants/apiRoutes';
@@ -125,6 +125,7 @@ function mergeTicketListItem(
export function useTenantTicketTableEvents(enabled = true) {
const queryClient = useQueryClient();
const hasInvalidatedAfterConnectionErrorRef = useRef(false);
const events = useMemo(
() => ({
@@ -154,14 +155,22 @@ export function useTenantTicketTableEvents(enabled = true) {
);
const onConnectionError = useCallback(() => {
if (hasInvalidatedAfterConnectionErrorRef.current) return;
hasInvalidatedAfterConnectionErrorRef.current = true;
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
}, [queryClient]);
const onConnectionOpen = useCallback(() => {
hasInvalidatedAfterConnectionErrorRef.current = false;
}, []);
useSseWithToken({
enabled,
getToken: ticketService.createTenantTicketEventsToken,
getPath,
events,
onConnectionError,
onConnectionOpen,
});
}