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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from "react"
import dynamic from "next/dynamic"
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"
// Dynamically import the map to avoid SSR issues with Leaflet
@@ -23,7 +23,7 @@ interface DashboardMapProps {
className?: string
selectedProjectId?: string | null
selectedPackageId?: string | null
selectedLocationId?: string | null
selectedChainageId?: string | null
projectSummary?: any
}
@@ -31,7 +31,7 @@ export function DashboardMap({
className,
selectedProjectId,
selectedPackageId,
selectedLocationId,
selectedChainageId,
projectSummary
}: DashboardMapProps) {
const [detections, setDetections] = useState<Detection[]>([])
@@ -56,14 +56,14 @@ export function DashboardMap({
: 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 || {}
const chainagesToProcess = selectedChainageId && selectedChainageId !== "all"
? { [selectedChainageId]: (pkg as any).chainages[selectedChainageId] }
: (pkg as any).chainages || {}
for (const [locName, loc] of Object.entries(locationsToProcess)) {
if (!loc) continue
const locationDetections = (loc as any).detections || []
filteredDetections.push(...locationDetections)
for (const [chnName, chn] of Object.entries(chainagesToProcess)) {
if (!chn) continue
const chainageDetections = (chn as any).detections || []
filteredDetections.push(...chainageDetections)
}
}
@@ -74,7 +74,7 @@ export function DashboardMap({
} finally {
setIsLoading(false)
}
}, [projectSummary, selectedPackageId, selectedLocationId])
}, [projectSummary, selectedPackageId, selectedChainageId])
// Filter detections with valid GPS coordinates
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 gap-3">
<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>
<CardTitle className="text-base font-bold">

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Upload, Loader2, AlertCircle } from "lucide-react"
import type { DetectionData, DetectionType } from "@/types"
import { videoService } from "@/services/api"
const API_URL = process.env.NEXT_PUBLIC_API_URL
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) => {
try {
console.log("[Upload] Loading results for:", videoId)
const response = await fetch(`${API_URL}/results/${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()
const detectionData: DetectionData = await videoService.getVideoResults(videoId);
console.log("[Upload] Results loaded:", detectionData)
setUploading(false)
@@ -162,29 +153,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
setError(null)
try {
console.log("[Upload] Uploading to:", `${API_URL}/upload`)
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 result = await videoService.uploadVideo(formData);
const videoId = result.video_id
console.log("[Upload] Video uploaded successfully, ID:", videoId)
@@ -200,7 +169,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
let errorMessage = "Upload failed"
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) {
errorMessage = error.message
}

View File

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