-
- {[
- { label: 'Project', value: session.projectName, primary: true },
- { label: 'Package', value: session.packageName },
- { label: 'Chainage', value: session.chainageName },
- ].map((item, idx) => (
-
-
- {item.label}
-
-
- {item.value}
- {item.label === 'Chainage' && session.chainageDirection && (
-
- {session.chainageDirection === 'UP' ? (
-
- ) : (
-
- )}
- {session.chainageDirection}
-
- )}
-
-
- ))}
+ return (
+
+
+ {/* Header */}
+
+
+
+ {/* Module 1: Project Selection */}
+
+
+
+ 1. Project Details
+
+ Select the target project, package, and chainage segment.
+
+
+
+
+
+
+
+
+ {/* Module 2: Upload Data */}
+
+
+
+
+ 2. Upload Video Data
+
+
+ Provide the video file and optional GPS telemetry for processing.
+
+
+
+
+ {/* Video File Input */}
+
+
+
+ {
+ setFile(e.target.files?.[0] || null);
+ setError(null);
+ }}
+ disabled={uploading || !isSessionComplete}
+ className="h-12 bg-muted/30 border-dashed border-2 group-hover:border-primary/50 transition-colors cursor-pointer file:mr-4 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary file:text-primary-foreground file:font-semibold file:text-xs"
+ />
+
+
+
+ {/* JSON File Input */}
+
+
+ {
+ setJsonFile(e.target.files?.[0] || null);
+ setError(null);
+ }}
+ disabled={uploading || !isSessionComplete}
+ className="h-11 bg-muted/20 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary/10 file:text-primary file:font-medium file:text-xs"
+ />
+
+
+ {/* Select Method */}
+
+
+
+
+
+
+
+ {/* Error Display */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Primary Action Button */}
+
-
-
- )}
+
+
+
+
- {/* Upload Card */}
-
-
- Upload Video
-
- Select video file, GPS JSON file, and method for analysis
-
-
-
-
- {/* Video File Input - Full Width */}
-
-
- {
- setFile(e.target.files?.[0] || null);
- setError(null);
- }}
- disabled={uploading}
- className="h-11 bg-muted/20 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary/10 file:text-primary file:font-medium file:text-xs hover:file:bg-primary/20"
- />
-
-
- {/* JSON File and Method - Both in one line */}
-
- {/* JSON File Input */}
-
-
- {
- setJsonFile(e.target.files?.[0] || null);
- setError(null);
- }}
- disabled={uploading}
- className="h-11 bg-muted/20 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary/10 file:text-primary file:font-medium file:text-xs hover:file:bg-primary/20"
- />
-
-
- {/* Select Method */}
-
-
-
-
-
-
-
- {/* Error Display */}
- {error && (
-
- {error}
-
- )}
-
- {/* Upload Button */}
-
-
-
-
-
+
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx
index 295c57d..478adc5 100644
--- a/src/components/app-sidebar.tsx
+++ b/src/components/app-sidebar.tsx
@@ -33,7 +33,7 @@ const data = {
},
{
title: 'New Analysis',
- url: ROUTES.NEW_ANALYSIS,
+ url: ROUTES.UPLOAD,
icon: Plus,
},
{
diff --git a/src/components/project-selection-section.tsx b/src/components/project-selection-section.tsx
index b3ac218..1f52329 100644
--- a/src/components/project-selection-section.tsx
+++ b/src/components/project-selection-section.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
@@ -17,12 +17,23 @@ import { Project, Package as PackageType, Chainage, SessionContext } from '@/typ
import { cn } from '@/lib/utils';
type ProjectSelectionSectionProps = {
- onSelectionComplete: (session: SessionContext) => void;
+ onSelectionComplete?: (session: SessionContext) => void;
+ onSelectionChange?: (session: SessionContext | null) => void;
+ asStep?: boolean;
+ hideButton?: boolean;
};
-export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectionSectionProps) {
+let globalProjectsCache: Project[] | null = null;
+let projectsPromise: Promise
| null = null;
+
+export function ProjectSelectionSection({
+ onSelectionComplete,
+ onSelectionChange,
+ asStep = false,
+ hideButton = false
+}: ProjectSelectionSectionProps) {
// Data states
- const [projects, setProjects] = useState([]);
+ const [projects, setProjects] = useState(globalProjectsCache || []);
const [packages, setPackages] = useState([]);
const [chainages, setChainages] = useState([]);
@@ -32,7 +43,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
const [selectedChainage, setSelectedChainage] = useState(null);
// Loading states
- const [loadingProjects, setLoadingProjects] = useState(true);
+ const [loadingProjects, setLoadingProjects] = useState(!globalProjectsCache);
const [loadingPackages, setLoadingPackages] = useState(false);
const [loadingChainages, setLoadingChainages] = useState(false);
@@ -41,20 +52,48 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
// Load projects on mount
useEffect(() => {
+ let isMounted = true;
+
const loadProjects = async () => {
+ // Use cache if we already have it
+ if (globalProjectsCache) {
+ if (isMounted) {
+ setProjects(globalProjectsCache);
+ setLoadingProjects(false);
+ }
+ return;
+ }
+
+ // If a fetch is already in flight, reuse that promise
+ if (!projectsPromise) {
+ projectsPromise = projectService.getProjects().then(data => {
+ globalProjectsCache = data.items;
+ return data.items;
+ }).catch(err => {
+ projectsPromise = null; // Reset on error to allow retry
+ throw err;
+ });
+ }
+
try {
setLoadingProjects(true);
- setError(null);
- const data = await projectService.getProjects();
- setProjects(data.items);
+ const data = await projectsPromise;
+ if (isMounted) {
+ setProjects(data);
+ }
} catch (err) {
- console.error('Failed to load projects:', err);
- setError('Failed to load projects. Please check if the backend is running.');
+ if (isMounted) {
+ setError('Failed to load projects. Please check connection.');
+ }
} finally {
- setLoadingProjects(false);
+ if (isMounted) {
+ setLoadingProjects(false);
+ }
}
};
+
loadProjects();
+ return () => { isMounted = false; };
}, []);
// Load packages when project changes
@@ -65,6 +104,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
return;
}
+ let isMounted = true;
const loadPackages = async () => {
try {
setLoadingPackages(true);
@@ -73,15 +113,22 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
setSelectedChainage(null);
setChainages([]);
const data = await packageService.getPackagesByProject(selectedProject.id);
- setPackages(data.items);
+ if (isMounted) {
+ setPackages(data.items);
+ }
} catch (err) {
- console.error('Failed to load packages:', err);
- setError('Failed to load packages for the selected project.');
+ if (isMounted) {
+ console.error('Failed to load packages:', err);
+ setError('Failed to load packages for the selected project.');
+ }
} finally {
- setLoadingPackages(false);
+ if (isMounted) {
+ setLoadingPackages(false);
+ }
}
};
loadPackages();
+ return () => { isMounted = false; };
}, [selectedProject]);
// Load chainages when package changes
@@ -92,21 +139,29 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
return;
}
+ let isMounted = true;
const loadChainages = async () => {
try {
setLoadingChainages(true);
setError(null);
setSelectedChainage(null);
const data = await chainageService.getChainagesByPackage(selectedPackage.id);
- setChainages(data.items);
+ if (isMounted) {
+ setChainages(data.items);
+ }
} catch (err) {
- console.error('Failed to load chainages:', err);
- setError('Failed to load chainages for the selected package.');
+ if (isMounted) {
+ console.error('Failed to load chainages:', err);
+ setError('Failed to load chainages for the selected package.');
+ }
} finally {
- setLoadingChainages(false);
+ if (isMounted) {
+ setLoadingChainages(false);
+ }
}
};
loadChainages();
+ return () => { isMounted = false; };
}, [selectedPackage]);
const handleProjectChange = (projectId: string) => {
@@ -124,8 +179,38 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
setSelectedChainage(chainage);
};
+ // Cache the last reported state to prevent infinite loops
+ const lastReportedIdRef = useRef(null);
+
+ // Handle change reporting
+ useEffect(() => {
+ if (onSelectionChange) {
+ const currentIds = selectedProject && selectedPackage && selectedChainage
+ ? `${selectedProject.id}-${selectedPackage.id}-${selectedChainage.id}`
+ : null;
+
+ if (currentIds !== lastReportedIdRef.current) {
+ lastReportedIdRef.current = currentIds;
+
+ if (selectedProject && selectedPackage && selectedChainage) {
+ onSelectionChange({
+ projectId: selectedProject.id,
+ projectName: selectedProject.name,
+ packageId: selectedPackage.id,
+ packageName: selectedPackage.name,
+ chainageId: selectedChainage.id,
+ chainageName: selectedChainage.segment_name,
+ chainageDirection: selectedChainage.direction,
+ });
+ } else {
+ onSelectionChange(null);
+ }
+ }
+ }
+ }, [selectedProject, selectedPackage, selectedChainage, onSelectionChange]);
+
const handleProceed = () => {
- if (selectedProject && selectedPackage && selectedChainage) {
+ if (selectedProject && selectedPackage && selectedChainage && onSelectionComplete) {
onSelectionComplete({
projectId: selectedProject.id,
projectName: selectedProject.name,
@@ -148,6 +233,167 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
return 'pending';
};
+ const content = (
+ <>
+ {/* Error Display */}
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {/* Project Dropdown */}
+
+
+
+
+
+ {/* Package Dropdown */}
+
+
+
+
+
+ {/* Chainage Dropdown */}
+
+
+
+
+
+
+ {/* Selected Summary - Only if not asStep or if specifically desired */}
+ {!asStep && isComplete && (
+
+
+
Selection:
+
{selectedProject?.name}
+
/
+
{selectedPackage?.name}
+
/
+
+ {selectedChainage?.segment_name}
+
+ {selectedChainage?.direction === 'UP' ? (
+
+ ) : (
+
+ )}
+ {selectedChainage?.direction}
+
+
+
+
+ )}
+
+ {/* Proceed Button */}
+ {!hideButton && (
+
+ )}
+ >
+ );
+
+ if (asStep) {
+ return content;
+ }
+
return (
@@ -203,157 +449,8 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
-