From 5e4cf61d6e5adf93d13e98ad2b1d3f94b357d8e9 Mon Sep 17 00:00:00 2001
From: "santasri.pachhal"
Date: Tue, 23 Jun 2026 22:48:34 +0530
Subject: [PATCH] refactor: remove static routes and stop multiple token api
call in sse
---
package-lock.json | 51 --
package.json | 3 -
src/app/(modules)/dashboard/page.tsx | 678 ------------------
src/app/(modules)/page.tsx | 18 +-
.../hooks/useTenantTicketTableEvents.ts | 11 +-
src/app/not-found.tsx | 3 +-
.../dashboard/chainage-bar-chart.tsx | 143 ----
.../dashboard/dashboard-map-content.tsx | 166 -----
src/components/dashboard/dashboard-map.tsx | 216 ------
.../dashboard/dashboard-skeleton.tsx | 102 ---
.../dashboard/detection-donut-chart.tsx | 176 -----
src/components/dashboard/filter-selector.tsx | 131 ----
src/components/dashboard/stats-card.tsx | 57 --
src/components/map-modal.tsx | 217 ------
src/config/menu.config.ts | 39 +-
src/constants/apiRoutes.ts | 3 -
src/guards/GuestGuard.tsx | 8 +-
src/hooks/sse/useSseWithToken.ts | 38 +-
src/hooks/useLoginForm.ts | 6 -
src/services/api/detection.service.ts | 62 --
src/services/api/index.ts | 2 -
src/services/api/project-summary.service.ts | 52 --
src/services/initializer.service.ts | 20 +-
src/store/app.store.ts | 12 +-
src/utils/routes.ts | 1 -
25 files changed, 129 insertions(+), 2086 deletions(-)
delete mode 100644 src/app/(modules)/dashboard/page.tsx
delete mode 100644 src/components/dashboard/chainage-bar-chart.tsx
delete mode 100644 src/components/dashboard/dashboard-map-content.tsx
delete mode 100644 src/components/dashboard/dashboard-map.tsx
delete mode 100644 src/components/dashboard/dashboard-skeleton.tsx
delete mode 100644 src/components/dashboard/detection-donut-chart.tsx
delete mode 100644 src/components/dashboard/filter-selector.tsx
delete mode 100644 src/components/dashboard/stats-card.tsx
delete mode 100644 src/components/map-modal.tsx
delete mode 100644 src/services/api/detection.service.ts
delete mode 100644 src/services/api/project-summary.service.ts
diff --git a/package-lock.json b/package-lock.json
index 98a5d3b..122dc86 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,7 +26,6 @@
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "leaflet": "^1.9.4",
"libphonenumber-js": "^1.13.7",
"lucide-react": "^0.548.0",
"motion": "^12.38.0",
@@ -36,7 +35,6 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.79.0",
- "react-leaflet": "^5.0.0",
"recharts": "2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^3.3.1",
@@ -45,7 +43,6 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.9",
- "@types/leaflet": "^1.9.21",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -2882,17 +2879,6 @@
"integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==",
"license": "MIT"
},
- "node_modules/@react-leaflet/core": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
- "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
- "license": "Hippocratic-2.1",
- "peerDependencies": {
- "leaflet": "^1.9.0",
- "react": "^19.0.0",
- "react-dom": "^19.0.0"
- }
- },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -3380,13 +3366,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/geojson": {
- "version": "7946.0.16",
- "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
- "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -3401,16 +3380,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/leaflet": {
- "version": "1.9.21",
- "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
- "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/geojson": "*"
- }
- },
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
@@ -6851,12 +6820,6 @@
"node": ">=0.10"
}
},
- "node_modules/leaflet": {
- "version": "1.9.4",
- "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
- "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
- "license": "BSD-2-Clause"
- },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -8024,20 +7987,6 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
- "node_modules/react-leaflet": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
- "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
- "license": "Hippocratic-2.1",
- "dependencies": {
- "@react-leaflet/core": "^3.0.0"
- },
- "peerDependencies": {
- "leaflet": "^1.9.0",
- "react": "^19.0.0",
- "react-dom": "^19.0.0"
- }
- },
"node_modules/react-remove-scroll": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
diff --git a/package.json b/package.json
index ae27b91..3e3f04d 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,6 @@
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "leaflet": "^1.9.4",
"libphonenumber-js": "^1.13.7",
"lucide-react": "^0.548.0",
"motion": "^12.38.0",
@@ -40,7 +39,6 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.79.0",
- "react-leaflet": "^5.0.0",
"recharts": "2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^3.3.1",
@@ -49,7 +47,6 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.9",
- "@types/leaflet": "^1.9.21",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
diff --git a/src/app/(modules)/dashboard/page.tsx b/src/app/(modules)/dashboard/page.tsx
deleted file mode 100644
index 04db731..0000000
--- a/src/app/(modules)/dashboard/page.tsx
+++ /dev/null
@@ -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([]);
- const [selectedProjectId, setSelectedProjectId] = useState(
- null,
- );
- const [projectSummary, setProjectSummary] = useState(
- null,
- );
- const [stats, setStats] = useState({
- 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(
- 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;
- }
- setProjectSummary(null);
-
- const loadProjectSummary = async () => {
- try {
- setIsLoading(true);
-
- const summary =
- await projectSummaryService.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);
- }, [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 */}
-
-
- {/* Header */}
-
-
-
-
-
-
-
-
-
-
-
-
- Filter Analysis
-
-
- Refine your view by project, package, or segment
-
-
-
-
-
-
-
-
-
-
- {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 +
- stats.totalDrainIssue +
- stats.totalDefectiveCulvert}
-
-
-
-
-
-
-
-
-
- Severity by Segment
-
-
- Detections grouped by road segment
-
-
-
-
-
-
-
- Analysis based on latest processed sequence
-
-
-
-
-
-
- {/* Map - Full Width Below Charts */}
-
-
-
-
-
- >
- )}
-
-
- >
- );
-}
diff --git a/src/app/(modules)/page.tsx b/src/app/(modules)/page.tsx
index f3ade7f..70538cc 100644
--- a/src/app/(modules)/page.tsx
+++ b/src/app/(modules)/page.tsx
@@ -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 ;
}
diff --git a/src/app/(modules)/ticket/hooks/useTenantTicketTableEvents.ts b/src/app/(modules)/ticket/hooks/useTenantTicketTableEvents.ts
index a527b29..ed7189b 100644
--- a/src/app/(modules)/ticket/hooks/useTenantTicketTableEvents.ts
+++ b/src/app/(modules)/ticket/hooks/useTenantTicketTableEvents.ts
@@ -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,
});
}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index f2bf662..ce2b0bd 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -1,7 +1,6 @@
import Link from 'next/link';
import { Button } from '@/components/ui/button';
-import { ROUTES } from '@/utils/routes';
export default function NotFound() {
return (
@@ -13,7 +12,7 @@ export default function NotFound() {
The page you are looking for does not exist or may have been moved.
diff --git a/src/components/dashboard/chainage-bar-chart.tsx b/src/components/dashboard/chainage-bar-chart.tsx
deleted file mode 100644
index 5064716..0000000
--- a/src/components/dashboard/chainage-bar-chart.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts';
-import { Loader2 } from 'lucide-react';
-
-import {
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
- type ChartConfig,
-} from '@/components/ui/chart';
-import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
-
-interface ChainageData {
- name: string;
- defected_sign_board: number;
- pothole: number;
- road_crack: number;
- damaged_road_marking: number;
- drain_issue: number;
- defective_culvert: number;
- total: number;
-}
-
-interface ChainageBarChartProps {
- data: ChainageData[];
- isLoading?: boolean;
-}
-
-const chartConfig = {
- pothole: {
- label: DETECTION_TYPES.pothole.label + 's',
- color: DETECTION_TYPES.pothole.color,
- },
- defected_sign_board: {
- label: DETECTION_TYPES.defected_sign_board.label + 's',
- color: DETECTION_TYPES.defected_sign_board.color,
- },
- road_crack: {
- label: DETECTION_TYPES.road_crack.label + 's',
- color: DETECTION_TYPES.road_crack.color,
- },
- damaged_road_marking: {
- label: DETECTION_TYPES.damaged_road_marking.label + 's',
- color: DETECTION_TYPES.damaged_road_marking.color,
- },
- drain_issue: {
- label: DETECTION_TYPES.drain_issue.label + 's',
- color: DETECTION_TYPES.drain_issue.color,
- },
- defective_culvert: {
- label: DETECTION_TYPES.defective_culvert.label + 's',
- color: DETECTION_TYPES.defective_culvert.color,
- },
-} satisfies ChartConfig;
-
-export function ChainageBarChart({
- data,
- isLoading = false,
-}: ChainageBarChartProps) {
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (data.length === 0) {
- return (
-
-
No segment data available
-
- Process videos to see detections by segment
-
-
- );
- }
-
- // Filter out good signboards for a more "damage-focused" standard chart as per multiple bar example
- const chartData = data.map((item) => ({
- name: item.name,
- pothole: item.pothole,
- defected_sign_board: item.defected_sign_board,
- road_crack: item.road_crack,
- damaged_road_marking: item.damaged_road_marking,
- drain_issue: item.drain_issue,
- defective_culvert: item.defective_culvert,
- }));
-
- return (
-
-
-
-
-
- value.length > 8 ? `${value.slice(0, 8)}...` : value
- }
- fontSize={12}
- />
-
- }
- />
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/dashboard/dashboard-map-content.tsx b/src/components/dashboard/dashboard-map-content.tsx
deleted file mode 100644
index a40b852..0000000
--- a/src/components/dashboard/dashboard-map-content.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-'use client';
-
-import { useEffect } from 'react';
-import {
- MapContainer,
- TileLayer,
- CircleMarker,
- Popup,
- useMap,
-} from 'react-leaflet';
-import { LatLngBounds, LatLng } from 'leaflet';
-import 'leaflet/dist/leaflet.css';
-import { Detection } from '@/types';
-import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
-
-interface DashboardMapContentProps {
- detections: Detection[];
-}
-
-// Component to auto-fit map bounds to show all markers
-function FitBounds({ bounds }: { bounds: LatLngBounds }) {
- const map = useMap();
-
- useEffect(() => {
- if (bounds.isValid()) {
- map.fitBounds(bounds, { padding: [50, 50] });
- }
- }, [bounds, map]);
-
- return null;
-}
-
-export default function DashboardMapContent({
- detections,
-}: DashboardMapContentProps) {
- if (detections.length === 0) {
- return (
-
-
No detections to display
-
- );
- }
-
- // Calculate bounds to fit all markers
- const firstDetection = detections[0];
- const bounds = new LatLngBounds(
- new LatLng(firstDetection.latitude!, firstDetection.longitude!),
- new LatLng(firstDetection.latitude!, firstDetection.longitude!),
- );
-
- detections.forEach((d) => {
- if (d.latitude && d.longitude) {
- bounds.extend(new LatLng(d.latitude, d.longitude));
- }
- });
-
- // Center point
- const center: [number, number] = [
- (bounds.getNorth() + bounds.getSouth()) / 2,
- (bounds.getEast() + bounds.getWest()) / 2,
- ];
-
- // Get marker color based on detection type
- const getMarkerColor = (typeId: string) => {
- const config = DETECTION_TYPES[typeId.toLowerCase()];
- if (config) {
- return {
- fill: config.color,
- stroke: '#ffffff', // White stroke for better visibility on the map
- };
- }
- return { fill: '#64748b', stroke: '#475569' }; // Default Slate
- };
-
- // Get display name for detection type
- const getTypeName = (typeId: string) => {
- const config = DETECTION_TYPES[typeId.toLowerCase()];
- return config ? config.label : typeId.replace(/_/g, ' ');
- };
-
- return (
-
-
-
- {/* Detection markers */}
- {detections.map((detection, idx) => {
- const colors = getMarkerColor(detection.type);
- const typeName = getTypeName(detection.type);
-
- return (
-
-
-
-
- {typeName}
-
-
-
-
- Class
-
- {detection.class.replace(/_/g, ' ')}
-
-
-
-
- Confidence
-
-
- {(detection.confidence * 100).toFixed(1)}%
-
-
-
-
-
-
- Location Details
-
-
-
-
- Latitude
-
-
- {detection.latitude!.toFixed(6)}
-
-
-
-
- Longitude
-
-
- {detection.longitude!.toFixed(6)}
-
-
-
-
-
-
-
- );
- })}
-
- {/* Auto-fit bounds */}
-
-
- );
-}
diff --git a/src/components/dashboard/dashboard-map.tsx b/src/components/dashboard/dashboard-map.tsx
deleted file mode 100644
index 3b98dd5..0000000
--- a/src/components/dashboard/dashboard-map.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-import dynamic from 'next/dynamic';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Loader2, Milestone, Maximize2, Minimize2 } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { Detection } from '@/types';
-import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
-
-// Dynamically import the map to avoid SSR issues with Leaflet
-const DashboardMapContent = dynamic(() => import('./dashboard-map-content'), {
- ssr: false,
- loading: () => (
-
-
-
- ),
-});
-
-interface DashboardMapProps {
- className?: string;
- selectedProjectId?: string | null;
- selectedPackageId?: string | null;
- selectedChainageId?: string | null;
- projectSummary?: any;
-}
-
-export function DashboardMap({
- className,
- selectedProjectId,
- selectedPackageId,
- selectedChainageId,
- projectSummary,
-}: DashboardMapProps) {
- const [detections, setDetections] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const [isMaximized, setIsMaximized] = useState(false);
- const [selectedTypes, setSelectedTypes] = useState([]);
-
- useEffect(() => {
- if (!projectSummary) {
- setDetections([]);
- setIsLoading(false);
- return;
- }
-
- try {
- setIsLoading(true);
- setError(null);
-
- const filteredDetections: Detection[] = [];
-
- 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 as any).chainages?.[
- selectedChainageId
- ],
- }
- : (pkg as any).chainages || {};
-
- for (const [chnName, chn] of Object.entries(chainagesToProcess)) {
- if (!chn) continue;
- const chainageDetections = ((chn as any).detections || []).filter(
- (d: any) =>
- (d.type || d.class || '').toLowerCase() !== 'good_sign_board',
- );
- filteredDetections.push(...chainageDetections);
- }
- }
-
- setDetections(filteredDetections);
- } catch (err) {
- console.error('Failed to extract detections:', err);
- setError('Failed to load detection data');
- } finally {
- setIsLoading(false);
- }
- }, [projectSummary, selectedPackageId, selectedChainageId]);
-
- // Handle detection type toggle
- const toggleType = (typeId: string) => {
- setSelectedTypes((prev) => {
- if (prev.includes(typeId)) {
- const next = prev.filter((id) => id !== typeId);
- return next.length === 0 ? [] : next;
- } else {
- return [...prev, typeId];
- }
- });
- };
-
- // Filter detections with valid GPS coordinates and selected types
- const validDetections = detections.filter((d) => {
- const hasGps = d.latitude && d.longitude;
- if (!hasGps) return false;
-
- if (selectedTypes.length === 0) return true;
- const typeId = (d.type || d.class || '').toLowerCase();
- return selectedTypes.includes(typeId);
- });
-
- return (
-
-
- {/* Top Right Actions */}
-
-
-
-
-
-
- {/* Bottom Left Legend */}
-
-
-
- {Object.values(DETECTION_TYPES).map((type) => {
- const typeId = type.id.toLowerCase();
- const count = detections.filter(
- (d) =>
- (d.type || d.class || '').toLowerCase() === typeId &&
- d.latitude &&
- d.longitude,
- ).length;
-
- if (
- count === 0 &&
- (type.id.includes('culvert') || type.id === 'drain_issue')
- )
- return null;
- if (type.id === 'good_sign_board') return null;
-
- const isSelected =
- selectedTypes.length === 0 || selectedTypes.includes(typeId);
-
- return (
-
- );
- })}
-
-
-
-
-
- {isLoading ? (
-
-
-
- ) : error ? (
-
- ) : validDetections.length === 0 ? (
-
-
-
- No detections with GPS coordinates found
-
-
- Process some videos to see detections on the map
-
-
-
- ) : (
-
- )}
-
- {/* Bottom Left Label Overlay */}
-
-
-
- );
-}
diff --git a/src/components/dashboard/dashboard-skeleton.tsx b/src/components/dashboard/dashboard-skeleton.tsx
deleted file mode 100644
index 94e355d..0000000
--- a/src/components/dashboard/dashboard-skeleton.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-'use client';
-
-import { Skeleton } from '@/components/ui/skeleton';
-import { Card, CardContent, CardHeader } from '@/components/ui/card';
-
-export function DashboardSkeleton() {
- return (
-
- {/* Stats Cards Skeleton */}
-
- {[...Array(6)].map((_, i) => (
-
-
-
-
-
-
- ))}
-
-
- {/* Charts Row Skeleton */}
-
- {/* Donut Chart Skeleton */}
-
-
-
-
-
-
-
-
-
-
- {/* Bar Chart Skeleton */}
-
-
-
-
-
-
- {[...Array(8)].map((_, i) => {
- const heights = [
- '60%',
- '40%',
- '75%',
- '50%',
- '65%',
- '35%',
- '80%',
- '45%',
- ];
- return (
-
-
-
-
- );
- })}
-
-
-
-
- {/* Map Skeleton */}
-
-
-
-
-
- {[...Array(3)].map((_, i) => (
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/dashboard/detection-donut-chart.tsx b/src/components/dashboard/detection-donut-chart.tsx
deleted file mode 100644
index a4871df..0000000
--- a/src/components/dashboard/detection-donut-chart.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import { Loader2 } from 'lucide-react';
-import { Label, Pie, PieChart } from 'recharts';
-
-import {
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
- type ChartConfig,
-} from '@/components/ui/chart';
-import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
-
-interface DetectionDonutChartProps {
- defectedSignboard: number;
- pothole: number;
- roadCrack: number;
- damagedRoadMarking: number;
- // goodSignboard: number;
- drainIssue: number;
- defectiveCulvert: number;
- isLoading?: boolean;
-}
-
-const chartConfig = {
- pothole: {
- label: DETECTION_TYPES.pothole.label + 's',
- color: DETECTION_TYPES.pothole.color,
- },
- defectedSignboard: {
- label: DETECTION_TYPES.defected_sign_board.label + 's',
- color: DETECTION_TYPES.defected_sign_board.color,
- },
- roadCrack: {
- label: DETECTION_TYPES.road_crack.label + 's',
- color: DETECTION_TYPES.road_crack.color,
- },
- damagedRoadMarking: {
- label: DETECTION_TYPES.damaged_road_marking.label + 's',
- color: DETECTION_TYPES.damaged_road_marking.color,
- },
- drainIssue: {
- label: DETECTION_TYPES.drain_issue.label + 's',
- color: DETECTION_TYPES.drain_issue.color,
- },
- defectiveCulvert: {
- label: DETECTION_TYPES.defective_culvert.label + 's',
- color: DETECTION_TYPES.defective_culvert.color,
- },
-} satisfies ChartConfig;
-
-export function DetectionDonutChart({
- defectedSignboard,
- pothole,
- roadCrack,
- damagedRoadMarking,
- // goodSignboard,
- drainIssue,
- defectiveCulvert,
- isLoading = false,
-}: DetectionDonutChartProps) {
- const chartData = React.useMemo(
- () =>
- [
- { type: 'pothole', count: pothole, fill: 'var(--color-pothole)' },
- {
- type: 'defectedSignboard',
- count: defectedSignboard,
- fill: 'var(--color-defectedSignboard)',
- },
- { type: 'roadCrack', count: roadCrack, fill: 'var(--color-roadCrack)' },
- {
- type: 'damagedRoadMarking',
- count: damagedRoadMarking,
- fill: 'var(--color-damagedRoadMarking)',
- },
- /* {
- type: 'goodSignboard',
- count: goodSignboard,
- fill: 'var(--color-goodSignboard)',
- }, */
- {
- type: 'drainIssue',
- count: drainIssue,
- fill: 'var(--color-drainIssue)',
- },
- {
- type: 'defectiveCulvert',
- count: defectiveCulvert,
- fill: 'var(--color-defectiveCulvert)',
- },
- ].filter((item) => item.count > 0),
- [
- pothole,
- defectedSignboard,
- roadCrack,
- damagedRoadMarking,
- // goodSignboard,
- drainIssue,
- defectiveCulvert,
- ],
- );
-
- const totalDetections = React.useMemo(() => {
- return chartData.reduce((acc, curr) => acc + curr.count, 0);
- }, [chartData]);
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (totalDetections === 0) {
- return (
-
-
No detections found
-
Process videos to see data
-
- );
- }
-
- return (
-
-
- }
- />
-
-
-
-
- );
-}
diff --git a/src/components/dashboard/filter-selector.tsx b/src/components/dashboard/filter-selector.tsx
deleted file mode 100644
index cba0a94..0000000
--- a/src/components/dashboard/filter-selector.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-'use client';
-
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
-import { FolderOpen, Package, Milestone } from 'lucide-react';
-import { Project } from '@/types';
-
-interface FilterSelectorProps {
- projects: Project[];
- selectedProjectId: string | null;
- selectedPackageId: string | null;
- selectedChainageId: string | null;
- onProjectChange: (projectId: string) => void;
- onPackageChange: (packageId: string) => void;
- onChainageChange: (chainageId: string) => void;
- packages: Array<{ id: string; name: string }>;
- chainages: Array<{ id: string; name: string }>;
- isLoading?: boolean;
-}
-
-export function FilterSelector({
- projects,
- selectedProjectId,
- selectedPackageId,
- selectedChainageId,
- onProjectChange,
- onPackageChange,
- onChainageChange,
- packages,
- chainages,
- isLoading = false,
-}: FilterSelectorProps) {
- return (
-
- {/* Project Dropdown */}
-
-
-
-
-
- {/* Package Dropdown */}
- {selectedProjectId && (
-
-
-
-
- )}
-
- {/* Chainage Dropdown */}
- {selectedPackageId && selectedPackageId !== 'all' && (
-
-
-
-
- )}
-
- );
-}
diff --git a/src/components/dashboard/stats-card.tsx b/src/components/dashboard/stats-card.tsx
deleted file mode 100644
index 46dc904..0000000
--- a/src/components/dashboard/stats-card.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-
-import { Card, CardContent } from '@/components/ui/card';
-import { LucideIcon } from 'lucide-react';
-import { motion } from 'motion/react';
-
-interface StatsCardProps {
- title: string;
- subtitle?: string;
- value: number | string;
- icon: LucideIcon;
- isLoading?: boolean;
-}
-
-export function StatsCard({
- title,
- subtitle,
- value,
- icon: Icon,
- isLoading = false,
-}: StatsCardProps) {
- return (
-
-
-
-
-
- {title}
-
-
- {isLoading ? (
-
- ) : (
- <>
-
{value}
- {subtitle && (
-
- {subtitle}
-
- )}
- >
- )}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/map-modal.tsx b/src/components/map-modal.tsx
deleted file mode 100644
index 3ac76d3..0000000
--- a/src/components/map-modal.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-'use client';
-
-import { useEffect } from 'react';
-import {
- MapContainer,
- TileLayer,
- Polyline,
- CircleMarker,
- Popup,
- useMap,
-} from 'react-leaflet';
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog';
-import { LatLngBounds, LatLng } from 'leaflet';
-import 'leaflet/dist/leaflet.css';
-import {
- getDetectionModeConfig,
- DETECTION_TYPES,
-} from '@/constants/detectionModeConfig';
-import { DetectionType } from '@/types';
-
-type Detection = {
- id: number;
- type: string;
- class: string;
- confidence: number;
- latitude: number;
- longitude: number;
- frame_number: number;
-};
-
-type MapModalProps = {
- open: boolean;
- onClose: () => void;
- detections: Detection[];
- detectionType: DetectionType | string;
-};
-
-// Component to auto-fit map bounds to show all markers
-function FitBounds({ bounds }: { bounds: LatLngBounds }) {
- const map = useMap();
-
- useEffect(() => {
- if (!map || !bounds.isValid()) return;
-
- const timer = setTimeout(() => {
- map.invalidateSize();
- map.fitBounds(bounds, {
- padding: [50, 50],
- maxZoom: 16,
- animate: true,
- });
- }, 200);
-
- return () => clearTimeout(timer);
- }, [bounds, map]);
-
- return null;
-}
-
-export default function MapModal({
- open,
- onClose,
- detections,
- detectionType,
-}: MapModalProps) {
- const validDetections = detections.filter((d) => d.latitude && d.longitude);
-
- if (validDetections.length === 0) {
- return (
-
- );
- }
-
- const routeCoordinates: [number, number][] = validDetections.map((d) => [
- d.latitude,
- d.longitude,
- ]);
- const bounds = new LatLngBounds(
- new LatLng(routeCoordinates[0][0], routeCoordinates[0][1]),
- new LatLng(routeCoordinates[0][0], routeCoordinates[0][1]),
- );
- routeCoordinates.forEach((coord) =>
- bounds.extend(new LatLng(coord[0], coord[1])),
- );
- const center: [number, number] = [
- (bounds.getNorth() + bounds.getSouth()) / 2,
- (bounds.getEast() + bounds.getWest()) / 2,
- ];
-
- const modeConfig = getDetectionModeConfig(detectionType);
-
- return (
-
- );
-}
diff --git a/src/config/menu.config.ts b/src/config/menu.config.ts
index 913f731..a79ede0 100644
--- a/src/config/menu.config.ts
+++ b/src/config/menu.config.ts
@@ -1,5 +1,4 @@
import {
- LayoutDashboard,
Layers,
Milestone,
Package,
@@ -26,11 +25,6 @@ export type MenuItem = {
};
export const menuItems: MenuItem[] = [
- {
- title: 'Dashboard',
- path: ROUTES.DASHBOARD,
- icon: LayoutDashboard,
- },
{
title: 'Upload',
path: ROUTES.UPLOAD,
@@ -120,3 +114,36 @@ export function filterMenuItems(
})
.filter((item): item is MenuItem => item !== null);
}
+
+function findFirstMenuPath(items: MenuItem[]): string | undefined {
+ for (const item of items) {
+ if (item.path) return item.path;
+
+ const childPath = item.children
+ ? findFirstMenuPath(item.children)
+ : undefined;
+ if (childPath) return childPath;
+ }
+}
+
+export function getFirstAccessibleMenuPath(
+ hasPermission: (permission?: string) => boolean,
+ fallback = ROUTES.ACCESS,
+) {
+ return findFirstMenuPath(filterMenuItems(menuItems, hasPermission)) ?? fallback;
+}
+
+export function getFirstAccessiblePathForPermissions(
+ grantedPermissions: string[],
+ fallback = ROUTES.ACCESS,
+) {
+ const normalizedPermissions = new Set(
+ grantedPermissions.map((permission) => permission.toLowerCase()),
+ );
+
+ return getFirstAccessibleMenuPath(
+ (permission) =>
+ !permission || normalizedPermissions.has(permission.toLowerCase()),
+ fallback,
+ );
+}
diff --git a/src/constants/apiRoutes.ts b/src/constants/apiRoutes.ts
index d096cfa..c9c6d14 100644
--- a/src/constants/apiRoutes.ts
+++ b/src/constants/apiRoutes.ts
@@ -47,9 +47,6 @@ export const API_ROUTES = {
BASE: 'biz/api/v1/chainages',
DETAIL: (id: string) => `biz/api/v1/chainages/${id}`,
},
- DASHBOARD: {
- OVERVIEW: 'biz/api/v1/dashboard/overview',
- },
VIDEOS: {
UPLOAD: '/biz/api/v1/upload',
MY_UPLOADS: '/biz/api/v1/videos/me',
diff --git a/src/guards/GuestGuard.tsx b/src/guards/GuestGuard.tsx
index ac71695..d625f5a 100644
--- a/src/guards/GuestGuard.tsx
+++ b/src/guards/GuestGuard.tsx
@@ -3,7 +3,6 @@
import { AppLoader } from '@/components/AppLoader';
import { useAppStore } from '@/store/app.store';
import { useAuthStore } from '@/store/auth.store';
-import { ROUTES } from '@/utils/routes';
import { useRouter } from 'next/navigation';
import type { ReactNode } from 'react';
import { useEffect } from 'react';
@@ -13,12 +12,13 @@ export function GuestGuard({ children }: { children: ReactNode }) {
const accessToken = useAuthStore((state) => state.accessToken);
const isInitialized = useAppStore((state) => state.isInitialized);
const user = useAppStore((state) => state.user);
+ const defaultRoute = useAppStore((state) => state.defaultRoute);
useEffect(() => {
if (isInitialized && accessToken && user) {
- router.replace(ROUTES.DASHBOARD);
+ router.replace(defaultRoute ?? '/access');
}
- }, [accessToken, isInitialized, router, user]);
+ }, [accessToken, defaultRoute, isInitialized, router, user]);
if (!isInitialized) {
return (
@@ -27,7 +27,7 @@ export function GuestGuard({ children }: { children: ReactNode }) {
}
if (isInitialized && accessToken && user)
- return ;
+ return ;
return children;
}
diff --git a/src/hooks/sse/useSseWithToken.ts b/src/hooks/sse/useSseWithToken.ts
index b75fe4a..f3e1195 100644
--- a/src/hooks/sse/useSseWithToken.ts
+++ b/src/hooks/sse/useSseWithToken.ts
@@ -9,6 +9,8 @@ import {
} from '@/services/sse';
const DEFAULT_RETRY_DELAY_MS = 2500;
+const DEFAULT_MAX_RETRY_DELAY_MS = 30000;
+const DEFAULT_MAX_RECONNECT_ATTEMPTS = 6;
const EMPTY_RECONNECT_EVENTS: ReadonlyArray = [];
interface SseToken {
@@ -21,7 +23,10 @@ interface UseSseWithTokenOptions> {
getPath: (token: string) => string;
events: SseEventMap;
retryDelayMs?: number;
+ maxRetryDelayMs?: number;
+ maxReconnectAttempts?: number;
onConnectionError?: () => void;
+ onConnectionOpen?: () => void;
reconnectOnEvents?: ReadonlyArray;
shouldStop?: (
eventName: keyof TEvents,
@@ -35,12 +40,16 @@ export function useSseWithToken>({
getPath,
events,
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
+ maxRetryDelayMs = DEFAULT_MAX_RETRY_DELAY_MS,
+ maxReconnectAttempts = DEFAULT_MAX_RECONNECT_ATTEMPTS,
onConnectionError,
+ onConnectionOpen,
reconnectOnEvents = EMPTY_RECONNECT_EVENTS as ReadonlyArray,
shouldStop,
}: UseSseWithTokenOptions) {
const connectionRef = useRef(null);
const retryTimerRef = useRef | null>(null);
+ const reconnectAttemptRef = useRef(0);
const stoppedRef = useRef(false);
const clearRetryTimer = useCallback(() => {
@@ -56,6 +65,7 @@ export function useSseWithToken>({
const stop = useCallback(() => {
stoppedRef.current = true;
+ reconnectAttemptRef.current = 0;
clearRetryTimer();
closeConnection();
}, [clearRetryTimer, closeConnection]);
@@ -64,16 +74,33 @@ export function useSseWithToken>({
if (!enabled) return;
stoppedRef.current = false;
+ reconnectAttemptRef.current = 0;
+
+ const getReconnectDelay = () =>
+ Math.min(
+ retryDelayMs * 2 ** Math.max(reconnectAttemptRef.current - 1, 0),
+ maxRetryDelayMs,
+ );
const scheduleReconnect = (notify = true) => {
if (stoppedRef.current || retryTimerRef.current) return;
closeConnection();
- if (notify) onConnectionError?.();
+
+ if (notify) {
+ onConnectionError?.();
+ }
+
+ if (reconnectAttemptRef.current >= maxReconnectAttempts) {
+ stoppedRef.current = true;
+ return;
+ }
+
+ reconnectAttemptRef.current += 1;
retryTimerRef.current = setTimeout(() => {
retryTimerRef.current = null;
connect();
- }, retryDelayMs);
+ }, getReconnectDelay());
};
const connect = () => {
@@ -116,6 +143,10 @@ export function useSseWithToken>({
wrappedEvents,
);
+ connection.source.onopen = () => {
+ reconnectAttemptRef.current = 0;
+ onConnectionOpen?.();
+ };
connection.source.onerror = () => scheduleReconnect();
connectionRef.current = connection;
})
@@ -131,7 +162,10 @@ export function useSseWithToken>({
events,
getPath,
getToken,
+ maxReconnectAttempts,
+ maxRetryDelayMs,
onConnectionError,
+ onConnectionOpen,
reconnectOnEvents,
retryDelayMs,
shouldStop,
diff --git a/src/hooks/useLoginForm.ts b/src/hooks/useLoginForm.ts
index 65416b3..4eb7878 100644
--- a/src/hooks/useLoginForm.ts
+++ b/src/hooks/useLoginForm.ts
@@ -1,15 +1,11 @@
import { authService } from '@/services/api/auth.service';
-import { initializeAuthenticatedApp } from '@/services/initializer.service';
import { useAppStore } from '@/store/app.store';
import { useAuthStore } from '@/store/auth.store';
import type { LoginPayload } from '@/types';
-import { ROUTES } from '@/utils/routes';
-import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
export const useLoginForm = () => {
- const router = useRouter();
const setAccessToken = useAuthStore((state) => state.setAccessToken);
const setLoading = useAppStore((state) => state.setLoading);
const {
@@ -39,10 +35,8 @@ export const useLoginForm = () => {
setAccessToken(accessToken);
setLoading();
- await initializeAuthenticatedApp();
toast.success('Login successful');
- router.replace(ROUTES.DASHBOARD);
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Unable to sign in');
}
diff --git a/src/services/api/detection.service.ts b/src/services/api/detection.service.ts
deleted file mode 100644
index 9dcac33..0000000
--- a/src/services/api/detection.service.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import axiosClient from '../axios/axios';
-import { API_ROUTES } from '@/constants/apiRoutes';
-import { Detection } from '@/types';
-import { projectService } from './project.service';
-
-/**
- * Detection Service
- */
-export const detectionService = {
- /**
- * Fetch all detections from completed videos
- * Uses the summary endpoint to get detections for each project
- */
- getAllDetections: async (): Promise => {
- try {
- // First get all projects
- const projectsResponse = await projectService.getProjects();
- const projects = projectsResponse.items;
-
- // Then fetch detections for each project
- const allDetections: Detection[] = [];
-
- for (const project of projects) {
- try {
- const response = await axiosClient.get<{
- packages: {
- [key: string]: {
- chainages: {
- [key: string]: {
- detections: Detection[];
- };
- };
- };
- };
- }>(API_ROUTES.DASHBOARD.OVERVIEW, {
- params: { project_id: project.id },
- });
-
- const summary = response.data;
-
- // Extract detections from the nested structure
- for (const pkg of Object.values(summary.packages || {})) {
- for (const loc of Object.values(pkg.chainages || {})) {
- allDetections.push(...(loc.detections || []));
- }
- }
- } catch (e) {
- // Skip projects that fail to load
- console.warn(
- `Failed to load detections for project ${project.id}:`,
- e,
- );
- }
- }
-
- return allDetections;
- } catch (e) {
- console.error('Failed to fetch all detections:', e);
- return [];
- }
- },
-};
diff --git a/src/services/api/index.ts b/src/services/api/index.ts
index 6343bef..ba92fdc 100644
--- a/src/services/api/index.ts
+++ b/src/services/api/index.ts
@@ -1,10 +1,8 @@
export * from './project.service';
-export * from './project-summary.service';
export * from './package.service';
export * from './auth.service';
export { chainageService } from './chainage.service';
export * from './video.service';
-export * from './detection.service';
export * from './permission.service';
export * from './role.service';
export * from './user.service';
diff --git a/src/services/api/project-summary.service.ts b/src/services/api/project-summary.service.ts
deleted file mode 100644
index df6efab..0000000
--- a/src/services/api/project-summary.service.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { API_ROUTES } from '@/constants/apiRoutes';
-
-import axiosClient from '../axios/axios';
-
-export const projectSummaryService = {
- getProjectSummary: async (projectId: string): Promise => {
- const response = await axiosClient.get(API_ROUTES.DASHBOARD.OVERVIEW, {
- params: { project_id: projectId },
- });
- return response.data;
- },
-
- getProjectSummaryByVideo: async (
- projectId: string,
- videoId: string,
- ): Promise => {
- const response = await axiosClient.get(API_ROUTES.DASHBOARD.OVERVIEW, {
- params: { project_id: projectId, video_id: videoId },
- });
- return response.data;
- },
-};
-
-export const projectDataService = {
- extractDetections(
- projectSummary: any,
- selectedPackageId?: string | null,
- selectedChainageId?: string | null,
- ): any[] {
- if (!projectSummary) return [];
-
- const detections: any[] = [];
- const packagesToProcess =
- selectedPackageId && selectedPackageId !== 'all'
- ? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
- : projectSummary.packages || {};
-
- for (const pkg of Object.values(packagesToProcess)) {
- const chainagesToProcess =
- selectedChainageId && selectedChainageId !== 'all'
- ? { [selectedChainageId]: (pkg as any).chainages[selectedChainageId] }
- : (pkg as any).chainages || {};
-
- for (const chainage of Object.values(chainagesToProcess)) {
- if (!chainage) continue;
- detections.push(...((chainage as any).detections || []));
- }
- }
-
- return detections;
- },
-};
diff --git a/src/services/initializer.service.ts b/src/services/initializer.service.ts
index 86635a1..ebee95c 100644
--- a/src/services/initializer.service.ts
+++ b/src/services/initializer.service.ts
@@ -1,16 +1,34 @@
+import { getFirstAccessiblePathForPermissions } from '@/config/menu.config';
import { authService } from '@/services/api/auth.service';
import { useAppStore } from '@/store/app.store';
import type { UserProfile } from '@/types';
+let authenticatedAppInitialization: Promise | null = null;
+
export async function initializeAuthenticatedApp() {
+ if (authenticatedAppInitialization) {
+ return authenticatedAppInitialization;
+ }
+
+ authenticatedAppInitialization = loadAuthenticatedApp().finally(() => {
+ authenticatedAppInitialization = null;
+ });
+
+ return authenticatedAppInitialization;
+}
+
+async function loadAuthenticatedApp() {
const [{ user, tenant }, permissionsResponse] = await Promise.all([
authService.me(),
authService.permissions(),
]);
+ const permissions = permissionsResponse.granted_permissions ?? [];
+
useAppStore.getState().setUserContext({
user: user as UserProfile,
tenant,
- permissions: permissionsResponse.granted_permissions ?? [],
+ permissions,
+ defaultRoute: getFirstAccessiblePathForPermissions(permissions),
});
}
diff --git a/src/store/app.store.ts b/src/store/app.store.ts
index 5d7e5d7..c4e78ba 100644
--- a/src/store/app.store.ts
+++ b/src/store/app.store.ts
@@ -7,6 +7,7 @@ interface AppState {
user: UserProfile | null;
tenant: TenantInfo | null;
permissions: string[];
+ defaultRoute: string | null;
loadStatus: AppLoadStatus;
isInitialized: boolean;
setLoading: () => void;
@@ -16,6 +17,7 @@ interface AppState {
user: UserProfile;
tenant?: TenantInfo | null;
permissions?: string[];
+ defaultRoute?: string | null;
}) => void;
clear: () => void;
}
@@ -24,16 +26,23 @@ export const useAppStore = create()((set) => ({
user: null,
tenant: null,
permissions: [],
+ defaultRoute: null,
loadStatus: 'idle',
isInitialized: false,
setLoading: () => set({ loadStatus: 'loading', isInitialized: false }),
setLoaded: () => set({ loadStatus: 'loaded', isInitialized: true }),
setLoadError: () => set({ loadStatus: 'error', isInitialized: true }),
- setUserContext: ({ user, tenant = null, permissions = [] }) =>
+ setUserContext: ({
+ user,
+ tenant = null,
+ permissions = [],
+ defaultRoute = null,
+ }) =>
set({
user,
tenant,
permissions,
+ defaultRoute,
loadStatus: 'loaded',
isInitialized: true,
}),
@@ -42,6 +51,7 @@ export const useAppStore = create()((set) => ({
user: null,
tenant: null,
permissions: [],
+ defaultRoute: null,
loadStatus: 'idle',
isInitialized: false,
}),
diff --git a/src/utils/routes.ts b/src/utils/routes.ts
index 6ced5db..dc25b2e 100644
--- a/src/utils/routes.ts
+++ b/src/utils/routes.ts
@@ -3,7 +3,6 @@ export const ROUTES = {
FORGOT_PASSWORD: '/forgot-password',
RESET_PASSWORD: '/reset-password',
SET_PASSWORD: '/set-password',
- DASHBOARD: '/dashboard',
PROJECT: '/project',
PACKAGE: '/package',
CHAINAGE: '/segment',