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

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

View File

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

View File

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

View File

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

View File

@@ -10,13 +10,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
import { Loader2, CheckCircle2, Package, FolderKanban, Globe } from "lucide-react"
import { DataTable } from "@/components/data-table"
import { PageHeader } from "@/components/page-header"
import {
fetchProjects,
fetchAllPackages,
createPackage,
updatePackage,
deletePackage
} from "@/lib/api"
import { projectService, packageService } from "@/services/api"
import {
Project,
Package as PackageType,
@@ -51,13 +45,15 @@ export default function PackagePage() {
const [selectedProjectId, setSelectedProjectId] = useState("")
const [name, setName] = useState("")
const [region, setRegion] = useState("")
const [chainageStartKm, setChainageStartKm] = useState<string>("")
const [chainageEndKm, setChainageEndKm] = useState<string>("")
// Load packages and projects
const loadPackages = async (currentSkip = skip, currentLimit = limit) => {
try {
setIsLoading(true)
setError(null)
const data = await fetchAllPackages({ skip: currentSkip, limit: currentLimit })
const data = await packageService.getPackages({ skip: currentSkip, limit: currentLimit })
setPackages(data.items)
setTotalItems(data.totalItems)
} catch (err) {
@@ -73,7 +69,7 @@ export default function PackagePage() {
const loadProjects = async () => {
try {
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)
} catch (err) {
setError("Failed to load projects.")
@@ -91,6 +87,8 @@ export default function PackagePage() {
setSelectedProjectId("")
setName("")
setRegion("")
setChainageStartKm("")
setChainageEndKm("")
setError(null)
setIsEditing(false)
setCurrentPackage(null)
@@ -115,8 +113,10 @@ export default function PackagePage() {
const data: PackageUpdate = {
name: name.trim(),
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", {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
})
@@ -125,8 +125,10 @@ export default function PackagePage() {
project_id: selectedProjectId,
name: name.trim(),
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", {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
})
@@ -155,6 +157,8 @@ export default function PackagePage() {
setSelectedProjectId(pkg.project_id)
setName(pkg.name || "")
setRegion(pkg.region || "")
setChainageStartKm(pkg.chainage_start_km?.toString() || "0")
setChainageEndKm(pkg.chainage_end_km?.toString() || "0")
setIsModalOpen(true)
}
@@ -163,7 +167,7 @@ export default function PackagePage() {
try {
setIsLoading(true)
await deletePackage(pkg.id)
await packageService.deletePackage(pkg.id)
toast.success("Package Deleted", {
description: `${pkg.name} has been removed from the system.`,
})
@@ -242,6 +246,16 @@ export default function PackagePage() {
accessorKey: "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 (
@@ -383,6 +397,36 @@ export default function PackagePage() {
className="h-11 bg-muted/20 border-border/60"
/>
</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>
@@ -396,14 +440,14 @@ export default function PackagePage() {
resetForm()
}}
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
</Button>
<Button
type="submit"
disabled={isSubmitting || !name.trim() || !selectedProjectId}
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs"
className="flex-1"
>
{isSubmitting ? (
<div className="flex items-center gap-2">

View File

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

View File

@@ -7,10 +7,7 @@ import { Loader2, TrendingUp } from "lucide-react"
import VideoPlayerSection from "@/components/video-player-section"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import {
loadSession,
clearSession
} from "@/lib/api"
import { sessionService, videoService } from "@/services/api"
import { SessionContext, DetectionData, DetectionType } from "@/types"
import { getVideoFile, clearVideoFile } from "@/lib/video-storage"
import { ROUTES } from "@/utils/routes"
@@ -29,24 +26,13 @@ export default function VideoResultsPage() {
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const storedSession = loadSession()
const storedSession = sessionService.loadSession()
setSession(storedSession)
const fetchResults = async () => {
try {
// Fetch detection data from backend
const response = await fetch(`${API_URL}/results/${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()
const data = await videoService.getVideoResults(videoId);
setDetectionData(data as any)
// 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)
}
}
clearSession()
sessionService.clearSession()
router.push(ROUTES.NEW_ANALYSIS)
}
@@ -146,9 +132,9 @@ export default function VideoResultsPage() {
</span>
</div>
<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">
{session.locationName}
{session.chainageName}
</span>
</div>
</div>

View File

@@ -7,12 +7,7 @@ import { Loader2, TrendingUp } from "lucide-react"
import VideoPlayerSection from "@/components/video-player-section"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import {
loadSession,
loadVideoData,
isSessionValid,
clearSession
} from "@/lib/api"
import { sessionService } from "@/services/api"
import { SessionContext, DetectionData, DetectionType } from "@/types"
import { clearVideoFile } from "@/lib/video-storage"
import { ROUTES } from "@/utils/routes"
@@ -30,10 +25,10 @@ export default function ResultsPage() {
// Load session and video data on mount
useEffect(() => {
const storedSession = loadSession()
const videoData = loadVideoData()
const storedSession = sessionService.loadSession()
const videoData = sessionService.loadVideoData()
if (!isSessionValid(storedSession) || !videoData) {
if (!sessionService.isSessionValid(storedSession) || !videoData) {
router.replace(ROUTES.NEW_ANALYSIS)
return
}
@@ -56,7 +51,7 @@ export default function ResultsPage() {
console.error("Failed to clear video file:", err)
}
}
clearSession()
sessionService.clearSession()
router.push(ROUTES.NEW_ANALYSIS)
}
@@ -118,9 +113,9 @@ export default function ResultsPage() {
</span>
</div>
<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">
{session.locationName}
{session.chainageName}
</span>
</div>
</div>

View File

@@ -6,9 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader2, TrendingUp } from "lucide-react"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import {
loadSession,
} from "@/lib/api"
import { sessionService, videoService } from "@/services/api"
import { SessionContext } from "@/types"
import { ROUTES } from "@/utils/routes"
import { Button } from "@/components/ui/button"
@@ -68,25 +66,12 @@ export default function VideoProcessingPage() {
}, [router])
useEffect(() => {
const storedSession = loadSession()
const storedSession = sessionService.loadSession()
setSession(storedSession)
const checkStatus = async () => {
try {
const response = await fetch(`${API_URL}/status/${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()
const statusData = await videoService.getVideoStatus(videoId);
if (statusData.status === "completed") {
router.replace(`/results/${videoId}`)
@@ -157,9 +142,9 @@ export default function VideoProcessingPage() {
</span>
</div>
<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">
{session.locationName}
{session.chainageName}
</span>
</div>
</div>

View File

@@ -10,12 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Loader2, TrendingUp } from "lucide-react"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import {
loadSession,
isSessionValid,
saveVideoData,
clearSession
} from "@/lib/api"
import { sessionService, videoService } from "@/services/api"
import { SessionContext } from "@/types"
import { ROUTES } from "@/utils/routes"
import { storeVideoFile } from "@/lib/video-storage"
@@ -59,8 +54,8 @@ export default function UploadPage() {
// Load session on mount
useEffect(() => {
const storedSession = loadSession()
if (!isSessionValid(storedSession)) {
const storedSession = sessionService.loadSession()
if (!sessionService.isSessionValid(storedSession)) {
router.replace(ROUTES.NEW_ANALYSIS)
return
}
@@ -89,18 +84,7 @@ export default function UploadPage() {
setError(null)
try {
const response = await fetch(`${API_URL}/upload`, {
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()
const result = await videoService.uploadVideo(formData);
// Store file locally for potential recovery/results display
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
router.push(`/upload/${result.video_id}`)
@@ -131,7 +115,7 @@ export default function UploadPage() {
}
const handleBackToSelection = () => {
clearSession()
sessionService.clearSession()
router.push(ROUTES.NEW_ANALYSIS)
}
@@ -184,9 +168,9 @@ export default function UploadPage() {
</span>
</div>
<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">
{session.locationName}
{session.chainageName}
</span>
</div>
</div>