feat(api): restructure api with axios and chainage related changes
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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.`,
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user