525 lines
16 KiB
TypeScript
525 lines
16 KiB
TypeScript
'use client';
|
|
|
|
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';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Loader2, Check, ArrowUp, ArrowDown } from 'lucide-react';
|
|
import {
|
|
projectService,
|
|
packageService,
|
|
chainageService,
|
|
} from '@/services/api';
|
|
import {
|
|
Project,
|
|
Package as PackageType,
|
|
Chainage,
|
|
SessionContext,
|
|
} from '@/types';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
type ProjectSelectionSectionProps = {
|
|
onSelectionComplete?: (session: SessionContext) => void;
|
|
onSelectionChange?: (session: SessionContext | null) => void;
|
|
asStep?: boolean;
|
|
hideButton?: boolean;
|
|
};
|
|
|
|
let globalProjectsCache: Project[] | null = null;
|
|
let projectsPromise: Promise<Project[]> | null = null;
|
|
|
|
export function ProjectSelectionSection({
|
|
onSelectionComplete,
|
|
onSelectionChange,
|
|
asStep = false,
|
|
hideButton = false,
|
|
}: ProjectSelectionSectionProps) {
|
|
// Data states
|
|
const [projects, setProjects] = useState<Project[]>(
|
|
globalProjectsCache || [],
|
|
);
|
|
const [packages, setPackages] = useState<PackageType[]>([]);
|
|
const [chainages, setChainages] = useState<Chainage[]>([]);
|
|
|
|
// Selection states
|
|
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
|
const [selectedPackage, setSelectedPackage] = useState<PackageType | null>(
|
|
null,
|
|
);
|
|
const [selectedChainage, setSelectedChainage] = useState<Chainage | null>(
|
|
null,
|
|
);
|
|
|
|
// Loading states
|
|
const [loadingProjects, setLoadingProjects] = useState(!globalProjectsCache);
|
|
const [loadingPackages, setLoadingPackages] = useState(false);
|
|
const [loadingChainages, setLoadingChainages] = useState(false);
|
|
|
|
// Error state
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// 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);
|
|
const data = await projectsPromise;
|
|
if (isMounted) {
|
|
setProjects(data);
|
|
}
|
|
} catch (err) {
|
|
if (isMounted) {
|
|
setError('Failed to load projects. Please check connection.');
|
|
}
|
|
} finally {
|
|
if (isMounted) {
|
|
setLoadingProjects(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
loadProjects();
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, []);
|
|
|
|
// Load packages when project changes
|
|
useEffect(() => {
|
|
if (!selectedProject) {
|
|
setPackages([]);
|
|
setSelectedPackage(null);
|
|
return;
|
|
}
|
|
|
|
let isMounted = true;
|
|
const loadPackages = async () => {
|
|
try {
|
|
setLoadingPackages(true);
|
|
setError(null);
|
|
setSelectedPackage(null);
|
|
setSelectedChainage(null);
|
|
setChainages([]);
|
|
const data = await packageService.getPackagesByProject(
|
|
selectedProject.id,
|
|
);
|
|
if (isMounted) {
|
|
setPackages(data.items);
|
|
}
|
|
} catch (err) {
|
|
if (isMounted) {
|
|
console.error('Failed to load packages:', err);
|
|
setError('Failed to load packages for the selected project.');
|
|
}
|
|
} finally {
|
|
if (isMounted) {
|
|
setLoadingPackages(false);
|
|
}
|
|
}
|
|
};
|
|
loadPackages();
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [selectedProject]);
|
|
|
|
// Load chainages when package changes
|
|
useEffect(() => {
|
|
if (!selectedPackage) {
|
|
setChainages([]);
|
|
setSelectedChainage(null);
|
|
return;
|
|
}
|
|
|
|
let isMounted = true;
|
|
const loadChainages = async () => {
|
|
try {
|
|
setLoadingChainages(true);
|
|
setError(null);
|
|
setSelectedChainage(null);
|
|
const data = await chainageService.getChainagesByPackage(
|
|
selectedPackage.id,
|
|
);
|
|
if (isMounted) {
|
|
setChainages(data.items);
|
|
}
|
|
} catch (err) {
|
|
if (isMounted) {
|
|
console.error('Failed to load chainages:', err);
|
|
setError('Failed to load segments for the selected package.');
|
|
}
|
|
} finally {
|
|
if (isMounted) {
|
|
setLoadingChainages(false);
|
|
}
|
|
}
|
|
};
|
|
loadChainages();
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [selectedPackage]);
|
|
|
|
const handleProjectChange = (projectId: string) => {
|
|
const project = projects.find((p) => p.id === projectId) || null;
|
|
setSelectedProject(project);
|
|
};
|
|
|
|
const handlePackageChange = (packageId: string) => {
|
|
const pkg = packages.find((p) => p.id === packageId) || null;
|
|
setSelectedPackage(pkg);
|
|
};
|
|
|
|
const handleChainageChange = (chainageId: string) => {
|
|
const chainage = chainages.find((chn) => chn.id === chainageId) || null;
|
|
setSelectedChainage(chainage);
|
|
};
|
|
|
|
// Cache the last reported state to prevent infinite loops
|
|
const lastReportedIdRef = useRef<string | null>(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 &&
|
|
onSelectionComplete
|
|
) {
|
|
onSelectionComplete({
|
|
projectId: selectedProject.id,
|
|
projectName: selectedProject.name,
|
|
packageId: selectedPackage.id,
|
|
packageName: selectedPackage.name,
|
|
chainageId: selectedChainage.id,
|
|
chainageName: selectedChainage.segment_name,
|
|
chainageDirection: selectedChainage.direction,
|
|
});
|
|
}
|
|
};
|
|
|
|
const isComplete = selectedProject && selectedPackage && selectedChainage;
|
|
|
|
// Step status helpers
|
|
const getStepStatus = (step: number) => {
|
|
if (step === 1) return selectedProject ? 'completed' : 'active';
|
|
if (step === 2)
|
|
return selectedPackage
|
|
? 'completed'
|
|
: selectedProject
|
|
? 'active'
|
|
: 'pending';
|
|
if (step === 3)
|
|
return selectedChainage
|
|
? 'completed'
|
|
: selectedPackage
|
|
? 'active'
|
|
: 'pending';
|
|
return 'pending';
|
|
};
|
|
|
|
const content = (
|
|
<>
|
|
{/* Error Display */}
|
|
{error && (
|
|
<div className="p-4 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm mb-6">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
{/* Project Dropdown */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="project" className="text-sm font-semibold">
|
|
Project
|
|
</Label>
|
|
<Select
|
|
value={selectedProject?.id || ''}
|
|
onValueChange={handleProjectChange}
|
|
disabled={loadingProjects}
|
|
>
|
|
<SelectTrigger id="project" className="h-11">
|
|
{loadingProjects ? (
|
|
<div className="flex items-center gap-2">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
<span>Loading...</span>
|
|
</div>
|
|
) : (
|
|
<SelectValue placeholder="Select project" />
|
|
)}
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{projects.map((project) => (
|
|
<SelectItem key={project.id} value={project.id}>
|
|
{project.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Package Dropdown */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="package" className="text-sm font-semibold">
|
|
Package
|
|
</Label>
|
|
<Select
|
|
value={selectedPackage?.id || ''}
|
|
onValueChange={handlePackageChange}
|
|
disabled={!selectedProject || loadingPackages}
|
|
>
|
|
<SelectTrigger id="package" className="h-11">
|
|
{loadingPackages ? (
|
|
<div className="flex items-center gap-2">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
<span>Loading...</span>
|
|
</div>
|
|
) : (
|
|
<SelectValue
|
|
placeholder={
|
|
selectedProject ? 'Select package' : 'Select project first'
|
|
}
|
|
/>
|
|
)}
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{packages.map((pkg) => (
|
|
<SelectItem key={pkg.id} value={pkg.id}>
|
|
{pkg.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Chainage Dropdown */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="chainage" className="text-sm font-semibold">
|
|
Segment
|
|
</Label>
|
|
<Select
|
|
value={selectedChainage?.id || ''}
|
|
onValueChange={handleChainageChange}
|
|
disabled={!selectedPackage || loadingChainages}
|
|
>
|
|
<SelectTrigger id="chainage" className="h-11">
|
|
{loadingChainages ? (
|
|
<div className="flex items-center gap-2">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
<span>Loading...</span>
|
|
</div>
|
|
) : (
|
|
<SelectValue
|
|
placeholder={
|
|
selectedPackage ? 'Select segment' : 'Select package first'
|
|
}
|
|
/>
|
|
)}
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{chainages.map((chn) => (
|
|
<SelectItem key={chn.id} value={chn.id}>
|
|
<div className="flex flex-col gap-0.5 py-0.5">
|
|
<span className="font-semibold text-sm">
|
|
{chn.segment_name}
|
|
</span>
|
|
<div className="flex items-center gap-2 text-[10px] text-muted-foreground font-medium">
|
|
<span>
|
|
{chn.chainage_start_km}-{chn.chainage_end_km} km
|
|
</span>
|
|
<span className="h-2.5 w-px bg-border" />
|
|
<span className="flex items-center gap-1 uppercase">
|
|
{chn.direction === 'UP' ? (
|
|
<ArrowUp className="h-2.5 w-2.5" />
|
|
) : (
|
|
<ArrowDown className="h-2.5 w-2.5" />
|
|
)}
|
|
{chn.direction}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Selected Summary - Only if not asStep or if specifically desired */}
|
|
{!asStep && isComplete && (
|
|
<div className="px-4 py-3 rounded-md bg-secondary/50 border text-sm mb-8">
|
|
<div className="flex items-center gap-2 flex-wrap text-muted-foreground">
|
|
<span className="font-semibold text-foreground">Selection:</span>
|
|
<span className="text-foreground">{selectedProject?.name}</span>
|
|
<span>/</span>
|
|
<span className="text-foreground">{selectedPackage?.name}</span>
|
|
<span>/</span>
|
|
<span className="text-foreground flex items-center gap-1.5">
|
|
{selectedChainage?.segment_name}
|
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-muted text-[10px] font-bold uppercase border">
|
|
{selectedChainage?.direction === 'UP' ? (
|
|
<ArrowUp className="h-2.5 w-2.5" />
|
|
) : (
|
|
<ArrowDown className="h-2.5 w-2.5" />
|
|
)}
|
|
{selectedChainage?.direction}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Proceed Button */}
|
|
{!hideButton && (
|
|
<Button
|
|
onClick={handleProceed}
|
|
disabled={!isComplete}
|
|
className="w-full h-14 text-base font-bold uppercase tracking-wider"
|
|
size="lg"
|
|
>
|
|
{isComplete ? 'Proceed to Upload' : 'Select all fields to proceed'}
|
|
</Button>
|
|
)}
|
|
</>
|
|
);
|
|
|
|
if (asStep) {
|
|
return content;
|
|
}
|
|
|
|
return (
|
|
<Card className="overflow-hidden">
|
|
<CardHeader className="pb-6">
|
|
<div className="flex flex-col gap-6">
|
|
<div>
|
|
<CardTitle className="text-2xl font-bold">
|
|
Select Project Segment
|
|
</CardTitle>
|
|
<CardDescription className="mt-2 text-base">
|
|
Select Project, Package & Segment to begin intelligent road
|
|
analysis.
|
|
</CardDescription>
|
|
</div>
|
|
|
|
{/* Step Progress Indicator */}
|
|
<div className="flex items-center justify-center pt-2">
|
|
{[1, 2, 3].map((step, index) => {
|
|
const status = getStepStatus(step);
|
|
const labels = ['Project', 'Package', 'Segment'];
|
|
return (
|
|
<div key={step} className="flex items-center">
|
|
<div className="flex flex-col items-center">
|
|
<div
|
|
className={cn(
|
|
'w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold transition-all',
|
|
status === 'completed'
|
|
? 'bg-primary text-primary-foreground'
|
|
: status === 'active'
|
|
? 'bg-primary text-primary-foreground ring-4 ring-primary/10'
|
|
: 'bg-muted text-muted-foreground',
|
|
)}
|
|
>
|
|
{status === 'completed' ? (
|
|
<Check className="h-5 w-5" />
|
|
) : (
|
|
step
|
|
)}
|
|
</div>
|
|
<span
|
|
className={cn(
|
|
'text-xs mt-2 font-medium',
|
|
status === 'pending'
|
|
? 'text-muted-foreground/60'
|
|
: 'text-foreground',
|
|
)}
|
|
>
|
|
{labels[index]}
|
|
</span>
|
|
</div>
|
|
{index < 2 && (
|
|
<div
|
|
className={cn(
|
|
'w-16 h-0.5 mx-2 mb-6 rounded-full',
|
|
getStepStatus(step + 1) !== 'pending'
|
|
? 'bg-primary'
|
|
: 'bg-border',
|
|
)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="pt-0">{content}</CardContent>
|
|
</Card>
|
|
);
|
|
}
|