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 ? ( -
-

{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 */} -
-
-
- -
- - Project - -
- -
- - {/* Package Dropdown */} - {selectedProjectId && ( -
-
-
- -
- - Package - -
- -
- )} - - {/* Chainage Dropdown */} - {selectedPackageId && selectedPackageId !== 'all' && ( -
-
-
- -
- - Segment - -
- -
- )} -
- ); -} 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 ( - - - - Detection Map - -
- No GPS data available for detections. -
-
-
- ); - } - - 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 ( - - - - - {modeConfig.label} Map - -

- {validDetections.length} points of interest identified with GPS data -

-
- -
- - - - - - {validDetections.map((detection, idx) => { - const type = (detection.type || '').toLowerCase(); - const typeConfig = DETECTION_TYPES[type]; - const color = typeConfig - ? { fill: typeConfig.color, stroke: typeConfig.color } - : { fill: '#64748b', stroke: '#475569' }; - - return ( - - -
-
- {(detection.type || '').replace(/_/g, ' ')}{' '} - - #{detection.id} - -
- -
-
- - Frame - - - {detection.frame_number} - -
-
- - Confidence - - - {(detection.confidence * 100).toFixed(1)}% - -
-
- -
-
- Location Details -
-
-
- - Latitude - - - {detection.latitude.toFixed(6)} - -
-
- - Longitude - - - {detection.longitude.toFixed(6)} - -
-
-
-
-
-
- ); - })} - -
-
-
-
- ); -} 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',