feat(api): restructure api with axios and chainage related changes

This commit is contained in:
2026-03-18 18:42:40 +05:30
parent 7f8854ed78
commit 598098d7e8
31 changed files with 933 additions and 931 deletions

15
package-lock.json generated
View File

@@ -4937,6 +4937,21 @@
"d3-time": "^3.0.0", "d3-time": "^3.0.0",
"d3-timer": "^3.0.1" "d3-timer": "^3.0.1"
} }
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz",
"integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
} }
} }
} }

View File

@@ -7,33 +7,25 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Loader2, CheckCircle2, MapPin, Navigation, Milestone } from "lucide-react" import { Loader2, CheckCircle2, MapPin, Navigation, Milestone, ArrowUpCircle, ArrowDownCircle } from "lucide-react"
import { DataTable } from "@/components/data-table" import { DataTable } from "@/components/data-table"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by" import { PoweredBy } from "@/components/powered-by"
import { import { projectService, packageService, chainageService } from "@/services/api"
fetchProjects,
fetchPackagesByProject,
fetchAllLocations,
fetchAllPackages,
createLocation,
updateLocation,
deleteLocation
} from "@/lib/api"
import { import {
Project, Project,
Package as PackageType, Package as PackageType,
Location, Chainage,
LocationCreate, ChainageCreate,
LocationUpdate ChainageUpdate
} from "@/types" } from "@/types"
import { ColumnDef } from "@tanstack/react-table" import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner" import { toast } from "sonner"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
export default function LocationPage() { export default function ChainagePage() {
const [locations, setLocations] = useState<Location[]>([]) const [chainages, setChainages] = useState<Chainage[]>([])
const [totalItems, setTotalItems] = useState(0) const [totalItems, setTotalItems] = useState(0)
const [projects, setProjects] = useState<Project[]>([]) const [projects, setProjects] = useState<Project[]>([])
const [packages, setPackages] = useState<PackageType[]>([]) const [packages, setPackages] = useState<PackageType[]>([])
@@ -51,7 +43,7 @@ export default function LocationPage() {
// Editing state // Editing state
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [currentLocation, setCurrentLocation] = useState<Location | null>(null) const [currentChainage, setCurrentChainage] = useState<Chainage | null>(null)
// Form fields // Form fields
const [selectedProjectId, setSelectedProjectId] = useState("") const [selectedProjectId, setSelectedProjectId] = useState("")
@@ -63,17 +55,18 @@ export default function LocationPage() {
const [startLng, setStartLng] = useState("") const [startLng, setStartLng] = useState("")
const [endLat, setEndLat] = useState("") const [endLat, setEndLat] = useState("")
const [endLng, setEndLng] = useState("") const [endLng, setEndLng] = useState("")
const [direction, setDirection] = useState<'UP' | 'DOWN'>("UP")
// Load locations and projects // Load chainages and projects
const loadLocations = async (currentSkip = skip, currentLimit = limit) => { const loadChainages = async (currentSkip = skip, currentLimit = limit) => {
try { try {
setIsLoading(true) setIsLoading(true)
setError(null) setError(null)
const data = await fetchAllLocations({ skip: currentSkip, limit: currentLimit }) const data = await chainageService.getChainages({ skip: currentSkip, limit: currentLimit })
setLocations(data.items) setChainages(data.items)
setTotalItems(data.totalItems) setTotalItems(data.totalItems)
} catch (err) { } catch (err) {
setError("Failed to load locations. Please check if the backend is running.") setError("Failed to load chainages. Please check if the backend is running.")
} finally { } finally {
// Add a small delay for animation stability // Add a small delay for animation stability
setTimeout(() => { setTimeout(() => {
@@ -85,7 +78,7 @@ export default function LocationPage() {
const loadProjects = async () => { const loadProjects = async () => {
try { try {
setLoadingProjects(true) setLoadingProjects(true)
const data = await fetchProjects({ skip: 0, limit: 1000 }) const data = await projectService.getProjects({ skip: 0, limit: 1000 })
setProjects(data.items) setProjects(data.items)
} catch (err) { } catch (err) {
setError("Failed to load projects.") setError("Failed to load projects.")
@@ -96,7 +89,7 @@ export default function LocationPage() {
const loadAllPackages = async () => { const loadAllPackages = async () => {
try { try {
const data = await fetchAllPackages({ skip: 0, limit: 1000 }) const data = await packageService.getPackages({ skip: 0, limit: 1000 })
setAllPackages(data.items) setAllPackages(data.items)
} catch (err) { } catch (err) {
console.error("Failed to load all packages") console.error("Failed to load all packages")
@@ -104,7 +97,7 @@ export default function LocationPage() {
} }
useEffect(() => { useEffect(() => {
loadLocations(skip, limit) loadChainages(skip, limit)
loadProjects() loadProjects()
loadAllPackages() loadAllPackages()
}, [skip, limit]) }, [skip, limit])
@@ -121,7 +114,7 @@ export default function LocationPage() {
try { try {
setLoadingPackages(true) setLoadingPackages(true)
if (!isEditing) setSelectedPackageId("") if (!isEditing) setSelectedPackageId("")
const data = await fetchPackagesByProject(selectedProjectId, { skip: 0, limit: 1000 }) const data = await packageService.getPackagesByProject(selectedProjectId, { skip: 0, limit: 1000 })
setPackages(data.items) setPackages(data.items)
} catch (err) { } catch (err) {
setError("Failed to load packages for the selected project.") setError("Failed to load packages for the selected project.")
@@ -142,9 +135,10 @@ export default function LocationPage() {
setStartLng("") setStartLng("")
setEndLat("") setEndLat("")
setEndLng("") setEndLng("")
setDirection("UP")
setError(null) setError(null)
setIsEditing(false) setIsEditing(false)
setCurrentLocation(null) setCurrentChainage(null)
} }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@@ -158,7 +152,11 @@ export default function LocationPage() {
return return
} }
if (!startLat || !startLng || !endLat || !endLng) { if (!startLat || !startLng || !endLat || !endLng) {
setError("All GPS coordinates are required for locations") setError("All GPS coordinates are required for chainages")
return
}
if (!chainageStartKm || !chainageEndKm) {
setError("Chainage start and end values are required")
return return
} }
@@ -166,45 +164,47 @@ export default function LocationPage() {
setError(null) setError(null)
try { try {
if (isEditing && currentLocation) { if (isEditing && currentChainage) {
const data: LocationUpdate = { const data: ChainageUpdate = {
segment_name: segmentName.trim(), segment_name: segmentName.trim(),
chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : null, chainage_start_km: parseFloat(chainageStartKm),
chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : null, chainage_end_km: parseFloat(chainageEndKm),
start_lat: parseFloat(startLat), start_lat: parseFloat(startLat),
start_lng: parseFloat(startLng), start_lng: parseFloat(startLng),
end_lat: parseFloat(endLat), end_lat: parseFloat(endLat),
end_lng: parseFloat(endLng), end_lng: parseFloat(endLng),
direction: direction as 'UP' | 'DOWN',
} }
await updateLocation(currentLocation.id, data) await chainageService.updateChainage(currentChainage.id, data)
toast.success("Location Updated", { toast.success("Chainage Updated", {
description: `${segmentName} has been updated successfully at ${new Date().toLocaleTimeString()}`, description: `${segmentName} has been updated successfully at ${new Date().toLocaleTimeString()}`,
}) })
} else { } else {
const data: LocationCreate = { const data: ChainageCreate = {
package_id: selectedPackageId, package_id: selectedPackageId,
segment_name: segmentName.trim(), segment_name: segmentName.trim(),
chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : null, chainage_start_km: parseFloat(chainageStartKm),
chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : null, chainage_end_km: parseFloat(chainageEndKm),
start_lat: parseFloat(startLat), start_lat: parseFloat(startLat),
start_lng: parseFloat(startLng), start_lng: parseFloat(startLng),
end_lat: parseFloat(endLat), end_lat: parseFloat(endLat),
end_lng: parseFloat(endLng), end_lng: parseFloat(endLng),
direction: direction,
} }
await createLocation(data) await chainageService.createChainage(data)
toast.success("Location Created", { toast.success("Chainage Created", {
description: `${segmentName} has been established successfully at ${new Date().toLocaleTimeString()}`, description: `${segmentName} has been established successfully at ${new Date().toLocaleTimeString()}`,
}) })
} }
// Refresh locations list // Refresh chainages list
await loadLocations() await loadChainages()
// Close modal and reset form immediately // Close modal and reset form immediately
setIsModalOpen(false) setIsModalOpen(false)
resetForm() resetForm()
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} location` const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} chainage`
setError(message) setError(message)
toast.error("Operation Failed", { toast.error("Operation Failed", {
description: message, description: message,
@@ -214,41 +214,42 @@ export default function LocationPage() {
} }
} }
const handleEdit = (location: Location) => { const handleEdit = (chainage: Chainage) => {
setIsEditing(true) setIsEditing(true)
setCurrentLocation(location) setCurrentChainage(chainage)
// Find project for this package // Find project for this package
const pkg = allPackages.find(p => p.id === location.package_id) const pkg = allPackages.find(p => p.id === chainage.package_id)
if (pkg) { if (pkg) {
setSelectedProjectId(pkg.project_id) setSelectedProjectId(pkg.project_id)
setSelectedPackageId(location.package_id) setSelectedPackageId(chainage.package_id)
} }
setSegmentName(location.segment_name || "") setSegmentName(chainage.segment_name || "")
setChainageStartKm(location.chainage_start_km?.toString() || "") setChainageStartKm(chainage.chainage_start_km?.toString() || "")
setChainageEndKm(location.chainage_end_km?.toString() || "") setChainageEndKm(chainage.chainage_end_km?.toString() || "")
setStartLat(location.start_lat.toString()) setStartLat(chainage.start_lat.toString())
setStartLng(location.start_lng.toString()) setStartLng(chainage.start_lng.toString())
setEndLat(location.end_lat.toString()) setEndLat(chainage.end_lat.toString())
setEndLng(location.end_lng.toString()) setEndLng(chainage.end_lng.toString())
setDirection(chainage.direction)
setIsModalOpen(true) setIsModalOpen(true)
} }
const handleDelete = async (location: Location) => { const handleDelete = async (chainage: Chainage) => {
if (!confirm(`Are you sure you want to delete location "${location.segment_name}"?`)) return if (!confirm(`Are you sure you want to delete chainage "${chainage.segment_name}"?`)) return
try { try {
setIsLoading(true) setIsLoading(true)
await deleteLocation(location.id) await chainageService.deleteChainage(chainage.id)
toast.success("Location Deleted", { toast.success("Chainage Deleted", {
description: `${location.segment_name} has been removed from the system.`, description: `${chainage.segment_name} has been removed from the system.`,
}) })
await loadLocations() await loadChainages()
} catch (err) { } catch (err) {
setError("Failed to delete location") setError("Failed to delete chainage")
toast.error("Deletion Failed", { toast.error("Deletion Failed", {
description: "The location could not be removed. Please try again.", description: "The chainage could not be removed. Please try again.",
}) })
} finally { } finally {
setIsLoading(false) setIsLoading(false)
@@ -259,14 +260,14 @@ export default function LocationPage() {
return allPackages.find(p => p.id === packageId)?.name || packageId return allPackages.find(p => p.id === packageId)?.name || packageId
} }
const isFormComplete = selectedPackageId && segmentName.trim() && startLat && startLng && endLat && endLng const isFormComplete = selectedPackageId && segmentName.trim() && startLat && startLng && endLat && endLng && chainageStartKm && chainageEndKm
const columns: ColumnDef<Location>[] = [ const columns: ColumnDef<Chainage>[] = [
{ {
accessorKey: "segment_name", accessorKey: "segment_name",
header: "Segment Name", header: "Segment Name",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{row.original.segment_name}</div> <div className="font-semibold">{row.original.segment_name}</div>
) )
}, },
{ {
@@ -278,8 +279,8 @@ export default function LocationPage() {
accessorKey: "project", accessorKey: "project",
header: "Project", header: "Project",
cell: ({ row }) => { cell: ({ row }) => {
const location = row.original const chainage = row.original
const pkg = allPackages.find(p => p.id === location.package_id) const pkg = allPackages.find(p => p.id === chainage.package_id)
const project = projects.find(p => p.id === pkg?.project_id) const project = projects.find(p => p.id === pkg?.project_id)
return project?.name || "—" return project?.name || "—"
} }
@@ -288,13 +289,13 @@ export default function LocationPage() {
id: "project_state", id: "project_state",
header: "Project State", header: "Project State",
cell: ({ row }) => { cell: ({ row }) => {
const location = row.original const chainage = row.original
const pkg = allPackages.find(p => p.id === location.package_id) const pkg = allPackages.find(p => p.id === chainage.package_id)
const project = projects.find(p => p.id === pkg?.project_id) const project = projects.find(p => p.id === pkg?.project_id)
if (!project?.state) return <span className="text-gray-400"></span> if (!project?.state) return <span className=""></span>
const states = project.state.split(',').map(s => s.trim()).filter(Boolean) const states = project.state.split(',').map(s => s.trim()).filter(Boolean)
if (states.length === 0) return <span className="text-gray-400"></span> if (states.length === 0) return <span className=""></span>
const firstState = states[0] const firstState = states[0]
const remainingStates = states.slice(1) const remainingStates = states.slice(1)
@@ -332,17 +333,26 @@ export default function LocationPage() {
accessorKey: "chainage", accessorKey: "chainage",
header: "Chainage (km)", header: "Chainage (km)",
cell: ({ row }) => { cell: ({ row }) => {
const location = row.original const chainage = row.original
if (location.chainage_start_km !== null && location.chainage_end_km !== null) {
return ( return (
<div className="flex flex-row gap-2">
<Badge variant="outline" className="flex items-center gap-1.5 border-amber-500/50 text-amber-500 font-bold whitespace-nowrap"> <Badge variant="outline" className="flex items-center gap-1.5 border-amber-500/50 text-amber-500 font-bold whitespace-nowrap">
<Milestone className="h-3 w-3" /> <Milestone className="h-3 w-3" />
{location.chainage_start_km} - {location.chainage_end_km} {chainage.chainage_start_km} - {chainage.chainage_end_km}
</Badge> </Badge>
<Badge
variant="secondary"
>
{chainage.direction === "UP" ? (
<ArrowUpCircle className="h-3 w-3" />
) : (
<ArrowDownCircle className="h-3 w-3" />
)}
{chainage.direction}
</Badge>
</div>
) )
} }
return "—"
}
}, },
{ {
accessorKey: "start_gps", accessorKey: "start_gps",
@@ -372,15 +382,15 @@ export default function LocationPage() {
{/* Refined Header */} {/* Refined Header */}
<div className="mb-8"> <div className="mb-8">
<PageHeader <PageHeader
title="Location Management" title="Chainage Management"
description="Manage road segment locations" description="Manage road segment chainages"
icon={MapPin} icon={Milestone}
actions={ actions={
<Button <Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }} onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
> >
<MapPin className="mr-2 h-4 w-4" /> <Milestone className="mr-2 h-4 w-4" />
Add New Location Add New Chainage
</Button> </Button>
} }
/> />
@@ -390,8 +400,8 @@ export default function LocationPage() {
{/* Data Table */} {/* Data Table */}
<div> <div>
<DataTable <DataTable
title="Locations" title="Chainages"
data={locations} data={chainages}
columns={columns} columns={columns}
onEdit={handleEdit} onEdit={handleEdit}
onDelete={handleDelete} onDelete={handleDelete}
@@ -420,12 +430,12 @@ export default function LocationPage() {
<DialogHeader className="gap-2"> <DialogHeader className="gap-2">
<DialogTitle className="flex items-center gap-3 text-xl"> <DialogTitle className="flex items-center gap-3 text-xl">
<div className="p-2 rounded-lg bg-primary text-primary-foreground shadow-sm"> <div className="p-2 rounded-lg bg-primary text-primary-foreground shadow-sm">
<MapPin className="h-5 w-5" /> <Milestone className="h-5 w-5" />
</div> </div>
{isEditing ? 'Edit Location Details' : 'Create New Location'} {isEditing ? 'Edit Chainage Details' : 'Create New Chainage'}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-sm"> <DialogDescription className="text-sm">
{isEditing ? 'Update the technical specifications for your road segment location.' : 'Select a project & package, then provide the essential location data.'} {isEditing ? 'Update the technical specifications for your road segment chainage.' : 'Select a project & package, then provide the essential chainage data.'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -494,14 +504,14 @@ export default function LocationPage() {
</div> </div>
)} )}
{/* Step 3: Location Details */} {/* Step 3: Chainage Details */}
<div className={`space-y-4 ${selectedPackageId || isEditing ? 'opacity-100' : 'opacity-40 pointer-events-none'}`}> <div className={`space-y-4 ${selectedPackageId || isEditing ? 'opacity-100' : 'opacity-40 pointer-events-none'}`}>
{!isEditing && ( {!isEditing && (
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className={`w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold uppercase ${selectedPackageId ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}> <div className={`w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold uppercase ${selectedPackageId ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
03 03
</div> </div>
<p className="text-[11px] font-black text-muted-foreground uppercase tracking-widest">Location Information</p> <p className="text-[11px] font-black text-muted-foreground uppercase tracking-widest">Chainage Information</p>
</div> </div>
)} )}
@@ -523,7 +533,7 @@ export default function LocationPage() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="ch-start" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider"> <Label htmlFor="ch-start" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Start (km) Start (km) <span className="text-destructive">*</span>
</Label> </Label>
<Input <Input
id="ch-start" id="ch-start"
@@ -534,11 +544,12 @@ export default function LocationPage() {
onChange={(e) => setChainageStartKm(e.target.value)} onChange={(e) => setChainageStartKm(e.target.value)}
placeholder="0.0" placeholder="0.0"
className="h-11 bg-muted/20 border-border/60 text-sm font-mono" className="h-11 bg-muted/20 border-border/60 text-sm font-mono"
required
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="ch-end" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider"> <Label htmlFor="ch-end" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
End (km) End (km) <span className="text-destructive">*</span>
</Label> </Label>
<Input <Input
id="ch-end" id="ch-end"
@@ -549,11 +560,32 @@ export default function LocationPage() {
onChange={(e) => setChainageEndKm(e.target.value)} onChange={(e) => setChainageEndKm(e.target.value)}
placeholder="1.0" placeholder="1.0"
className="h-11 bg-muted/20 border-border/60 text-sm font-mono" className="h-11 bg-muted/20 border-border/60 text-sm font-mono"
required
/> />
</div> </div>
</div> </div>
</div> </div>
{/* Direction Row */}
<div className="space-y-3">
<Label htmlFor="direction" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Direction <span className="text-destructive">*</span>
</Label>
<Select value={direction} onValueChange={(val: 'UP' | 'DOWN') => setDirection(val)}>
<SelectTrigger className="h-11 bg-muted/20 border-border/60 focus:ring-primary/20">
<SelectValue placeholder="Select Direction" />
</SelectTrigger>
<SelectContent>
<SelectItem value="UP" className="py-2.5">
<span>UP</span>
</SelectItem>
<SelectItem value="DOWN" className="py-2.5">
<span>DOWN</span>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* GPS Coordinates Grid */} {/* GPS Coordinates Grid */}
<div className="grid grid-cols-2 gap-8 pt-4"> <div className="grid grid-cols-2 gap-8 pt-4">
{/* Start Point */} {/* Start Point */}
@@ -631,6 +663,7 @@ export default function LocationPage() {
{/* Submit Button */} {/* Submit Button */}
<div className="flex gap-4 pt-4"> <div className="flex gap-4 pt-4">
<Button <Button
className="flex-1"
type="button" type="button"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
@@ -638,14 +671,13 @@ export default function LocationPage() {
resetForm() resetForm()
}} }}
disabled={isSubmitting} disabled={isSubmitting}
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs border-border/80 hover:bg-muted/50"
> >
Cancel Cancel
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={isSubmitting || !isFormComplete} disabled={isSubmitting || !isFormComplete}
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs shadow-lg shadow-primary/20" className="flex-1"
> >
{isSubmitting ? ( {isSubmitting ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -654,8 +686,8 @@ export default function LocationPage() {
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isEditing ? <CheckCircle2 className="h-4 w-4" /> : <MapPin className="h-4 w-4" />} {isEditing ? <CheckCircle2 className="h-4 w-4" /> : <Milestone className="h-4 w-4" />}
<span>{isEditing ? 'Update Location' : 'Create Location'}</span> <span>{isEditing ? 'Update Chainage' : 'Create Chainage'}</span>
</div> </div>
)} )}
</Button> </Button>

View File

@@ -6,6 +6,7 @@ import {
AlertCircle, AlertCircle,
Activity, Activity,
MapPin as MapPinIcon, MapPin as MapPinIcon,
Milestone,
BarChart3, BarChart3,
AlertTriangle, AlertTriangle,
Zap, Zap,
@@ -17,10 +18,10 @@ import { StatsCard } from "@/components/dashboard/stats-card";
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector"; import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector";
import { FilterSelector } from "@/components/dashboard/filter-selector"; import { FilterSelector } from "@/components/dashboard/filter-selector";
import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart"; import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart";
import { LocationBarChart } from "@/components/dashboard/location-bar-chart"; import { ChainageBarChart } from "@/components/dashboard/chainage-bar-chart"
import { DashboardMap } from "@/components/dashboard/dashboard-map"; import { DashboardMap } from "@/components/dashboard/dashboard-map";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { fetchProjects } from "@/lib/api"; import { projectService } from "@/services/api";
import { Project } from "@/types"; import { Project } from "@/types";
import { import {
Popover, Popover,
@@ -46,9 +47,9 @@ interface ProjectSummary {
[key: string]: { [key: string]: {
package_id: string; package_id: string;
region: string | null; region: string | null;
locations: { chainages: {
[key: string]: { [key: string]: {
location_id: string; chainage_id: string;
chainage: string | null; chainage: string | null;
detection_count: number; detection_count: number;
detections: Array<{ detections: Array<{
@@ -72,7 +73,7 @@ interface DetectionStats {
totalDamagedRoadMarking: number; totalDamagedRoadMarking: number;
totalGoodSignboard: number; totalGoodSignboard: number;
totalRoadDamage: number; totalRoadDamage: number;
locationData: Array<{ chainageData: Array<{
name: string; name: string;
defected_sign_board: number; defected_sign_board: number;
pothole: number; pothole: number;
@@ -92,103 +93,93 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
totalDamagedRoadMarking: 0, totalDamagedRoadMarking: 0,
totalGoodSignboard: 0, totalGoodSignboard: 0,
totalRoadDamage: 0, totalRoadDamage: 0,
locationData: [], chainageData: [],
}; };
} }
let totalDefectedSignboard = 0; let total_defected_sign_board = 0;
let totalPothole = 0; let total_pothole = 0;
let totalRoadCrack = 0; let total_road_crack = 0;
let totalDamagedRoadMarking = 0; let total_damaged_road_marking = 0;
let totalGoodSignboard = 0; let total_good_sign_board = 0;
let totalRoadDamage = 0; let total_road_damage = 0;
const locationData: DetectionStats["locationData"] = []; const chainageData: DetectionStats["chainageData"] = [];
for (const pkg of Object.values(summary.packages || {})) { for (const pkg of Object.values(summary.packages || {})) {
for (const [locName, loc] of Object.entries(pkg.locations || {})) { for (const [chnName, chn] of Object.entries(pkg.chainages || {})) { // Changed 'locName, loc' to 'chnName, chn' and 'pkg.locations' to 'pkg.chainages'
let locDefectedSignboard = 0; let chnDefectedSignboard = 0; // Changed 'locDefectedSignboard' to 'chnDefectedSignboard'
let locPothole = 0; let chnPothole = 0; // Changed 'locPothole' to 'chnPothole'
let locRoadCrack = 0; let chnRoadCrack = 0; // Changed 'locRoadCrack' to 'chnRoadCrack'
let locDamagedRoadMarking = 0; let chnDamagedRoadMarking = 0; // Changed 'locDamagedRoadMarking' to 'chnDamagedRoadMarking'
let locGoodSignboard = 0; let chnGoodSignboard = 0; // Changed 'locGoodSignboard' to 'chnGoodSignboard'
for (const detection of loc.detections || []) { for (const detection of chn.detections || []) { // Changed 'loc.detections' to 'chn.detections'
const type = detection.type.toLowerCase(); const type = detection.type.toLowerCase();
if (type === "defected_sign_board") { if (type === "defected_sign_board") {
locDefectedSignboard++; chnDefectedSignboard++;
totalDefectedSignboard++; total_defected_sign_board++;
} else if (type === "pothole") { } else if (type === "pothole") {
locPothole++; chnPothole++;
totalPothole++; total_pothole++;
} else if (type === "road_crack") { } else if (type === "road_crack") {
locRoadCrack++; chnRoadCrack++;
totalRoadCrack++; total_road_crack++;
} else if (type === "damaged_road_marking") { } else if (type === "damaged_road_marking") {
locDamagedRoadMarking++; chnDamagedRoadMarking++;
totalDamagedRoadMarking++; total_damaged_road_marking++;
} else if (type === "good_sign_board") { } else if (type === "good_sign_board") {
locGoodSignboard++; chnGoodSignboard++;
totalGoodSignboard++; total_good_sign_board++;
} }
} }
const locTotalDamage = const chnTotalDamage = // Changed 'locTotalDamage' to 'chnTotalDamage'
locDefectedSignboard + chnDefectedSignboard +
locPothole + chnPothole +
locRoadCrack + chnRoadCrack +
locDamagedRoadMarking; chnDamagedRoadMarking;
totalRoadDamage +=
locDefectedSignboard > 0 ||
locPothole > 0 ||
locRoadCrack > 0 ||
locDamagedRoadMarking > 0
? 1
: 0; // This logic might need refinement based on "total_road_damage" definition
if ( if (
locDefectedSignboard > 0 || chnDefectedSignboard > 0 ||
locPothole > 0 || chnPothole > 0 ||
locRoadCrack > 0 || chnRoadCrack > 0 ||
locDamagedRoadMarking > 0 || chnDamagedRoadMarking > 0 ||
locGoodSignboard > 0 chnGoodSignboard > 0
) { ) {
const shortName = const shortName =
locName.length > 20 ? locName.substring(0, 20) + "..." : locName; chnName.length > 20 ? chnName.substring(0, 20) + "..." : chnName;
locationData.push({ chainageData.push({ // Changed 'locationData.push' to 'chainageData.push'
name: shortName, name: shortName,
defected_sign_board: locDefectedSignboard, defected_sign_board: chnDefectedSignboard,
pothole: locPothole, pothole: chnPothole,
road_crack: locRoadCrack, road_crack: chnRoadCrack,
damaged_road_marking: locDamagedRoadMarking, damaged_road_marking: chnDamagedRoadMarking,
good_sign_board: locGoodSignboard, good_sign_board: chnGoodSignboard,
total: total:
locDefectedSignboard + chnDefectedSignboard +
locPothole + chnPothole +
locRoadCrack + chnRoadCrack +
locDamagedRoadMarking + chnDamagedRoadMarking +
locGoodSignboard, chnGoodSignboard,
}); });
} }
} }
} }
// Recalculate totalRoadDamage based on backends unique count total_road_damage =
// But since we are aggregating from locations, we just sum them up or use a simpler metric total_defected_sign_board +
// The user's JSON shows "total_road_damage": 9 which is sum of 2+6+0+1 total_pothole +
totalRoadDamage = total_road_crack +
totalDefectedSignboard + total_damaged_road_marking;
totalPothole +
totalRoadCrack +
totalDamagedRoadMarking;
return { return {
totalDefectedSignboard, totalDefectedSignboard: total_defected_sign_board,
totalPothole, totalPothole: total_pothole,
totalRoadCrack, totalRoadCrack: total_road_crack,
totalDamagedRoadMarking, totalDamagedRoadMarking: total_damaged_road_marking,
totalGoodSignboard, totalGoodSignboard: total_good_sign_board,
totalRoadDamage, totalRoadDamage: total_road_damage,
locationData, chainageData,
}; };
} }
@@ -208,7 +199,7 @@ export default function DashboardPage() {
totalDamagedRoadMarking: 0, totalDamagedRoadMarking: 0,
totalGoodSignboard: 0, totalGoodSignboard: 0,
totalRoadDamage: 0, totalRoadDamage: 0,
locationData: [], chainageData: [],
}); });
// Load projects on mount // Load projects on mount
@@ -216,7 +207,7 @@ export default function DashboardPage() {
const loadProjects = async () => { const loadProjects = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const projectsData = await fetchProjects(); const projectsData = await projectService.getProjects();
setProjects(projectsData.items); setProjects(projectsData.items);
if (projectsData.items.length > 0) { if (projectsData.items.length > 0) {
@@ -247,21 +238,21 @@ export default function DashboardPage() {
})) }))
: []; : [];
// Extract locations from selected package // Extract chainages from selected package
const [selectedPackageId, setSelectedPackageId] = useState<string | null>( const [selectedPackageId, setSelectedPackageId] = useState<string | null>(
null, null,
); );
const [selectedLocationId, setSelectedLocationId] = useState<string | null>( const [selectedChainageId, setSelectedChainageId] = useState<string | null>( // Changed 'selectedLocationId' to 'selectedChainageId'
null, null,
); );
const locations = const chainages =
projectSummary && selectedPackageId && selectedPackageId !== "all" projectSummary && selectedPackageId && selectedPackageId !== "all"
? Object.keys( ? Object.keys(
projectSummary.packages[selectedPackageId]?.locations || {}, projectSummary.packages[selectedPackageId]?.chainages || {},
).map((locName) => ({ ).map((chnName) => ({
id: locName, id: chnName,
name: locName, name: chnName,
})) }))
: []; : [];
@@ -276,21 +267,7 @@ export default function DashboardPage() {
try { try {
setIsLoading(true); setIsLoading(true);
const response = await fetch( const summary: ProjectSummary = await projectService.getProjectSummary(selectedProjectId);
`${API_URL}/summary/projects/${selectedProjectId}`,
{
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "true",
},
},
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const summary: ProjectSummary = await response.json();
setProjectSummary(summary); setProjectSummary(summary);
} catch (err) { } catch (err) {
console.error("Failed to load project summary:", err); console.error("Failed to load project summary:", err);
@@ -306,15 +283,15 @@ export default function DashboardPage() {
loadProjectSummary(); loadProjectSummary();
}, [selectedProjectId]); }, [selectedProjectId]);
// Reset package and location when project changes // Reset package and chainage when project changes
useEffect(() => { useEffect(() => {
setSelectedPackageId(null); setSelectedPackageId(null);
setSelectedLocationId(null); setSelectedChainageId(null); // Changed 'setSelectedLocationId' to 'setSelectedChainageId'
}, [selectedProjectId]); }, [selectedProjectId]);
// Reset location when package changes // Reset chainage when package changes
useEffect(() => { useEffect(() => {
setSelectedLocationId(null); setSelectedChainageId(null);
}, [selectedPackageId]); }, [selectedPackageId]);
// Filter stats based on selections // Filter stats based on selections
@@ -327,7 +304,7 @@ export default function DashboardPage() {
totalDamagedRoadMarking: 0, totalDamagedRoadMarking: 0,
totalGoodSignboard: 0, totalGoodSignboard: 0,
totalRoadDamage: 0, totalRoadDamage: 0,
locationData: [], chainageData: [], // Changed 'locationData' to 'chainageData'
}); });
return; return;
} }
@@ -337,7 +314,7 @@ export default function DashboardPage() {
let totalRoadCrack = 0; let totalRoadCrack = 0;
let totalDamagedRoadMarking = 0; let totalDamagedRoadMarking = 0;
let totalGoodSignboard = 0; let totalGoodSignboard = 0;
const locationData: DetectionStats["locationData"] = []; const chainageData: DetectionStats["chainageData"] = [];
const packagesToProcess = const packagesToProcess =
selectedPackageId && selectedPackageId !== "all" selectedPackageId && selectedPackageId !== "all"
@@ -345,62 +322,62 @@ export default function DashboardPage() {
: projectSummary.packages || {}; : projectSummary.packages || {};
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) { for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
const locationsToProcess = const chainagesToProcess =
selectedLocationId && selectedLocationId !== "all" selectedChainageId && selectedChainageId !== "all"
? { [selectedLocationId]: pkg.locations[selectedLocationId] } ? { [selectedChainageId]: pkg.chainages[selectedChainageId] }
: pkg.locations || {}; : pkg.chainages || {};
for (const [locName, loc] of Object.entries(locationsToProcess)) { for (const [chnName, chn] of Object.entries(chainagesToProcess)) {
if (!loc) continue; if (!chn) continue;
let locDefectedSignboard = 0; let chnDefectedSignboard = 0;
let locPothole = 0; let chnPothole = 0;
let locRoadCrack = 0; let chnRoadCrack = 0;
let locDamagedRoadMarking = 0; let chnDamagedRoadMarking = 0;
let locGoodSignboard = 0; let chnGoodSignboard = 0;
for (const detection of loc.detections || []) { for (const detection of chn.detections || []) {
const type = detection.type.toLowerCase(); const type = detection.type.toLowerCase();
if (type === "defected_sign_board") { if (type === "defected_sign_board") {
locDefectedSignboard++; chnDefectedSignboard++;
totalDefectedSignboard++; totalDefectedSignboard++;
} else if (type === "pothole") { } else if (type === "pothole") {
locPothole++; chnPothole++;
totalPothole++; totalPothole++;
} else if (type === "road_crack") { } else if (type === "road_crack") {
locRoadCrack++; chnRoadCrack++;
totalRoadCrack++; totalRoadCrack++;
} else if (type === "damaged_road_marking") { } else if (type === "damaged_road_marking") {
locDamagedRoadMarking++; chnDamagedRoadMarking++;
totalDamagedRoadMarking++; totalDamagedRoadMarking++;
} else if (type === "good_sign_board") { } else if (type === "good_sign_board") {
locGoodSignboard++; chnGoodSignboard++;
totalGoodSignboard++; totalGoodSignboard++;
} }
} }
if ( if (
locDefectedSignboard > 0 || chnDefectedSignboard > 0 ||
locPothole > 0 || chnPothole > 0 ||
locRoadCrack > 0 || chnRoadCrack > 0 ||
locDamagedRoadMarking > 0 || chnDamagedRoadMarking > 0 ||
locGoodSignboard > 0 chnGoodSignboard > 0
) { ) {
const shortName = const shortName =
locName.length > 20 ? locName.substring(0, 20) + "..." : locName; chnName.length > 20 ? chnName.substring(0, 20) + "..." : chnName;
locationData.push({ chainageData.push({
name: shortName, name: shortName,
defected_sign_board: locDefectedSignboard, defected_sign_board: chnDefectedSignboard,
pothole: locPothole, pothole: chnPothole,
road_crack: locRoadCrack, road_crack: chnRoadCrack,
damaged_road_marking: locDamagedRoadMarking, damaged_road_marking: chnDamagedRoadMarking,
good_sign_board: locGoodSignboard, good_sign_board: chnGoodSignboard,
total: total:
locDefectedSignboard + chnDefectedSignboard +
locPothole + chnPothole +
locRoadCrack + chnRoadCrack +
locDamagedRoadMarking + chnDamagedRoadMarking +
locGoodSignboard, chnGoodSignboard,
}); });
} }
} }
@@ -417,9 +394,9 @@ export default function DashboardPage() {
totalPothole + totalPothole +
totalRoadCrack + totalRoadCrack +
totalDamagedRoadMarking, totalDamagedRoadMarking,
locationData, chainageData,
}); });
}, [projectSummary, selectedPackageId, selectedLocationId]); }, [projectSummary, selectedPackageId, selectedChainageId]);
return ( return (
<> <>
@@ -445,22 +422,22 @@ export default function DashboardPage() {
<PopoverContent className="w-[320px] p-0 overflow-hidden" align="end"> <PopoverContent className="w-[320px] p-0 overflow-hidden" align="end">
<div className="p-4 border-b bg-muted/30"> <div className="p-4 border-b bg-muted/30">
<h3 className="font-bold text-sm flex items-center gap-2 text-foreground"> <h3 className="font-bold text-sm flex items-center gap-2 text-foreground">
<Settings2 className="h-4 w-4 text-primary" /> <Milestone className="h-4 w-4 text-primary" />
Filter Analysis Filter Analysis
</h3> </h3>
<p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or location</p> <p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or chainage</p>
</div> </div>
<div> <div>
<FilterSelector <FilterSelector
projects={projects} projects={projects}
selectedProjectId={selectedProjectId} selectedProjectId={selectedProjectId}
selectedPackageId={selectedPackageId} selectedPackageId={selectedPackageId}
selectedLocationId={selectedLocationId} selectedChainageId={selectedChainageId} // Changed 'selectedLocationId' to 'selectedChainageId'
onProjectChange={setSelectedProjectId} onProjectChange={setSelectedProjectId}
onPackageChange={setSelectedPackageId} onPackageChange={setSelectedPackageId}
onLocationChange={setSelectedLocationId} onChainageChange={setSelectedChainageId} // Changed 'onLocationChange' to 'onChainageChange'
packages={packages} packages={packages}
locations={locations} chainages={chainages} // Changed 'locations' to 'chainages'
isLoading={isLoading} isLoading={isLoading}
/> />
</div> </div>
@@ -554,19 +531,13 @@ export default function DashboardPage() {
<Reveal delay={0.3} direction="right" className="flex flex-col"> <Reveal delay={0.3} direction="right" className="flex flex-col">
<Card className="flex-1 h-full"> <Card className="flex-1 h-full">
<CardHeader className="items-center pb-0"> <CardHeader className="items-center pb-0">
<CardTitle>Detections by Location</CardTitle> <CardTitle className="text-base font-bold">Severity by Chainage</CardTitle>
<CardDescription>Frequency of road issues across different map segments</CardDescription> <CardDescription className="text-xs">Detections grouped by road segment</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex-1 pb-0"> <CardContent className="h-[300px]">
<LocationBarChart <ChainageBarChart
data={stats.locationData.map((loc) => ({ data={stats.chainageData || []}
name: loc.name, isLoading={isLoading}
defected_sign_board: loc.defected_sign_board,
pothole: loc.pothole,
road_crack: loc.road_crack,
damaged_road_marking: loc.damaged_road_marking,
total: loc.total,
}))}
/> />
</CardContent> </CardContent>
<CardFooter className="flex-col gap-2 text-sm"> <CardFooter className="flex-col gap-2 text-sm">
@@ -584,7 +555,7 @@ export default function DashboardPage() {
<DashboardMap <DashboardMap
selectedProjectId={selectedProjectId} selectedProjectId={selectedProjectId}
selectedPackageId={selectedPackageId} selectedPackageId={selectedPackageId}
selectedLocationId={selectedLocationId} selectedChainageId={selectedChainageId} // Changed 'selectedLocationId' to 'selectedChainageId'
projectSummary={projectSummary} projectSummary={projectSummary}
/> />
</div> </div>

View File

@@ -2,7 +2,7 @@
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import { saveSession } from "@/lib/api" import { sessionService } from "@/services/api"
import { SessionContext } from "@/types" import { SessionContext } from "@/types"
import { PowerCircle, TrendingUp } from "lucide-react" import { PowerCircle, TrendingUp } from "lucide-react"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
@@ -21,7 +21,7 @@ export default function NewAnalysisPage() {
const handleSelectionComplete = (session: SessionContext) => { const handleSelectionComplete = (session: SessionContext) => {
// Save session to storage and navigate to upload page // Save session to storage and navigate to upload page
saveSession(session) sessionService.saveSession(session)
router.push(ROUTES.UPLOAD) router.push(ROUTES.UPLOAD)
} }

View File

@@ -10,13 +10,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
import { Loader2, CheckCircle2, Package, FolderKanban, Globe } from "lucide-react" import { Loader2, CheckCircle2, Package, FolderKanban, Globe } from "lucide-react"
import { DataTable } from "@/components/data-table" import { DataTable } from "@/components/data-table"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { import { projectService, packageService } from "@/services/api"
fetchProjects,
fetchAllPackages,
createPackage,
updatePackage,
deletePackage
} from "@/lib/api"
import { import {
Project, Project,
Package as PackageType, Package as PackageType,
@@ -51,13 +45,15 @@ export default function PackagePage() {
const [selectedProjectId, setSelectedProjectId] = useState("") const [selectedProjectId, setSelectedProjectId] = useState("")
const [name, setName] = useState("") const [name, setName] = useState("")
const [region, setRegion] = useState("") const [region, setRegion] = useState("")
const [chainageStartKm, setChainageStartKm] = useState<string>("")
const [chainageEndKm, setChainageEndKm] = useState<string>("")
// Load packages and projects // Load packages and projects
const loadPackages = async (currentSkip = skip, currentLimit = limit) => { const loadPackages = async (currentSkip = skip, currentLimit = limit) => {
try { try {
setIsLoading(true) setIsLoading(true)
setError(null) setError(null)
const data = await fetchAllPackages({ skip: currentSkip, limit: currentLimit }) const data = await packageService.getPackages({ skip: currentSkip, limit: currentLimit })
setPackages(data.items) setPackages(data.items)
setTotalItems(data.totalItems) setTotalItems(data.totalItems)
} catch (err) { } catch (err) {
@@ -73,7 +69,7 @@ export default function PackagePage() {
const loadProjects = async () => { const loadProjects = async () => {
try { try {
setLoadingProjects(true) setLoadingProjects(true)
const data = await fetchProjects({ skip: 0, limit: 1000 }) // Load all projects for selector const data = await projectService.getProjects({ skip: 0, limit: 1000 }) // Load all projects for selector
setProjects(data.items) setProjects(data.items)
} catch (err) { } catch (err) {
setError("Failed to load projects.") setError("Failed to load projects.")
@@ -91,6 +87,8 @@ export default function PackagePage() {
setSelectedProjectId("") setSelectedProjectId("")
setName("") setName("")
setRegion("") setRegion("")
setChainageStartKm("")
setChainageEndKm("")
setError(null) setError(null)
setIsEditing(false) setIsEditing(false)
setCurrentPackage(null) setCurrentPackage(null)
@@ -115,8 +113,10 @@ export default function PackagePage() {
const data: PackageUpdate = { const data: PackageUpdate = {
name: name.trim(), name: name.trim(),
region: region.trim() || null, region: region.trim() || null,
chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : 0,
chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : 0,
} }
await updatePackage(currentPackage.id, data) await packageService.updatePackage(currentPackage.id, data)
toast.success("Package Updated", { toast.success("Package Updated", {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`, description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
}) })
@@ -125,8 +125,10 @@ export default function PackagePage() {
project_id: selectedProjectId, project_id: selectedProjectId,
name: name.trim(), name: name.trim(),
region: region.trim() || null, region: region.trim() || null,
chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : 0,
chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : 0,
} }
await createPackage(data) await packageService.createPackage(data)
toast.success("Package Created", { toast.success("Package Created", {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`, description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
}) })
@@ -155,6 +157,8 @@ export default function PackagePage() {
setSelectedProjectId(pkg.project_id) setSelectedProjectId(pkg.project_id)
setName(pkg.name || "") setName(pkg.name || "")
setRegion(pkg.region || "") setRegion(pkg.region || "")
setChainageStartKm(pkg.chainage_start_km?.toString() || "0")
setChainageEndKm(pkg.chainage_end_km?.toString() || "0")
setIsModalOpen(true) setIsModalOpen(true)
} }
@@ -163,7 +167,7 @@ export default function PackagePage() {
try { try {
setIsLoading(true) setIsLoading(true)
await deletePackage(pkg.id) await packageService.deletePackage(pkg.id)
toast.success("Package Deleted", { toast.success("Package Deleted", {
description: `${pkg.name} has been removed from the system.`, description: `${pkg.name} has been removed from the system.`,
}) })
@@ -242,6 +246,16 @@ export default function PackagePage() {
accessorKey: "region", accessorKey: "region",
header: "Region", header: "Region",
}, },
{
accessorKey: "chainage_start_km",
header: "Start (km)",
cell: ({ row }) => row.original.chainage_start_km?.toFixed(2) ?? "0.00"
},
{
accessorKey: "chainage_end_km",
header: "End (km)",
cell: ({ row }) => row.original.chainage_end_km?.toFixed(2) ?? "0.00"
},
] ]
return ( return (
@@ -383,6 +397,36 @@ export default function PackagePage() {
className="h-11 bg-muted/20 border-border/60" className="h-11 bg-muted/20 border-border/60"
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="chainage-start" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Chainage Start (km)
</Label>
<Input
id="chainage-start"
type="number"
step="0.01"
value={chainageStartKm}
onChange={(e) => setChainageStartKm(e.target.value)}
placeholder="0.00"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
<div className="space-y-2">
<Label htmlFor="chainage-end" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Chainage End (km)
</Label>
<Input
id="chainage-end"
type="number"
step="0.01"
value={chainageEndKm}
onChange={(e) => setChainageEndKm(e.target.value)}
placeholder="0.00"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
</div> </div>
</div> </div>
@@ -396,14 +440,14 @@ export default function PackagePage() {
resetForm() resetForm()
}} }}
disabled={isSubmitting} disabled={isSubmitting}
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs border-border/80 hover:bg-muted/50" className="flex-1"
> >
Cancel Cancel
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={isSubmitting || !name.trim() || !selectedProjectId} disabled={isSubmitting || !name.trim() || !selectedProjectId}
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs" className="flex-1"
> >
{isSubmitting ? ( {isSubmitting ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
import { Loader2, CheckCircle2, Layers, MapPin, Building2, Route, X } from "lucide-react" import { Loader2, CheckCircle2, Layers, MapPin, Building2, Route, X } from "lucide-react"
import { DataTable } from "@/components/data-table" import { DataTable } from "@/components/data-table"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { createProject, fetchProjects, updateProject, deleteProject } from "@/lib/api" import { projectService } from "@/services/api"
import { ProjectCreate, Project, ProjectUpdate } from "@/types" import { ProjectCreate, Project, ProjectUpdate } from "@/types"
import { ColumnDef } from "@tanstack/react-table" import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner" import { toast } from "sonner"
@@ -47,7 +47,7 @@ export default function ProjectPage() {
try { try {
setIsLoading(true) setIsLoading(true)
setError(null) setError(null)
const data = await fetchProjects({ skip: currentSkip, limit: currentLimit }) const data = await projectService.getProjects({ skip: currentSkip, limit: currentLimit })
setProjects(data.items) setProjects(data.items)
setTotalItems(data.totalItems) setTotalItems(data.totalItems)
} catch (err) { } catch (err) {
@@ -98,7 +98,7 @@ export default function ProjectPage() {
end_lat: endLat ? parseFloat(endLat) : null, end_lat: endLat ? parseFloat(endLat) : null,
end_lng: endLng ? parseFloat(endLng) : null, end_lng: endLng ? parseFloat(endLng) : null,
} }
await updateProject(currentProject.id, data) await projectService.updateProject(currentProject.id, data)
toast.success("Project Updated", { toast.success("Project Updated", {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`, description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
}) })
@@ -112,7 +112,7 @@ export default function ProjectPage() {
end_lat: endLat ? parseFloat(endLat) : null, end_lat: endLat ? parseFloat(endLat) : null,
end_lng: endLng ? parseFloat(endLng) : null, end_lng: endLng ? parseFloat(endLng) : null,
} }
await createProject(data) await projectService.createProject(data)
toast.success("Project Created", { toast.success("Project Created", {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`, description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
}) })
@@ -153,7 +153,7 @@ export default function ProjectPage() {
try { try {
setIsLoading(true) setIsLoading(true)
await deleteProject(project.id) await projectService.deleteProject(project.id)
toast.success("Project Deleted", { toast.success("Project Deleted", {
description: `${project.name} has been removed from the system.`, description: `${project.name} has been removed from the system.`,
}) })

View File

@@ -7,10 +7,7 @@ import { Loader2, TrendingUp } from "lucide-react"
import VideoPlayerSection from "@/components/video-player-section" import VideoPlayerSection from "@/components/video-player-section"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by" import { PoweredBy } from "@/components/powered-by"
import { import { sessionService, videoService } from "@/services/api"
loadSession,
clearSession
} from "@/lib/api"
import { SessionContext, DetectionData, DetectionType } from "@/types" import { SessionContext, DetectionData, DetectionType } from "@/types"
import { getVideoFile, clearVideoFile } from "@/lib/video-storage" import { getVideoFile, clearVideoFile } from "@/lib/video-storage"
import { ROUTES } from "@/utils/routes" import { ROUTES } from "@/utils/routes"
@@ -29,24 +26,13 @@ export default function VideoResultsPage() {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
const storedSession = loadSession() const storedSession = sessionService.loadSession()
setSession(storedSession) setSession(storedSession)
const fetchResults = async () => { const fetchResults = async () => {
try { try {
// Fetch detection data from backend // Fetch detection data from backend
const response = await fetch(`${API_URL}/results/${videoId}`, { const data = await videoService.getVideoResults(videoId);
headers: { "ngrok-skip-browser-warning": "true" }
})
if (!response.ok) {
if (response.status === 404) {
throw new Error("Results not found for this video.")
}
throw new Error(`Failed to load results: ${response.status}`)
}
const data = await response.json()
setDetectionData(data as any) setDetectionData(data as any)
// Try to infer detection type from results if possible // Try to infer detection type from results if possible
@@ -81,7 +67,7 @@ export default function VideoResultsPage() {
console.error("Failed to clear video file:", err) console.error("Failed to clear video file:", err)
} }
} }
clearSession() sessionService.clearSession()
router.push(ROUTES.NEW_ANALYSIS) router.push(ROUTES.NEW_ANALYSIS)
} }
@@ -146,9 +132,9 @@ export default function VideoResultsPage() {
</span> </span>
</div> </div>
<div className="flex flex-col border-l pl-12 border-border/60"> <div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Location</span> <span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Chainage</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight"> <span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.locationName} {session.chainageName}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -7,12 +7,7 @@ import { Loader2, TrendingUp } from "lucide-react"
import VideoPlayerSection from "@/components/video-player-section" import VideoPlayerSection from "@/components/video-player-section"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by" import { PoweredBy } from "@/components/powered-by"
import { import { sessionService } from "@/services/api"
loadSession,
loadVideoData,
isSessionValid,
clearSession
} from "@/lib/api"
import { SessionContext, DetectionData, DetectionType } from "@/types" import { SessionContext, DetectionData, DetectionType } from "@/types"
import { clearVideoFile } from "@/lib/video-storage" import { clearVideoFile } from "@/lib/video-storage"
import { ROUTES } from "@/utils/routes" import { ROUTES } from "@/utils/routes"
@@ -30,10 +25,10 @@ export default function ResultsPage() {
// Load session and video data on mount // Load session and video data on mount
useEffect(() => { useEffect(() => {
const storedSession = loadSession() const storedSession = sessionService.loadSession()
const videoData = loadVideoData() const videoData = sessionService.loadVideoData()
if (!isSessionValid(storedSession) || !videoData) { if (!sessionService.isSessionValid(storedSession) || !videoData) {
router.replace(ROUTES.NEW_ANALYSIS) router.replace(ROUTES.NEW_ANALYSIS)
return return
} }
@@ -56,7 +51,7 @@ export default function ResultsPage() {
console.error("Failed to clear video file:", err) console.error("Failed to clear video file:", err)
} }
} }
clearSession() sessionService.clearSession()
router.push(ROUTES.NEW_ANALYSIS) router.push(ROUTES.NEW_ANALYSIS)
} }
@@ -118,9 +113,9 @@ export default function ResultsPage() {
</span> </span>
</div> </div>
<div className="flex flex-col border-l pl-12 border-border/60"> <div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Location</span> <span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Chainage</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight"> <span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.locationName} {session.chainageName}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -6,9 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader2, TrendingUp } from "lucide-react" import { Loader2, TrendingUp } from "lucide-react"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by" import { PoweredBy } from "@/components/powered-by"
import { import { sessionService, videoService } from "@/services/api"
loadSession,
} from "@/lib/api"
import { SessionContext } from "@/types" import { SessionContext } from "@/types"
import { ROUTES } from "@/utils/routes" import { ROUTES } from "@/utils/routes"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -68,25 +66,12 @@ export default function VideoProcessingPage() {
}, [router]) }, [router])
useEffect(() => { useEffect(() => {
const storedSession = loadSession() const storedSession = sessionService.loadSession()
setSession(storedSession) setSession(storedSession)
const checkStatus = async () => { const checkStatus = async () => {
try { try {
const response = await fetch(`${API_URL}/status/${videoId}`, { const statusData = await videoService.getVideoStatus(videoId);
headers: { "ngrok-skip-browser-warning": "true" }
})
if (response.status === 404) {
router.replace(ROUTES.UPLOAD)
return
}
if (!response.ok) {
throw new Error("Failed to fetch status")
}
const statusData = await response.json()
if (statusData.status === "completed") { if (statusData.status === "completed") {
router.replace(`/results/${videoId}`) router.replace(`/results/${videoId}`)
@@ -157,9 +142,9 @@ export default function VideoProcessingPage() {
</span> </span>
</div> </div>
<div className="flex flex-col border-l pl-12 border-border/60"> <div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Location</span> <span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Chainage</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight"> <span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.locationName} {session.chainageName}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -10,12 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Loader2, TrendingUp } from "lucide-react" import { Loader2, TrendingUp } from "lucide-react"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by" import { PoweredBy } from "@/components/powered-by"
import { import { sessionService, videoService } from "@/services/api"
loadSession,
isSessionValid,
saveVideoData,
clearSession
} from "@/lib/api"
import { SessionContext } from "@/types" import { SessionContext } from "@/types"
import { ROUTES } from "@/utils/routes" import { ROUTES } from "@/utils/routes"
import { storeVideoFile } from "@/lib/video-storage" import { storeVideoFile } from "@/lib/video-storage"
@@ -59,8 +54,8 @@ export default function UploadPage() {
// Load session on mount // Load session on mount
useEffect(() => { useEffect(() => {
const storedSession = loadSession() const storedSession = sessionService.loadSession()
if (!isSessionValid(storedSession)) { if (!sessionService.isSessionValid(storedSession)) {
router.replace(ROUTES.NEW_ANALYSIS) router.replace(ROUTES.NEW_ANALYSIS)
return return
} }
@@ -89,18 +84,7 @@ export default function UploadPage() {
setError(null) setError(null)
try { try {
const response = await fetch(`${API_URL}/upload`, { const result = await videoService.uploadVideo(formData);
method: "POST",
headers: { "ngrok-skip-browser-warning": "true" },
body: formData
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Upload failed (${response.status}): ${errorText}`)
}
const result = await response.json()
// Store file locally for potential recovery/results display // Store file locally for potential recovery/results display
if (file) { if (file) {
@@ -111,7 +95,7 @@ export default function UploadPage() {
} }
} }
saveVideoData({ videoId: result.video_id, detectionType }) sessionService.saveVideoData({ videoId: result.video_id, detectionType })
// Redirect to the dynamic processing page // Redirect to the dynamic processing page
router.push(`/upload/${result.video_id}`) router.push(`/upload/${result.video_id}`)
@@ -131,7 +115,7 @@ export default function UploadPage() {
} }
const handleBackToSelection = () => { const handleBackToSelection = () => {
clearSession() sessionService.clearSession()
router.push(ROUTES.NEW_ANALYSIS) router.push(ROUTES.NEW_ANALYSIS)
} }
@@ -184,9 +168,9 @@ export default function UploadPage() {
</span> </span>
</div> </div>
<div className="flex flex-col border-l pl-12 border-border/60"> <div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Location</span> <span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Chainage</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight"> <span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.locationName} {session.chainageName}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@ import {
Layers, Layers,
Package, Package,
MapPin, MapPin,
Milestone,
} from "lucide-react"; } from "lucide-react";
import { NavUser } from "@/components/nav-user"; import { NavUser } from "@/components/nav-user";
import { ROUTES } from "@/utils/routes"; import { ROUTES } from "@/utils/routes";
@@ -53,9 +54,9 @@ const data = {
icon: Package, icon: Package,
}, },
{ {
title: "Location", title: "Chainage",
url: ROUTES.LOCATION, url: ROUTES.CHAINAGE,
icon: MapPin, icon: Milestone,
}, },
], ],
}; };

View File

@@ -11,7 +11,7 @@ import {
type ChartConfig, type ChartConfig,
} from "@/components/ui/chart" } from "@/components/ui/chart"
interface LocationData { interface ChainageData {
name: string name: string
defected_sign_board: number defected_sign_board: number
pothole: number pothole: number
@@ -20,8 +20,8 @@ interface LocationData {
total: number total: number
} }
interface LocationBarChartProps { interface ChainageBarChartProps {
data: LocationData[] data: ChainageData[]
isLoading?: boolean isLoading?: boolean
} }
@@ -44,7 +44,7 @@ const chartConfig = {
}, },
} satisfies ChartConfig } satisfies ChartConfig
export function LocationBarChart({ data, isLoading = false }: LocationBarChartProps) { export function ChainageBarChart({ data, isLoading = false }: ChainageBarChartProps) {
if (isLoading) { if (isLoading) {
return ( return (
<div className="h-[250px] flex items-center justify-center"> <div className="h-[250px] flex items-center justify-center">
@@ -56,8 +56,8 @@ export function LocationBarChart({ data, isLoading = false }: LocationBarChartPr
if (data.length === 0) { if (data.length === 0) {
return ( return (
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground"> <div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No location data available</p> <p className="text-sm">No chainage data available</p>
<p className="text-xs mt-1">Process videos to see detections by location</p> <p className="text-xs mt-1">Process videos to see detections by chainage</p>
</div> </div>
) )
} }

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader2, MapPin } from "lucide-react" import { Loader2, Milestone } from "lucide-react"
import { Detection } from "@/types" import { Detection } from "@/types"
// Dynamically import the map to avoid SSR issues with Leaflet // Dynamically import the map to avoid SSR issues with Leaflet
@@ -23,7 +23,7 @@ interface DashboardMapProps {
className?: string className?: string
selectedProjectId?: string | null selectedProjectId?: string | null
selectedPackageId?: string | null selectedPackageId?: string | null
selectedLocationId?: string | null selectedChainageId?: string | null
projectSummary?: any projectSummary?: any
} }
@@ -31,7 +31,7 @@ export function DashboardMap({
className, className,
selectedProjectId, selectedProjectId,
selectedPackageId, selectedPackageId,
selectedLocationId, selectedChainageId,
projectSummary projectSummary
}: DashboardMapProps) { }: DashboardMapProps) {
const [detections, setDetections] = useState<Detection[]>([]) const [detections, setDetections] = useState<Detection[]>([])
@@ -56,14 +56,14 @@ export function DashboardMap({
: projectSummary.packages || {} : projectSummary.packages || {}
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) { for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
const locationsToProcess = selectedLocationId && selectedLocationId !== "all" const chainagesToProcess = selectedChainageId && selectedChainageId !== "all"
? { [selectedLocationId]: (pkg as any).locations[selectedLocationId] } ? { [selectedChainageId]: (pkg as any).chainages[selectedChainageId] }
: (pkg as any).locations || {} : (pkg as any).chainages || {}
for (const [locName, loc] of Object.entries(locationsToProcess)) { for (const [chnName, chn] of Object.entries(chainagesToProcess)) {
if (!loc) continue if (!chn) continue
const locationDetections = (loc as any).detections || [] const chainageDetections = (chn as any).detections || []
filteredDetections.push(...locationDetections) filteredDetections.push(...chainageDetections)
} }
} }
@@ -74,7 +74,7 @@ export function DashboardMap({
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
}, [projectSummary, selectedPackageId, selectedLocationId]) }, [projectSummary, selectedPackageId, selectedChainageId])
// Filter detections with valid GPS coordinates // Filter detections with valid GPS coordinates
const validDetections = detections.filter(d => d.latitude && d.longitude) const validDetections = detections.filter(d => d.latitude && d.longitude)
@@ -94,7 +94,7 @@ export function DashboardMap({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 rounded-md bg-secondary text-secondary-foreground flex items-center justify-center"> <div className="p-2 rounded-md bg-secondary text-secondary-foreground flex items-center justify-center">
<MapPin className="h-5 w-5" /> <Milestone className="h-5 w-5" />
</div> </div>
<div> <div>
<CardTitle className="text-base font-bold"> <CardTitle className="text-base font-bold">

View File

@@ -7,19 +7,19 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { FolderOpen, Package, MapPin } from "lucide-react" import { FolderOpen, Package, Milestone } from "lucide-react"
import { Project } from "@/types" import { Project } from "@/types"
interface FilterSelectorProps { interface FilterSelectorProps {
projects: Project[] projects: Project[]
selectedProjectId: string | null selectedProjectId: string | null
selectedPackageId: string | null selectedPackageId: string | null
selectedLocationId: string | null selectedChainageId: string | null
onProjectChange: (projectId: string) => void onProjectChange: (projectId: string) => void
onPackageChange: (packageId: string) => void onPackageChange: (packageId: string) => void
onLocationChange: (locationId: string) => void onChainageChange: (chainageId: string) => void
packages: Array<{ id: string; name: string }> packages: Array<{ id: string; name: string }>
locations: Array<{ id: string; name: string }> chainages: Array<{ id: string; name: string }>
isLoading?: boolean isLoading?: boolean
} }
@@ -27,12 +27,12 @@ export function FilterSelector({
projects, projects,
selectedProjectId, selectedProjectId,
selectedPackageId, selectedPackageId,
selectedLocationId, selectedChainageId,
onProjectChange, onProjectChange,
onPackageChange, onPackageChange,
onLocationChange, onChainageChange,
packages, packages,
locations, chainages,
isLoading = false isLoading = false
}: FilterSelectorProps) { }: FilterSelectorProps) {
return ( return (
@@ -92,28 +92,28 @@ export function FilterSelector({
</div> </div>
)} )}
{/* Location Dropdown */} {/* Chainage Dropdown */}
{selectedPackageId && selectedPackageId !== "all" && ( {selectedPackageId && selectedPackageId !== "all" && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm"> <div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
<MapPin className="h-4 w-4" /> <Milestone className="h-4 w-4" />
</div> </div>
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">Location</span> <span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">Chainage</span>
</div> </div>
<Select <Select
value={selectedLocationId || "all"} value={selectedChainageId || "all"}
onValueChange={onLocationChange} onValueChange={onChainageChange}
disabled={isLoading} disabled={isLoading}
> >
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs"> <SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
<SelectValue placeholder="All locations" /> <SelectValue placeholder="All chainages" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Locations</SelectItem> <SelectItem value="all">All Chainages</SelectItem>
{locations.map((loc) => ( {chainages.map((chn) => (
<SelectItem key={loc.id} value={loc.id}> <SelectItem key={chn.id} value={chn.id}>
{loc.name} {chn.name}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>

View File

@@ -6,15 +6,11 @@ import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Loader2, Check } from "lucide-react" import { Loader2, Check } from "lucide-react"
import { import { projectService, packageService, chainageService } from "@/services/api"
fetchProjects,
fetchPackagesByProject,
fetchLocationsByPackage,
} from "@/lib/api"
import { import {
Project, Project,
Package as PackageType, Package as PackageType,
Location, Chainage,
SessionContext SessionContext
} from "@/types" } from "@/types"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -27,17 +23,17 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
// Data states // Data states
const [projects, setProjects] = useState<Project[]>([]) const [projects, setProjects] = useState<Project[]>([])
const [packages, setPackages] = useState<PackageType[]>([]) const [packages, setPackages] = useState<PackageType[]>([])
const [locations, setLocations] = useState<Location[]>([]) const [chainages, setChainages] = useState<Chainage[]>([])
// Selection states // Selection states
const [selectedProject, setSelectedProject] = useState<Project | null>(null) const [selectedProject, setSelectedProject] = useState<Project | null>(null)
const [selectedPackage, setSelectedPackage] = useState<PackageType | null>(null) const [selectedPackage, setSelectedPackage] = useState<PackageType | null>(null)
const [selectedLocation, setSelectedLocation] = useState<Location | null>(null) const [selectedChainage, setSelectedChainage] = useState<Chainage | null>(null)
// Loading states // Loading states
const [loadingProjects, setLoadingProjects] = useState(true) const [loadingProjects, setLoadingProjects] = useState(true)
const [loadingPackages, setLoadingPackages] = useState(false) const [loadingPackages, setLoadingPackages] = useState(false)
const [loadingLocations, setLoadingLocations] = useState(false) const [loadingChainages, setLoadingChainages] = useState(false)
// Error state // Error state
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -48,7 +44,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
try { try {
setLoadingProjects(true) setLoadingProjects(true)
setError(null) setError(null)
const data = await fetchProjects() const data = await projectService.getProjects()
setProjects(data.items) setProjects(data.items)
} catch (err) { } catch (err) {
console.error("Failed to load projects:", err) console.error("Failed to load projects:", err)
@@ -73,9 +69,9 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
setLoadingPackages(true) setLoadingPackages(true)
setError(null) setError(null)
setSelectedPackage(null) setSelectedPackage(null)
setSelectedLocation(null) setSelectedChainage(null)
setLocations([]) setChainages([])
const data = await fetchPackagesByProject(selectedProject.id) const data = await packageService.getPackagesByProject(selectedProject.id)
setPackages(data.items) setPackages(data.items)
} catch (err) { } catch (err) {
console.error("Failed to load packages:", err) console.error("Failed to load packages:", err)
@@ -87,29 +83,29 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
loadPackages() loadPackages()
}, [selectedProject]) }, [selectedProject])
// Load locations when package changes // Load chainages when package changes
useEffect(() => { useEffect(() => {
if (!selectedPackage) { if (!selectedPackage) {
setLocations([]) setChainages([])
setSelectedLocation(null) setSelectedChainage(null)
return return
} }
const loadLocations = async () => { const loadChainages = async () => {
try { try {
setLoadingLocations(true) setLoadingChainages(true)
setError(null) setError(null)
setSelectedLocation(null) setSelectedChainage(null)
const data = await fetchLocationsByPackage(selectedPackage.id) const data = await chainageService.getChainagesByPackage(selectedPackage.id)
setLocations(data.items) setChainages(data.items)
} catch (err) { } catch (err) {
console.error("Failed to load locations:", err) console.error("Failed to load chainages:", err)
setError("Failed to load locations for the selected package.") setError("Failed to load chainages for the selected package.")
} finally { } finally {
setLoadingLocations(false) setLoadingChainages(false)
} }
} }
loadLocations() loadChainages()
}, [selectedPackage]) }, [selectedPackage])
const handleProjectChange = (projectId: string) => { const handleProjectChange = (projectId: string) => {
@@ -122,31 +118,31 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
setSelectedPackage(pkg) setSelectedPackage(pkg)
} }
const handleLocationChange = (locationId: string) => { const handleChainageChange = (chainageId: string) => {
const location = locations.find(l => l.id === locationId) || null const chainage = chainages.find(chn => chn.id === chainageId) || null
setSelectedLocation(location) setSelectedChainage(chainage)
} }
const handleProceed = () => { const handleProceed = () => {
if (selectedProject && selectedPackage && selectedLocation) { if (selectedProject && selectedPackage && selectedChainage) {
onSelectionComplete({ onSelectionComplete({
projectId: selectedProject.id, projectId: selectedProject.id,
projectName: selectedProject.name, projectName: selectedProject.name,
packageId: selectedPackage.id, packageId: selectedPackage.id,
packageName: selectedPackage.name, packageName: selectedPackage.name,
locationId: selectedLocation.id, chainageId: selectedChainage.id,
locationName: selectedLocation.segment_name chainageName: selectedChainage.segment_name
}) })
} }
} }
const isComplete = selectedProject && selectedPackage && selectedLocation const isComplete = selectedProject && selectedPackage && selectedChainage
// Step status helpers // Step status helpers
const getStepStatus = (step: number) => { const getStepStatus = (step: number) => {
if (step === 1) return selectedProject ? 'completed' : 'active' if (step === 1) return selectedProject ? 'completed' : 'active'
if (step === 2) return selectedPackage ? 'completed' : selectedProject ? 'active' : 'pending' if (step === 2) return selectedPackage ? 'completed' : selectedProject ? 'active' : 'pending'
if (step === 3) return selectedLocation ? 'completed' : selectedPackage ? 'active' : 'pending' if (step === 3) return selectedChainage ? 'completed' : selectedPackage ? 'active' : 'pending'
return 'pending' return 'pending'
} }
@@ -155,9 +151,9 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
<CardHeader className="pb-6"> <CardHeader className="pb-6">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div> <div>
<CardTitle className="text-2xl font-bold">Select Project Location</CardTitle> <CardTitle className="text-2xl font-bold">Select Project Chainage</CardTitle>
<CardDescription className="mt-2 text-base"> <CardDescription className="mt-2 text-base">
Select Project, Package & Location to begin intelligent road analysis. Select Project, Package & Chainage to begin intelligent road analysis.
</CardDescription> </CardDescription>
</div> </div>
@@ -165,7 +161,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
<div className="flex items-center justify-center pt-2"> <div className="flex items-center justify-center pt-2">
{[1, 2, 3].map((step, index) => { {[1, 2, 3].map((step, index) => {
const status = getStepStatus(step) const status = getStepStatus(step)
const labels = ['Project', 'Package', 'Location'] const labels = ['Project', 'Package', 'Chainage']
return ( return (
<div key={step} className="flex items-center"> <div key={step} className="flex items-center">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
@@ -268,30 +264,35 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
</Select> </Select>
</div> </div>
{/* Location Dropdown */} {/* Chainage Dropdown */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="location" className="text-sm font-semibold"> <Label htmlFor="chainage" className="text-sm font-semibold">
Location Chainage
</Label> </Label>
<Select <Select
value={selectedLocation?.id || ""} value={selectedChainage?.id || ""}
onValueChange={handleLocationChange} onValueChange={handleChainageChange}
disabled={!selectedPackage || loadingLocations} disabled={!selectedPackage || loadingChainages}
> >
<SelectTrigger id="location" className="h-11"> <SelectTrigger id="chainage" className="h-11">
{loadingLocations ? ( {loadingChainages ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
<span>Loading...</span> <span>Loading...</span>
</div> </div>
) : ( ) : (
<SelectValue placeholder={selectedPackage ? "Select location" : "Select package first"} /> <SelectValue placeholder={selectedPackage ? "Select chainage" : "Select package first"} />
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{locations.map((location) => ( {chainages.map((chn) => (
<SelectItem key={location.id} value={location.id}> <SelectItem key={chn.id} value={chn.id}>
{location.segment_name} <div className="flex flex-col">
<span>{chn.segment_name}</span>
<span className="text-[10px] text-muted-foreground">
{chn.chainage_start_km}-{chn.chainage_end_km} km | {chn.direction}
</span>
</div>
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -308,7 +309,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
<span>/</span> <span>/</span>
<span className="text-foreground">{selectedPackage?.name}</span> <span className="text-foreground">{selectedPackage?.name}</span>
<span>/</span> <span>/</span>
<span className="text-foreground">{selectedLocation?.segment_name}</span> <span className="text-foreground">{selectedChainage?.segment_name}</span>
</div> </div>
</div> </div>
)} )}

View File

@@ -10,6 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Upload, Loader2, AlertCircle } from "lucide-react" import { Upload, Loader2, AlertCircle } from "lucide-react"
import type { DetectionData, DetectionType } from "@/types" import type { DetectionData, DetectionType } from "@/types"
import { videoService } from "@/services/api"
const API_URL = process.env.NEXT_PUBLIC_API_URL const API_URL = process.env.NEXT_PUBLIC_API_URL
const WS_URL = API_URL?.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") const WS_URL = API_URL?.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://")
@@ -109,17 +110,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
const loadResults = async (videoId: string) => { const loadResults = async (videoId: string) => {
try { try {
console.log("[Upload] Loading results for:", videoId) console.log("[Upload] Loading results for:", videoId)
const response = await fetch(`${API_URL}/results/${videoId}`, { const detectionData: DetectionData = await videoService.getVideoResults(videoId);
headers: {
"ngrok-skip-browser-warning": "true"
}
})
if (!response.ok) {
throw new Error(`Failed to load results: ${response.status}`)
}
const detectionData: DetectionData = await response.json()
console.log("[Upload] Results loaded:", detectionData) console.log("[Upload] Results loaded:", detectionData)
setUploading(false) setUploading(false)
@@ -162,29 +153,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
setError(null) setError(null)
try { try {
console.log("[Upload] Uploading to:", `${API_URL}/upload`) const result = await videoService.uploadVideo(formData);
console.log("[Upload] Detection type:", detectionType)
console.log("[Upload] FormData contents:")
console.log(" - file:", file.name)
console.log(" - detection_type:", detectionType)
console.log(" - speed_kmh:", speed)
const response = await fetch(`${API_URL}/upload`, {
method: "POST",
headers: {
"ngrok-skip-browser-warning": "true"
},
body: formData
})
console.log("[Upload] Upload response status:", response.status)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Upload failed (${response.status}): ${errorText}`)
}
const result = await response.json()
const videoId = result.video_id const videoId = result.video_id
console.log("[Upload] Video uploaded successfully, ID:", videoId) console.log("[Upload] Video uploaded successfully, ID:", videoId)
@@ -200,7 +169,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
let errorMessage = "Upload failed" let errorMessage = "Upload failed"
if (error instanceof TypeError && error.message === "Failed to fetch") { if (error instanceof TypeError && error.message === "Failed to fetch") {
errorMessage = "Cannot connect to server. Please check:\n• Backend is running on " + API_URL + "\n• CORS is configured properly\n• No firewall blocking the connection" errorMessage = "Cannot connect to server. Please check if backend is running."
} else if (error instanceof Error) { } else if (error instanceof Error) {
errorMessage = error.message errorMessage = error.message
} }

View File

@@ -14,7 +14,7 @@ const MapModal = dynamic(() => import("@/components/map-modal"), { ssr: false })
import { DetectionData, DetectionType } from "@/types" import { DetectionData, DetectionType } from "@/types"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const API_URL = process.env.NEXT_PUBLIC_API_URL import { projectService } from "@/services/api"
type VideoPlayerSectionProps = { type VideoPlayerSectionProps = {
data: DetectionData data: DetectionData
@@ -37,7 +37,7 @@ type DetectionLog = {
videoTime: string // Video timestamp (MM:SS) videoTime: string // Video timestamp (MM:SS)
} }
type LocationSummaryData = { type ChainageSummaryData = {
project: { project: {
id: string id: string
name: string name: string
@@ -48,9 +48,9 @@ type LocationSummaryData = {
[packageName: string]: { [packageName: string]: {
package_id: string package_id: string
region: string | null region: string | null
locations: { chainages: {
[locationName: string]: { [chainageName: string]: {
location_id: string chainage_id: string
chainage: string | null chainage: string | null
detection_count: number detection_count: number
detections: Array<{ detections: Array<{
@@ -87,7 +87,7 @@ function DetailedSummarySection({
show: boolean show: boolean
detectionType: DetectionType detectionType: DetectionType
}) { }) {
const [summaryData, setSummaryData] = useState<LocationSummaryData | null>(null) const [summaryData, setSummaryData] = useState<ChainageSummaryData | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [showMap, setShowMap] = useState(false) const [showMap, setShowMap] = useState(false)
@@ -97,15 +97,8 @@ function DetailedSummarySection({
const fetchSummary = async () => { const fetchSummary = async () => {
setLoading(true) setLoading(true)
try { try {
const response = await fetch(`${API_URL}/summary/projects/${projectId}?video_id=${videoId}`, { const data = await projectService.getProjectSummaryByVideo(projectId, videoId);
headers: { "ngrok-skip-browser-warning": "true" }
})
if (response.ok) {
const data = await response.json()
setSummaryData(data) setSummaryData(data)
} else {
console.error(`Failed to fetch summary: ${response.status}`)
}
} catch (err) { } catch (err) {
console.error("Failed to fetch summary:", err) console.error("Failed to fetch summary:", err)
} finally { } finally {
@@ -124,14 +117,14 @@ function DetailedSummarySection({
// Flatten all detections for the scrollable list // Flatten all detections for the scrollable list
const allDetections: Array<{ const allDetections: Array<{
detection: any detection: any
locationName: string chainageName: string
packageName: string packageName: string
}> = [] }> = []
Object.entries(summaryData.packages || {}).forEach(([packageName, packageData]) => { Object.entries(summaryData.packages || {}).forEach(([packageName, packageData]) => {
Object.entries(packageData?.locations || {}).forEach(([locationName, locationData]) => { Object.entries(packageData?.chainages || {}).forEach(([chainageName, chainageData]) => {
locationData?.detections?.forEach(detection => { chainageData?.detections?.forEach(detection => {
allDetections.push({ detection, locationName, packageName }) allDetections.push({ detection, chainageName, packageName })
}) })
}) })
}) })
@@ -147,7 +140,7 @@ function DetailedSummarySection({
</div> </div>
<div> <div>
<CardTitle className="text-base font-bold"> <CardTitle className="text-base font-bold">
Locations Chainages
</CardTitle> </CardTitle>
<CardDescription className="text-xs"> <CardDescription className="text-xs">
{isCombined ? "Potholes & Signboards" : isPothole ? "Potholes" : "Signboards"} detected {isCombined ? "Potholes & Signboards" : isPothole ? "Potholes" : "Signboards"} detected
@@ -186,16 +179,16 @@ function DetailedSummarySection({
)} )}
</div> </div>
{/* Packages and Locations */} {/* Packages and Chainages */}
{Object.entries(summaryData.packages).map(([packageName, packageData]) => ( {Object.entries(summaryData.packages).map(([packageName, packageData]) => (
<div key={packageData.package_id} className="space-y-2"> <div key={packageData.package_id} className="space-y-2">
<div className="text-xs font-bold text-muted-foreground truncate">{packageName}</div> <div className="text-xs font-bold text-muted-foreground truncate">{packageName}</div>
<div className="pl-2 space-y-2 border-l-2 border-muted"> <div className="pl-2 space-y-2 border-l-2 border-muted">
{Object.entries(packageData.locations).map(([locationName, locationData]) => ( {Object.entries(packageData.chainages).map(([chainageName, chainageData]) => (
<div key={locationData.location_id} className="text-xs p-3 rounded-md bg-muted/50 flex items-center justify-between gap-4"> <div key={chainageData.chainage_id} className="text-xs p-3 rounded-md bg-muted/50 flex items-center justify-between gap-4">
<div className="font-semibold truncate">{locationName}</div> <div className="font-semibold truncate">{chainageName}</div>
<div className="text-[10px] bg-background px-2 py-0.5 rounded border font-bold"> <div className="text-[10px] bg-background px-2 py-0.5 rounded border font-bold">
{locationData.detection_count} {chainageData.detection_count}
</div> </div>
</div> </div>
))} ))}
@@ -226,7 +219,7 @@ function DetailedSummarySection({
<CardContent className="flex-1 p-0 overflow-hidden"> <CardContent className="flex-1 p-0 overflow-hidden">
<ScrollArea className="h-[300px] p-4"> <ScrollArea className="h-[300px] p-4">
<div className="space-y-2"> <div className="space-y-2">
{allDetections.map(({ detection, locationName }, idx) => ( {allDetections.map(({ detection, chainageName }, idx) => (
<div <div
key={`${detection.id}-${idx}`} key={`${detection.id}-${idx}`}
className="text-xs p-3 bg-muted/30 border rounded-md hover:bg-muted/50 transition-colors" className="text-xs p-3 bg-muted/30 border rounded-md hover:bg-muted/50 transition-colors"
@@ -240,7 +233,7 @@ function DetailedSummarySection({
</Badge> </Badge>
</div> </div>
<div className="grid grid-cols-2 gap-2 text-[10px] text-muted-foreground"> <div className="grid grid-cols-2 gap-2 text-[10px] text-muted-foreground">
<div className="truncate">Location: <span className="text-foreground font-medium">{locationName}</span></div> <div className="truncate">Chainage: <span className="text-foreground font-medium">{chainageName}</span></div>
<div>Confidence: <span className="text-foreground font-medium">{(detection.confidence * 100).toFixed(1)}%</span></div> <div>Confidence: <span className="text-foreground font-medium">{(detection.confidence * 100).toFixed(1)}%</span></div>
<div className="col-span-2 font-mono bg-muted/50 p-1 rounded border"> <div className="col-span-2 font-mono bg-muted/50 p-1 rounded border">
GPS: {detection.latitude}, {detection.longitude} GPS: {detection.latitude}, {detection.longitude}
@@ -512,7 +505,7 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection
useEffect(() => { useEffect(() => {
const video = videoRef.current const video = videoRef.current
if (!video) return if (!video) return
const videoUrl = videoFile ? URL.createObjectURL(videoFile) : (videoId ? `${API_URL}/video/${videoId}` : '') const videoUrl = videoFile ? URL.createObjectURL(videoFile) : (videoId ? `${process.env.NEXT_PUBLIC_API_URL}/video/${videoId}` : '')
if (videoUrl) { if (videoUrl) {
video.src = videoUrl video.src = videoUrl
video.onloadeddata = resizeCanvas video.onloadeddata = resizeCanvas

View File

@@ -1,365 +0,0 @@
/**
* API Service Layer for VisionRoad Frontend
* Provides typed functions for interacting with the backend API
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL
import {
Project, ProjectCreate, ProjectUpdate,
Package, PackageCreate, PackageUpdate,
Location, LocationCreate, LocationUpdate,
PaginatedResponse, PaginationParams,
Video, VideoResultData,
Detection,
SessionContext, emptySessionContext
} from "@/types"
// Helper function for GET API requests
async function apiRequest<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_URL}${endpoint}`, {
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "true"
}
})
if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`)
}
return response.json()
}
// Helper function for POST API requests
async function apiPostRequest<T>(endpoint: string, body: unknown): Promise<T> {
const response = await fetch(`${API_URL}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "true"
},
body: JSON.stringify(body)
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`API Error: ${response.status} - ${errorText}`)
}
return response.json()
}
// Helper function for PUT API requests
async function apiPutRequest<T>(endpoint: string, body: unknown): Promise<T> {
const response = await fetch(`${API_URL}${endpoint}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "true"
},
body: JSON.stringify(body)
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`API Error: ${response.status} - ${errorText}`)
}
return response.json()
}
// Helper function for DELETE API requests
async function apiDeleteRequest<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_URL}${endpoint}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "true"
}
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`API Error: ${response.status} - ${errorText}`)
}
if (response.status === 204) {
return { message: "Deleted successfully" } as unknown as T
}
return response.json()
}
/**
* Create a new project
*/
export async function createProject(data: ProjectCreate): Promise<Project> {
return apiPostRequest<Project>("/projects/", data)
}
/**
* Create a new package in a project
*/
export async function createPackage(data: PackageCreate): Promise<Package> {
return apiPostRequest<Package>("/packages/", data)
}
/**
* Create a new location in a package
*/
export async function createLocation(data: LocationCreate): Promise<Location> {
return apiPostRequest<Location>("/locations/", data)
}
/**
* Update an existing project
*/
export async function updateProject(projectId: string, data: ProjectUpdate): Promise<Project> {
return apiPutRequest<Project>(`/projects/${projectId}`, data)
}
/**
* Delete a project
*/
export async function deleteProject(projectId: string): Promise<{ message: string }> {
return apiDeleteRequest<{ message: string }>(`/projects/${projectId}`)
}
/**
* Update an existing package
*/
export async function updatePackage(packageId: string, data: PackageUpdate): Promise<Package> {
return apiPutRequest<Package>(`/packages/${packageId}`, data)
}
/**
* Delete a package
*/
export async function deletePackage(packageId: string): Promise<{ message: string }> {
return apiDeleteRequest<{ message: string }>(`/packages/${packageId}`)
}
/**
* Update an existing location
*/
export async function updateLocation(locationId: string, data: LocationUpdate): Promise<Location> {
return apiPutRequest<Location>(`/locations/${locationId}`, data)
}
/**
* Delete a location
*/
export async function deleteLocation(locationId: string): Promise<{ message: string }> {
return apiDeleteRequest<{ message: string }>(`/locations/${locationId}`)
}
// API Functions
/**
* Fetch all projects
*/
export async function fetchProjects(params?: PaginationParams): Promise<PaginatedResponse<Project>> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<PaginatedResponse<Project>>(`/projects/?skip=${skip}&limit=${limit}`)
}
/**
* Fetch packages filtered by project ID
*/
export async function fetchPackagesByProject(projectId: string, params?: PaginationParams): Promise<PaginatedResponse<Package>> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<PaginatedResponse<Package>>(`/packages/?project_id=${projectId}&skip=${skip}&limit=${limit}`)
}
/**
* Fetch locations filtered by package ID
*/
export async function fetchLocationsByPackage(packageId: string, params?: PaginationParams): Promise<PaginatedResponse<Location>> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<PaginatedResponse<Location>>(`/locations/?package_id=${packageId}&skip=${skip}&limit=${limit}`)
}
/**
* Fetch all packages (for dashboard)
*/
export async function fetchAllPackages(params?: PaginationParams): Promise<PaginatedResponse<Package>> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<PaginatedResponse<Package>>(`/packages/?skip=${skip}&limit=${limit}`)
}
/**
* Fetch all locations (for dashboard)
*/
export async function fetchAllLocations(params?: PaginationParams): Promise<PaginatedResponse<Location>> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<PaginatedResponse<Location>>(`/locations/?skip=${skip}&limit=${limit}`)
}
// Video functions
/**
* Fetch all videos (for dashboard)
*/
export async function fetchVideos(params?: PaginationParams): Promise<Video[]> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
const response = await apiRequest<{
videos: Array<{
video_id: string;
status: string;
progress: number;
summary?: {
unique_defected_sign_board?: number;
unique_pothole?: number;
unique_road_crack?: number;
unique_damaged_road_marking?: number;
unique_good_sign_board?: number;
total_road_damage?: number;
total_detections?: number;
}
}>
}>(`/videos?skip=${skip}&limit=${limit}`)
// Transform the response to match our Video interface
return response.videos.map(v => ({
id: v.video_id,
filename: v.video_id,
detection_type: "pot-sign-detection" as const,
status: v.status as Video["status"],
unique_defected_sign_board: v.summary?.unique_defected_sign_board,
unique_pothole: v.summary?.unique_pothole,
unique_road_crack: v.summary?.unique_road_crack,
unique_damaged_road_marking: v.summary?.unique_damaged_road_marking,
unique_good_sign_board: v.summary?.unique_good_sign_board,
total_road_damage: v.summary?.total_road_damage,
total_detections: v.summary?.total_detections,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}))
}
// Detection specific functions
/**
* Fetch all detections from completed videos (for dashboard map)
* Uses the summary endpoint to get detections for each project
*/
export async function fetchAllDetections(): Promise<Detection[]> {
try {
// First get all projects
const projects = (await fetchProjects()).items
// Then fetch detections for each project
const allDetections: Detection[] = []
for (const project of projects) {
try {
const summary = await apiRequest<{
packages: {
[key: string]: {
locations: {
[key: string]: {
detections: Detection[]
}
}
}
}
}>(`/summary/projects/${project.id}`)
// Extract detections from the nested structure
for (const pkg of Object.values(summary.packages || {})) {
for (const loc of Object.values(pkg.locations || {})) {
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 []
}
}
/**
* Session storage utility functions
*/
// Session Storage Keys
const SESSION_KEY = "visionroad_session"
const VIDEO_DATA_KEY = "visionroad_video_data"
const DETECTION_TYPE_KEY = "visionroad_detection_type"
/**
* Save session to sessionStorage
*/
export function saveSession(session: SessionContext): void {
if (typeof window !== "undefined") {
localStorage.setItem(SESSION_KEY, JSON.stringify(session))
}
}
/**
* Load session from sessionStorage
*/
export function loadSession(): SessionContext {
if (typeof window !== "undefined") {
const stored = localStorage.getItem(SESSION_KEY)
if (stored) {
return JSON.parse(stored)
}
}
return emptySessionContext
}
/**
* Clear all session data
*/
export function clearSession(): void {
if (typeof window !== "undefined") {
localStorage.removeItem(SESSION_KEY)
localStorage.removeItem(VIDEO_DATA_KEY)
localStorage.removeItem(DETECTION_TYPE_KEY)
}
}
/**
* Save video result data
*/
export function saveVideoData(data: VideoResultData): void {
if (typeof window !== "undefined") {
localStorage.setItem(VIDEO_DATA_KEY, JSON.stringify(data))
}
}
/**
* Load video result data
*/
export function loadVideoData(): VideoResultData | null {
if (typeof window !== "undefined") {
const stored = localStorage.getItem(VIDEO_DATA_KEY)
if (stored) {
return JSON.parse(stored)
}
}
return null
}
/**
* Check if session is complete
*/
export function isSessionValid(session: SessionContext): boolean {
return !!(session.projectId && session.packageId && session.locationId)
}

View File

@@ -1,49 +0,0 @@
"use client"
import {
type Project,
type Package,
type Location,
type Video,
type Detection
} from "./api"
/**
* Service to handle data extraction from project summaries
*/
export const projectDataService = {
/**
* Extracts all detections from a project summary, optionally filtered by package and location
*/
extractDetections(
projectSummary: any,
selectedPackageId?: string | null,
selectedLocationId?: string | null
): Detection[] {
if (!projectSummary) return []
const detections: Detection[] = []
const packagesToProcess = selectedPackageId && selectedPackageId !== "all"
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
: projectSummary.packages || {}
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
const locationsToProcess = selectedLocationId && selectedLocationId !== "all"
? { [selectedLocationId]: (pkg as any).locations[selectedLocationId] }
: (pkg as any).locations || {}
for (const [locName, loc] of Object.entries(locationsToProcess)) {
if (!loc) continue
const locationDetections = (loc as any).detections || []
detections.push(...locationDetections)
}
}
return detections
}
}
/**
* API service for project and video data
*/
export * from "./api"

View File

@@ -0,0 +1,58 @@
import axiosClient from "../axios/axios";
import {
Chainage, ChainageCreate, ChainageUpdate,
PaginatedResponse, PaginationParams
} from "@/types";
/**
* Chainage Service
*/
export const chainageService = {
/**
* Fetch all chainages
*/
getChainages: async (params?: PaginationParams): Promise<PaginatedResponse<Chainage>> => {
const skip = params?.skip ?? 0;
const limit = params?.limit ?? 100;
const response = await axiosClient.get<PaginatedResponse<Chainage>>(`/chainages/`, {
params: { skip, limit }
});
return response.data;
},
/**
* Fetch chainages filtered by package ID
*/
getChainagesByPackage: async (packageId: string, params?: PaginationParams): Promise<PaginatedResponse<Chainage>> => {
const skip = params?.skip ?? 0;
const limit = params?.limit ?? 100;
const response = await axiosClient.get<PaginatedResponse<Chainage>>(`/chainages/`, {
params: { package_id: packageId, skip, limit }
});
return response.data;
},
/**
* Create a new chainage
*/
createChainage: async (data: ChainageCreate): Promise<Chainage> => {
const response = await axiosClient.post<Chainage>("/chainages/", data);
return response.data;
},
/**
* Update an existing chainage
*/
updateChainage: async (chainageId: string, data: ChainageUpdate): Promise<Chainage> => {
const response = await axiosClient.put<Chainage>(`/chainages/${chainageId}`, data);
return response.data;
},
/**
* Delete a chainage
*/
deleteChainage: async (chainageId: string): Promise<{ message: string }> => {
const response = await axiosClient.delete<{ message: string }>(`/chainages/${chainageId}`);
return response.data;
}
};

View File

@@ -0,0 +1,56 @@
import axiosClient from "../axios/axios";
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<Detection[]> => {
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[]
}
}
}
}
}>(`/summary/projects/${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 []
}
}
};

View File

@@ -0,0 +1,7 @@
export * from "./project.service";
export * from "./package.service";
export { chainageService } from "./chainage.service";
export * from "./video.service";
export * from "./detection.service";
export * from "./session.service";
export { projectDataService } from "./project.service";

View File

@@ -0,0 +1,58 @@
import axiosClient from "../axios/axios";
import {
Package, PackageCreate, PackageUpdate,
PaginatedResponse, PaginationParams
} from "@/types";
/**
* Package Service
*/
export const packageService = {
/**
* Fetch all packages
*/
getPackages: async (params?: PaginationParams): Promise<PaginatedResponse<Package>> => {
const skip = params?.skip ?? 0;
const limit = params?.limit ?? 100;
const response = await axiosClient.get<PaginatedResponse<Package>>(`/packages/`, {
params: { skip, limit }
});
return response.data;
},
/**
* Fetch packages filtered by project ID
*/
getPackagesByProject: async (projectId: string, params?: PaginationParams): Promise<PaginatedResponse<Package>> => {
const skip = params?.skip ?? 0;
const limit = params?.limit ?? 100;
const response = await axiosClient.get<PaginatedResponse<Package>>(`/packages/`, {
params: { project_id: projectId, skip, limit }
});
return response.data;
},
/**
* Create a new package
*/
createPackage: async (data: PackageCreate): Promise<Package> => {
const response = await axiosClient.post<Package>("/packages/", data);
return response.data;
},
/**
* Update an existing package
*/
updatePackage: async (packageId: string, data: PackageUpdate): Promise<Package> => {
const response = await axiosClient.put<Package>(`/packages/${packageId}`, data);
return response.data;
},
/**
* Delete a package
*/
deletePackage: async (packageId: string): Promise<{ message: string }> => {
const response = await axiosClient.delete<{ message: string }>(`/packages/${packageId}`);
return response.data;
}
};

View File

@@ -0,0 +1,100 @@
import axiosClient from "../axios/axios";
import {
Project, ProjectCreate, ProjectUpdate,
PaginatedResponse, PaginationParams
} from "@/types";
/**
* Project Service
*/
export const projectService = {
/**
* Fetch all projects
*/
getProjects: async (params?: PaginationParams): Promise<PaginatedResponse<Project>> => {
const skip = params?.skip ?? 0;
const limit = params?.limit ?? 100;
const response = await axiosClient.get<PaginatedResponse<Project>>(`/projects/`, {
params: { skip, limit }
});
return response.data;
},
/**
* Create a new project
*/
createProject: async (data: ProjectCreate): Promise<Project> => {
const response = await axiosClient.post<Project>("/projects/", data);
return response.data;
},
/**
* Update an existing project
*/
updateProject: async (projectId: string, data: ProjectUpdate): Promise<Project> => {
const response = await axiosClient.put<Project>(`/projects/${projectId}`, data);
return response.data;
},
/**
* Delete a project
*/
deleteProject: async (projectId: string): Promise<{ message: string }> => {
const response = await axiosClient.delete<{ message: string }>(`/projects/${projectId}`);
return response.data;
},
/**
* Fetch project summary (detections across packages and chainages)
*/
getProjectSummary: async (projectId: string): Promise<any> => {
const response = await axiosClient.get(`/summary/projects/${projectId}`);
return response.data;
},
/**
* Fetch project summary filtered by video ID
*/
getProjectSummaryByVideo: async (projectId: string, videoId: string): Promise<any> => {
const response = await axiosClient.get(`/summary/projects/${projectId}`, {
params: { video_id: videoId }
});
return response.data;
}
};
/**
* Service to handle data extraction from project summaries
* Moved from legacy project-service.ts
*/
export const projectDataService = {
/**
* Extracts all detections from a project summary, optionally filtered by package and chainage
*/
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 [pkgName, pkg] of Object.entries(packagesToProcess)) {
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 || []
detections.push(...chainageDetections)
}
}
return detections
}
};

View File

@@ -0,0 +1,73 @@
import { SessionContext, VideoResultData, emptySessionContext } from "@/types";
// Session Storage Keys
const SESSION_KEY = "visionroad_session";
const VIDEO_DATA_KEY = "visionroad_video_data";
const DETECTION_TYPE_KEY = "visionroad_detection_type";
/**
* Session Service
*/
export const sessionService = {
/**
* Save session to localStorage
*/
saveSession: (session: SessionContext): void => {
if (typeof window !== "undefined") {
localStorage.setItem(SESSION_KEY, JSON.stringify(session))
}
},
/**
* Load session from localStorage
*/
loadSession: (): SessionContext => {
if (typeof window !== "undefined") {
const stored = localStorage.getItem(SESSION_KEY)
if (stored) {
return JSON.parse(stored)
}
}
return emptySessionContext;
},
/**
* Clear all session data
*/
clearSession: (): void => {
if (typeof window !== "undefined") {
localStorage.removeItem(SESSION_KEY)
localStorage.removeItem(VIDEO_DATA_KEY)
localStorage.removeItem(DETECTION_TYPE_KEY)
}
},
/**
* Save video result data
*/
saveVideoData: (data: VideoResultData): void => {
if (typeof window !== "undefined") {
localStorage.setItem(VIDEO_DATA_KEY, JSON.stringify(data))
}
},
/**
* Load video result data
*/
loadVideoData: (): VideoResultData | null => {
if (typeof window !== "undefined") {
const stored = localStorage.getItem(VIDEO_DATA_KEY)
if (stored) {
return JSON.parse(stored)
}
}
return null;
},
/**
* Check if session is complete
*/
isSessionValid: (session: SessionContext): boolean => {
return !!(session.projectId && session.packageId && session.chainageId)
}
};

View File

@@ -0,0 +1,79 @@
import axiosClient from "../axios/axios";
import { Video, PaginationParams } from "@/types";
/**
* Video Service
*/
export const videoService = {
/**
* Fetch all videos and transform them to match the Video interface
*/
getVideos: async (params?: PaginationParams): Promise<Video[]> => {
const skip = params?.skip ?? 0;
const limit = params?.limit ?? 100;
const response = await axiosClient.get<{
videos: Array<{
video_id: string;
status: string;
progress: number;
summary?: {
unique_defected_sign_board?: number;
unique_pothole?: number;
unique_road_crack?: number;
unique_damaged_road_marking?: number;
unique_good_sign_board?: number;
total_road_damage?: number;
total_detections?: number;
};
}>;
}>(`/videos`, {
params: { skip, limit }
});
// Transform the response to match our Video interface
return response.data.videos.map(v => ({
id: v.video_id,
filename: v.video_id,
detection_type: "pot-sign-detection" as const,
status: v.status as Video["status"],
unique_defected_sign_board: v.summary?.unique_defected_sign_board,
unique_pothole: v.summary?.unique_pothole,
unique_road_crack: v.summary?.unique_road_crack,
unique_damaged_road_marking: v.summary?.unique_damaged_road_marking,
unique_good_sign_board: v.summary?.unique_good_sign_board,
total_road_damage: v.summary?.total_road_damage,
total_detections: v.summary?.total_detections,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}));
},
/**
* Upload a video for processing
*/
uploadVideo: async (formData: FormData): Promise<any> => {
const response = await axiosClient.post("/upload", formData, {
headers: {
"Content-Type": "multipart/form-data"
}
});
return response.data;
},
/**
* Get processing status of a video
*/
getVideoStatus: async (videoId: string): Promise<any> => {
const response = await axiosClient.get(`/status/${videoId}`);
return response.data;
},
/**
* Get analysis results for a video
*/
getVideoResults: async (videoId: string): Promise<any> => {
const response = await axiosClient.get(`/results/${videoId}`);
return response.data;
}
};

View File

@@ -1,37 +1,40 @@
/** /**
* Location related types * Chainage related types
*/ */
export interface Location { export interface Chainage {
id: string id: string
package_id: string package_id: string
segment_name: string segment_name: string
chainage_start_km: number | null chainage_start_km: number
chainage_end_km: number | null chainage_end_km: number
start_lat: number start_lat: number
start_lng: number start_lng: number
end_lat: number end_lat: number
end_lng: number end_lng: number
direction: 'UP' | 'DOWN'
created_at: string created_at: string
updated_at: string updated_at: string
} }
export interface LocationCreate { export interface ChainageCreate {
package_id: string package_id: string
segment_name: string segment_name: string
chainage_start_km?: number | null chainage_start_km: number
chainage_end_km?: number | null chainage_end_km: number
start_lat: number start_lat: number
start_lng: number start_lng: number
end_lat: number end_lat: number
end_lng: number end_lng: number
direction: 'UP' | 'DOWN'
} }
export interface LocationUpdate { export interface ChainageUpdate {
segment_name?: string segment_name?: string
chainage_start_km?: number | null chainage_start_km?: number
chainage_end_km?: number | null chainage_end_km?: number
start_lat?: number start_lat?: number
start_lng?: number start_lng?: number
end_lat?: number end_lat?: number
end_lng?: number end_lng?: number
direction?: 'UP' | 'DOWN'
} }

View File

@@ -1,7 +1,7 @@
export * from "./common" export * from "./common"
export * from "./project" export * from "./project"
export * from "./package" export * from "./package"
export * from "./location" export * from "./chainage"
export * from "./video" export * from "./video"
export * from "./detection" export * from "./detection"
export * from "./analysis" export * from "./analysis"

View File

@@ -6,6 +6,8 @@ export interface Package {
project_id: string project_id: string
name: string name: string
region: string | null region: string | null
chainage_start_km: number
chainage_end_km: number
created_at: string created_at: string
updated_at: string updated_at: string
} }
@@ -14,9 +16,13 @@ export interface PackageCreate {
project_id: string project_id: string
name: string name: string
region?: string | null region?: string | null
chainage_start_km?: number
chainage_end_km?: number
} }
export interface PackageUpdate { export interface PackageUpdate {
name?: string name?: string
region?: string | null region?: string | null
chainage_start_km?: number
chainage_end_km?: number
} }

View File

@@ -6,8 +6,8 @@ export interface SessionContext {
projectName: string | null projectName: string | null
packageId: string | null packageId: string | null
packageName: string | null packageName: string | null
locationId: string | null chainageId: string | null
locationName: string | null chainageName: string | null
} }
export const emptySessionContext: SessionContext = { export const emptySessionContext: SessionContext = {
@@ -15,6 +15,6 @@ export const emptySessionContext: SessionContext = {
projectName: null, projectName: null,
packageId: null, packageId: null,
packageName: null, packageName: null,
locationId: null, chainageId: null,
locationName: null chainageName: null
} }

View File

@@ -2,7 +2,7 @@ export const ROUTES = {
DASHBOARD: "/dashboard", DASHBOARD: "/dashboard",
PROJECT: "/project", PROJECT: "/project",
PACKAGE: "/package", PACKAGE: "/package",
LOCATION: "/location", CHAINAGE: "/chainage",
ACCOUNT: "/account", ACCOUNT: "/account",
NEW_ANALYSIS: "/new-analysis", NEW_ANALYSIS: "/new-analysis",
UPLOAD: "/upload", UPLOAD: "/upload",