feat(api): restructure api with axios and chainage related changes
This commit is contained in:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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.`,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
365
src/lib/api.ts
365
src/lib/api.ts
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
58
src/services/api/chainage.service.ts
Normal file
58
src/services/api/chainage.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
56
src/services/api/detection.service.ts
Normal file
56
src/services/api/detection.service.ts
Normal 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 []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
7
src/services/api/index.ts
Normal file
7
src/services/api/index.ts
Normal 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";
|
||||||
58
src/services/api/package.service.ts
Normal file
58
src/services/api/package.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
100
src/services/api/project.service.ts
Normal file
100
src/services/api/project.service.ts
Normal 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
|
||||||
|
}
|
||||||
|
};
|
||||||
73
src/services/api/session.service.ts
Normal file
73
src/services/api/session.service.ts
Normal 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)
|
||||||
|
}
|
||||||
|
};
|
||||||
79
src/services/api/video.service.ts
Normal file
79
src/services/api/video.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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'
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user