diff --git a/app/create-location/page.tsx b/app/create-location/page.tsx new file mode 100644 index 0000000..7ca5669 --- /dev/null +++ b/app/create-location/page.tsx @@ -0,0 +1,543 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +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 { SidebarNavigation } from "@/components/sidebar-navigation" +import { DataTable } from "@/components/data-table" +import { + fetchProjects, + fetchPackagesByProject, + fetchAllLocations, + fetchAllPackages, + createLocation, + type Project, + type Package as PackageType, + type Location, + type LocationCreate +} from "@/lib/api" + +export default function CreateLocationPage() { + const [locations, setLocations] = useState([]) + const [projects, setProjects] = useState([]) + const [packages, setPackages] = useState([]) + const [allPackages, setAllPackages] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [isModalOpen, setIsModalOpen] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [success, setSuccess] = useState(false) + const [error, setError] = useState(null) + const [loadingProjects, setLoadingProjects] = useState(false) + const [loadingPackages, setLoadingPackages] = useState(false) + + // Form fields + const [selectedProjectId, setSelectedProjectId] = useState("") + const [selectedPackageId, setSelectedPackageId] = useState("") + const [segmentName, setSegmentName] = useState("") + const [chainageStartKm, setChainageStartKm] = useState("") + const [chainageEndKm, setChainageEndKm] = useState("") + const [startLat, setStartLat] = useState("") + const [startLng, setStartLng] = useState("") + const [endLat, setEndLat] = useState("") + const [endLng, setEndLng] = useState("") + + // Load locations and projects + const loadLocations = async () => { + try { + setIsLoading(true) + setError(null) + const data = await fetchAllLocations() + setLocations(data) + } catch (err) { + setError("Failed to load locations. Please check if the backend is running.") + } finally { + setIsLoading(false) + } + } + + const loadProjects = async () => { + try { + setLoadingProjects(true) + const data = await fetchProjects() + setProjects(data) + } catch (err) { + setError("Failed to load projects.") + } finally { + setLoadingProjects(false) + } + } + + const loadAllPackages = async () => { + try { + const data = await fetchAllPackages() + setAllPackages(data) + } catch (err) { + console.error("Failed to load all packages") + } + } + + useEffect(() => { + loadLocations() + loadProjects() + loadAllPackages() + }, []) + + // Load packages when project changes + useEffect(() => { + if (!selectedProjectId) { + setPackages([]) + setSelectedPackageId("") + return + } + + const loadPackages = async () => { + try { + setLoadingPackages(true) + setSelectedPackageId("") + const data = await fetchPackagesByProject(selectedProjectId) + setPackages(data) + } catch (err) { + setError("Failed to load packages for the selected project.") + } finally { + setLoadingPackages(false) + } + } + loadPackages() + }, [selectedProjectId]) + + const resetForm = () => { + setSelectedProjectId("") + setSelectedPackageId("") + setSegmentName("") + setChainageStartKm("") + setChainageEndKm("") + setStartLat("") + setStartLng("") + setEndLat("") + setEndLng("") + setError(null) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!selectedPackageId) { + setError("Please select a project and package first") + return + } + if (!segmentName.trim()) { + setError("Segment name is required") + return + } + if (!startLat || !startLng || !endLat || !endLng) { + setError("All GPS coordinates are required for locations") + return + } + + setIsSubmitting(true) + setError(null) + + try { + const data: LocationCreate = { + package_id: selectedPackageId, + segment_name: segmentName.trim(), + chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : null, + chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : null, + start_lat: parseFloat(startLat), + start_lng: parseFloat(startLng), + end_lat: parseFloat(endLat), + end_lng: parseFloat(endLng), + } + + await createLocation(data) + setSuccess(true) + + // Refresh locations list + await loadLocations() + + setTimeout(() => { + resetForm() + setSuccess(false) + setIsModalOpen(false) + }, 2000) + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create location") + } finally { + setIsSubmitting(false) + } + } + + const getPackageName = (packageId: string) => { + return allPackages.find(p => p.id === packageId)?.name || packageId + } + + const selectedProject = projects.find(p => p.id === selectedProjectId) + const selectedPackage = packages.find(p => p.id === selectedPackageId) + const isFormComplete = selectedPackageId && segmentName.trim() && startLat && startLng && endLat && endLng + + const columns = [ + { + key: "segment_name", + header: "Segment Name", + render: (location: Location) => ( +
{location.segment_name}
+ ) + }, + { + key: "package_id", + header: "Package", + render: (location: Location) => getPackageName(location.package_id) + }, + { + key: "chainage", + header: "Chainage (km)", + render: (location: Location) => { + if (location.chainage_start_km && location.chainage_end_km) { + return `${location.chainage_start_km} - ${location.chainage_end_km}` + } + return "—" + } + }, + { + key: "created_at", + header: "Created", + render: (location: Location) => new Date(location.created_at).toLocaleDateString() + }, + ] + + return ( +
+ +
+
+ {/* Header */} +
+
+
+ +
+
+

+ Locations +

+

+ Manage road segment locations +

+
+
+
+ + {/* Error Message */} + {error && !isModalOpen && ( +
+
+ ! +
+

{error}

+
+ )} + + {/* Data Table */} +
+ setIsModalOpen(true)} + addButtonText="Add New Location" + isLoading={isLoading} + /> +
+ + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
+
+
+ + {/* Modal Dialog */} + + + + + + Create New Location + + + Select project & package, then fill in the location details + + + + {/* Success Message in Modal */} + {success && ( +
+ +

Location created successfully!

+
+ )} + + {/* Error Message in Modal */} + {error && ( +
+
+ ! +
+

{error}

+
+ )} + +
+ {/* Step 1: Select Project */} +
+
+
+ 1 +
+

Select Project

+
+ + {selectedProject && ( +
+ Selected: {selectedProject.name} +
+ )} +
+ + {/* Step 2: Select Package */} +
+
+
+ 2 +
+

Select Package

+
+ + {selectedPackage && ( +
+ Selected: {selectedPackage.name} + {selectedPackage.region && ` • ${selectedPackage.region}`} +
+ )} +
+ + {/* Step 3: Location Details */} +
+
+
+ 3 +
+

Location Information

+
+ + {/* Segment Name */} +
+ + setSegmentName(e.target.value)} + placeholder="e.g., KM 120 to KM 135" + className="h-11" + required + /> +
+ + {/* Chainage */} +
+
+ + setChainageStartKm(e.target.value)} + placeholder="0" + className="h-10 text-sm" + /> +
+
+ + setChainageEndKm(e.target.value)} + placeholder="0" + className="h-10 text-sm" + /> +
+
+ + {/* GPS Coordinates */} +
+
+
+ +
+

GPS Coordinates

+ Required +
+ + {/* Start Point */} +
+

Start Point

+
+
+ + setStartLat(e.target.value)} + placeholder="-90 to 90" + className="h-10 text-sm" + required + /> +
+
+ + setStartLng(e.target.value)} + placeholder="-180 to 180" + className="h-10 text-sm" + required + /> +
+
+
+ + {/* End Point */} +
+

End Point

+
+
+ + setEndLat(e.target.value)} + placeholder="-90 to 90" + className="h-10 text-sm" + required + /> +
+
+ + setEndLng(e.target.value)} + placeholder="-180 to 180" + className="h-10 text-sm" + required + /> +
+
+
+
+
+ + {/* Submit Button */} +
+ + +
+
+
+
+
+ ) +} diff --git a/app/create-package/page.tsx b/app/create-package/page.tsx new file mode 100644 index 0000000..a6c107a --- /dev/null +++ b/app/create-package/page.tsx @@ -0,0 +1,340 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +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, Package, FolderKanban, Globe } from "lucide-react" +import { SidebarNavigation } from "@/components/sidebar-navigation" +import { DataTable } from "@/components/data-table" +import { + fetchProjects, + fetchAllPackages, + createPackage, + type Project, + type Package as PackageType, + type PackageCreate +} from "@/lib/api" + +export default function CreatePackagePage() { + const [packages, setPackages] = useState([]) + const [projects, setProjects] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [isModalOpen, setIsModalOpen] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [success, setSuccess] = useState(false) + const [error, setError] = useState(null) + const [loadingProjects, setLoadingProjects] = useState(false) + + // Form fields + const [selectedProjectId, setSelectedProjectId] = useState("") + const [name, setName] = useState("") + const [region, setRegion] = useState("") + + // Load packages and projects + const loadPackages = async () => { + try { + setIsLoading(true) + setError(null) + const data = await fetchAllPackages() + setPackages(data) + } catch (err) { + setError("Failed to load packages. Please check if the backend is running.") + } finally { + setIsLoading(false) + } + } + + const loadProjects = async () => { + try { + setLoadingProjects(true) + const data = await fetchProjects() + setProjects(data) + } catch (err) { + setError("Failed to load projects.") + } finally { + setLoadingProjects(false) + } + } + + useEffect(() => { + loadPackages() + loadProjects() + }, []) + + const resetForm = () => { + setSelectedProjectId("") + setName("") + setRegion("") + setError(null) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!selectedProjectId) { + setError("Please select a project first") + return + } + if (!name.trim()) { + setError("Package name is required") + return + } + + setIsSubmitting(true) + setError(null) + + try { + const data: PackageCreate = { + project_id: selectedProjectId, + name: name.trim(), + region: region.trim() || null, + } + + await createPackage(data) + setSuccess(true) + + // Refresh packages list + await loadPackages() + + setTimeout(() => { + resetForm() + setSuccess(false) + setIsModalOpen(false) + }, 2000) + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create package") + } finally { + setIsSubmitting(false) + } + } + + const getProjectName = (projectId: string) => { + return projects.find(p => p.id === projectId)?.name || projectId + } + + const selectedProject = projects.find(p => p.id === selectedProjectId) + + const columns = [ + { + key: "name", + header: "Package Name", + render: (pkg: PackageType) => ( +
{pkg.name}
+ ) + }, + { + key: "project_id", + header: "Project", + render: (pkg: PackageType) => getProjectName(pkg.project_id) + }, + { + key: "region", + header: "Region", + }, + { + key: "created_at", + header: "Created", + render: (pkg: PackageType) => new Date(pkg.created_at).toLocaleDateString() + }, + ] + + return ( +
+ +
+
+ {/* Header */} +
+
+
+ +
+
+

+ Packages +

+

+ Manage project packages +

+
+
+
+ + {/* Error Message */} + {error && !isModalOpen && ( +
+
+ ! +
+

{error}

+
+ )} + + {/* Data Table */} +
+ setIsModalOpen(true)} + addButtonText="Add New Package" + isLoading={isLoading} + /> +
+ + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
+
+
+ + {/* Modal Dialog */} + + + + + + Create New Package + + + Select a project and fill in the package details + + + + {/* Success Message in Modal */} + {success && ( +
+ +

Package created successfully!

+
+ )} + + {/* Error Message in Modal */} + {error && ( +
+
+ ! +
+

{error}

+
+ )} + +
+ {/* Step 1: Select Project */} +
+
+
+ 1 +
+

Select Project

+
+ + {selectedProject && ( +
+ Selected: {selectedProject.name} + {selectedProject.corridor_name && ` • ${selectedProject.corridor_name}`} +
+ )} +
+ + {/* Step 2: Package Info */} +
+
+
+ 2 +
+

Package Information

+
+ +
+ + setName(e.target.value)} + placeholder="e.g., Package A - Section 1" + className="h-11" + required + /> +
+ +
+ + setRegion(e.target.value)} + placeholder="e.g., Delhi, Haryana, Punjab" + className="h-11" + /> +
+
+ + {/* Submit Button */} +
+ + +
+
+
+
+
+ ) +} diff --git a/app/create-project/page.tsx b/app/create-project/page.tsx new file mode 100644 index 0000000..1904525 --- /dev/null +++ b/app/create-project/page.tsx @@ -0,0 +1,376 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Loader2, CheckCircle2, FolderPlus, MapPin, Building2, Route, X } from "lucide-react" +import { SidebarNavigation } from "@/components/sidebar-navigation" +import { DataTable } from "@/components/data-table" +import { createProject, fetchProjects, type ProjectCreate, type Project } from "@/lib/api" + +export default function CreateProjectPage() { + const [projects, setProjects] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [isModalOpen, setIsModalOpen] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [success, setSuccess] = useState(false) + const [error, setError] = useState(null) + + // Form fields + const [name, setName] = useState("") + const [state, setState] = useState("") + const [corridorName, setCorridorName] = useState("") + const [startLat, setStartLat] = useState("") + const [startLng, setStartLng] = useState("") + const [endLat, setEndLat] = useState("") + const [endLng, setEndLng] = useState("") + + // Load projects + const loadProjects = async () => { + try { + setIsLoading(true) + setError(null) + const data = await fetchProjects() + setProjects(data) + } catch (err) { + setError("Failed to load projects. Please check if the backend is running.") + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + loadProjects() + }, []) + + const resetForm = () => { + setName("") + setState("") + setCorridorName("") + setStartLat("") + setStartLng("") + setEndLat("") + setEndLng("") + setError(null) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!name.trim()) { + setError("Project name is required") + return + } + + setIsSubmitting(true) + setError(null) + + try { + const data: ProjectCreate = { + name: name.trim(), + state: state.trim() || null, + corridor_name: corridorName.trim() || null, + start_lat: startLat ? parseFloat(startLat) : null, + start_lng: startLng ? parseFloat(startLng) : null, + end_lat: endLat ? parseFloat(endLat) : null, + end_lng: endLng ? parseFloat(endLng) : null, + } + + await createProject(data) + setSuccess(true) + + // Refresh projects list + await loadProjects() + + setTimeout(() => { + resetForm() + setSuccess(false) + setIsModalOpen(false) + }, 2000) + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create project") + } finally { + setIsSubmitting(false) + } + } + + const columns = [ + { + key: "name", + header: "Project Name", + render: (project: Project) => ( +
{project.name}
+ ) + }, + { + key: "state", + header: "State", + render: (project: Project) => { + if (!project.state) return + return ( +
+ {project.state.split(',').map((item, idx) => ( + + {item.trim()} + + ))} +
+ ) + } + }, + { + key: "corridor_name", + header: "Corridor", + }, + { + key: "created_at", + header: "Created", + render: (project: Project) => new Date(project.created_at).toLocaleDateString() + }, + ] + + return ( +
+ +
+
+ {/* Header */} +
+
+
+ +
+
+

+ Projects +

+

+ Manage road infrastructure projects +

+
+
+
+ + {/* Error Message */} + {error && !isModalOpen && ( +
+
+ ! +
+

{error}

+
+ )} + + {/* Data Table */} +
+ setIsModalOpen(true)} + addButtonText="Add New Project" + isLoading={isLoading} + /> +
+ + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
+
+
+ + {/* Modal Dialog */} + + + + + + Create New Project + + + Fill in the details for your new road infrastructure project + + + + {/* Success Message in Modal */} + {success && ( +
+ +

Project created successfully!

+
+ )} + + {/* Error Message in Modal */} + {error && ( +
+
+ ! +
+

{error}

+
+ )} + +
+ {/* Project Name */} +
+ + setName(e.target.value)} + placeholder="e.g., Delhi-Chandigarh Highway" + className="h-11" + required + /> +
+ + {/* State & Corridor Row */} +
+
+ + setState(e.target.value)} + placeholder="e.g., Haryana" + className="h-11" + /> +
+
+ + setCorridorName(e.target.value)} + placeholder="e.g., National Highway 44" + className="h-11" + /> +
+
+ + {/* GPS Coordinates Section */} +
+
+
+ +
+

GPS Coordinates

+ (Optional) +
+ + {/* Start Point */} +
+

Start Point

+
+
+ + setStartLat(e.target.value)} + placeholder="-90 to 90" + className="h-10 text-sm" + /> +
+
+ + setStartLng(e.target.value)} + placeholder="-180 to 180" + className="h-10 text-sm" + /> +
+
+
+ + {/* End Point */} +
+

End Point

+
+
+ + setEndLat(e.target.value)} + placeholder="-90 to 90" + className="h-10 text-sm" + /> +
+
+ + setEndLng(e.target.value)} + placeholder="-180 to 180" + className="h-10 text-sm" + /> +
+
+
+
+ + {/* Submit Button */} +
+ + +
+
+
+
+
+ ) +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 899a157..c02d274 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,12 +1,8 @@ "use client" import { useState, useEffect } from "react" -import { useRouter } from "next/navigation" -import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { - Plus, - Loader2, AlertCircle, TrendingUp, MapPin as MapPinIcon, @@ -17,6 +13,7 @@ import { import { SidebarNavigation } from "@/components/sidebar-navigation" import { GradientStatsCard } from "@/components/dashboard/gradient-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 { DashboardMap } from "@/components/dashboard/dashboard-map" @@ -114,7 +111,6 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats { } export default function DashboardPage() { - const router = useRouter() const [isLoading, setIsLoading] = useState(true) const [projects, setProjects] = useState([]) const [selectedProjectId, setSelectedProjectId] = useState(null) @@ -150,11 +146,31 @@ export default function DashboardPage() { loadProjects() }, []) + const selectedProject = projects.find(p => p.id === selectedProjectId) + + // Extract packages from project summary + const packages = projectSummary + ? Object.keys(projectSummary.packages || {}).map(pkgName => ({ + id: pkgName, + name: pkgName + })) + : [] + + // Extract locations from selected package + const [selectedPackageId, setSelectedPackageId] = useState(null) + const [selectedLocationId, setSelectedLocationId] = useState(null) + + const locations = projectSummary && selectedPackageId && selectedPackageId !== "all" + ? Object.keys(projectSummary.packages[selectedPackageId]?.locations || {}).map(locName => ({ + id: locName, + name: locName + })) + : [] + // Load project summary when project changes useEffect(() => { if (!selectedProjectId) { setProjectSummary(null) - setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] }) return } @@ -176,12 +192,10 @@ export default function DashboardPage() { const summary: ProjectSummary = await response.json() setProjectSummary(summary) - setStats(calculateStats(summary)) } catch (err) { console.error("Failed to load project summary:", err) setError("Failed to load project summary.") setProjectSummary(null) - setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] }) } finally { setIsLoading(false) } @@ -190,14 +204,73 @@ export default function DashboardPage() { loadProjectSummary() }, [selectedProjectId]) - const handleNewAnalysis = () => { - router.push("/") - } + // Reset package and location when project changes + useEffect(() => { + setSelectedPackageId(null) + setSelectedLocationId(null) + }, [selectedProjectId]) - const selectedProject = projects.find(p => p.id === selectedProjectId) + // Reset location when package changes + useEffect(() => { + setSelectedLocationId(null) + }, [selectedPackageId]) + + // Filter stats based on selections + useEffect(() => { + if (!projectSummary) { + setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] }) + return + } + + let totalPotholes = 0 + let totalSignboards = 0 + const locationData: DetectionStats["locationData"] = [] + + const packagesToProcess = selectedPackageId && selectedPackageId !== "all" + ? { [selectedPackageId]: projectSummary.packages[selectedPackageId] } + : projectSummary.packages || {} + + for (const [pkgName, pkg] of Object.entries(packagesToProcess)) { + const locationsToProcess = selectedLocationId && selectedLocationId !== "all" + ? { [selectedLocationId]: pkg.locations[selectedLocationId] } + : pkg.locations || {} + + for (const [locName, loc] of Object.entries(locationsToProcess)) { + let locPotholes = 0 + let locSignboards = 0 + + for (const detection of loc.detections || []) { + if (detection.type.toLowerCase().includes("pothole")) { + locPotholes++ + totalPotholes++ + } else { + locSignboards++ + totalSignboards++ + } + } + + if (locPotholes > 0 || locSignboards > 0) { + const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName + locationData.push({ + name: shortName, + potholes: locPotholes, + signboards: locSignboards, + total: locPotholes + locSignboards + }) + } + } + } + + setStats({ + totalPotholes, + totalSignboards, + totalDetections: totalPotholes + totalSignboards, + locationData + }) + }, [projectSummary, selectedPackageId, selectedLocationId]) return ( -
+
{/* Sidebar Navigation */} @@ -206,22 +279,13 @@ export default function DashboardPage() {
{/* Header */}
-
-
-

- VisionRoad Analytics Dashboard -

-

- A Comprehensive Overview Of Your Road Infrastructure Analysis -

-
- +
+

+ VisionRoad Analytics Dashboard +

+

+ A Comprehensive Overview Of Your Road Infrastructure Analysis +

@@ -261,67 +325,74 @@ export default function DashboardPage() { />
- {/* Project Selector - Above main content */} + {/* Filter Selector - Above main content */}
-
- {/* Main Content Grid - Map Left, Charts Right */} -
- {/* Left Side - Map */} -
- -
+ {/* Charts Row - Side by Side */} +
+ {/* Left Chart - Detection Distribution */} + + + +
+ +
+ + Detection Distribution + +
+
+ + + +
- {/* Right Side - Stacked Charts */} -
- {/* Detection Distribution */} - - - -
- -
- - Detection Distribution - -
-
- - - -
+ {/* Right Chart - Location Bar Chart */} + + + +
+ +
+ + Detections by Location + +
+
+ + + +
+
- {/* Location Bar Chart */} - - - -
- -
- - Detections by Location - -
-
- - - -
-
+ {/* Map - Full Width Below Charts */} +
+
{/* Footer */} diff --git a/app/globals.css b/app/globals.css index b6b6b13..1e7d0de 100644 --- a/app/globals.css +++ b/app/globals.css @@ -7,45 +7,49 @@ /* Primary - Indigo */ --primary: oklch(0.55 0.24 264); --primary-foreground: oklch(0.98 0.01 264); - + /* Background & Surfaces */ - --background: oklch(0.98 0.01 280); + --background: hsla(210, 100%, 98%, 1); --foreground: oklch(0.15 0.02 280); - --card: oklch(0.99 0.005 280); + --card: oklch(1 0 0); --card-foreground: oklch(0.15 0.02 280); - --popover: oklch(0.99 0.005 280); + --popover: oklch(1 0 0); --popover-foreground: oklch(0.15 0.02 280); - + /* Secondary */ --secondary: oklch(0.95 0.02 264); --secondary-foreground: oklch(0.25 0.05 264); - + /* Muted */ --muted: oklch(0.94 0.02 280); --muted-foreground: oklch(0.45 0.03 280); - + /* Accent - Cyan */ --accent: oklch(0.92 0.04 200); --accent-foreground: oklch(0.25 0.08 200); - + /* Destructive */ --destructive: oklch(0.55 0.22 25); --destructive-foreground: oklch(0.98 0.01 25); - + /* Borders & Inputs */ - --border: oklch(0.90 0.02 280); + --border: hsla(210, 100%, 85%, 1); --input: oklch(0.92 0.02 280); - --ring: oklch(0.55 0.24 264); - + --ring: hsla(210, 100%, 85%, 1); + + /* Card Glow */ + --card-glow: hsla(210, 100%, 85%, 0.3); + --card-hover-border: hsla(210, 100%, 75%, 0.8); + /* Chart Colors - Vibrant Palette */ --chart-1: oklch(0.55 0.24 264); --chart-2: oklch(0.65 0.20 200); --chart-3: oklch(0.60 0.18 160); --chart-4: oklch(0.70 0.20 85); --chart-5: oklch(0.60 0.22 320); - + --radius: 0.75rem; - + /* Sidebar */ --sidebar: oklch(0.98 0.01 280); --sidebar-foreground: oklch(0.15 0.02 280); @@ -61,7 +65,7 @@ /* Primary - Light Indigo */ --primary: oklch(0.70 0.20 264); --primary-foreground: oklch(0.15 0.02 264); - + /* Background & Surfaces */ --background: oklch(0.12 0.02 280); --foreground: oklch(0.95 0.01 280); @@ -69,35 +73,35 @@ --card-foreground: oklch(0.95 0.01 280); --popover: oklch(0.16 0.02 280); --popover-foreground: oklch(0.95 0.01 280); - + /* Secondary */ --secondary: oklch(0.22 0.03 264); --secondary-foreground: oklch(0.90 0.02 264); - + /* Muted */ --muted: oklch(0.20 0.02 280); --muted-foreground: oklch(0.65 0.02 280); - + /* Accent */ --accent: oklch(0.22 0.04 200); --accent-foreground: oklch(0.85 0.08 200); - + /* Destructive */ --destructive: oklch(0.45 0.18 25); --destructive-foreground: oklch(0.90 0.08 25); - + /* Borders & Inputs */ --border: oklch(0.25 0.02 280); --input: oklch(0.22 0.02 280); --ring: oklch(0.70 0.20 264); - + /* Chart Colors */ --chart-1: oklch(0.65 0.22 264); --chart-2: oklch(0.70 0.18 200); --chart-3: oklch(0.65 0.16 160); --chart-4: oklch(0.75 0.18 85); --chart-5: oklch(0.65 0.20 320); - + /* Sidebar */ --sidebar: oklch(0.14 0.02 280); --sidebar-foreground: oklch(0.95 0.01 280); @@ -154,6 +158,7 @@ * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; } @@ -162,44 +167,37 @@ /* Premium Background with Animated Mesh Gradient */ @layer utilities { .bg-mesh-gradient { - background: - radial-gradient(ellipse 80% 50% at 20% 20%, oklch(0.75 0.15 264 / 0.15), transparent), - radial-gradient(ellipse 60% 40% at 80% 80%, oklch(0.70 0.12 200 / 0.12), transparent), - radial-gradient(ellipse 50% 30% at 50% 50%, oklch(0.65 0.10 320 / 0.08), transparent), - var(--background); + background: hsla(210, 100%, 98%, 1); + background: linear-gradient(135deg, hsla(210, 100%, 98%, 1) 0%, hsla(220, 100%, 95%, 1) 100%); } - + .dark .bg-mesh-gradient { - background: - radial-gradient(ellipse 80% 50% at 20% 20%, oklch(0.40 0.18 264 / 0.20), transparent), - radial-gradient(ellipse 60% 40% at 80% 80%, oklch(0.35 0.15 200 / 0.15), transparent), - radial-gradient(ellipse 50% 30% at 50% 50%, oklch(0.30 0.12 320 / 0.10), transparent), - var(--background); + background: hsla(210, 30%, 15%, 1); } - + /* Glassmorphism Card */ .glass-card { background: oklch(1 0 0 / 0.7); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid oklch(1 0 0 / 0.2); - box-shadow: + box-shadow: 0 4px 6px -1px oklch(0 0 0 / 0.05), 0 10px 15px -3px oklch(0 0 0 / 0.08), 0 20px 25px -5px oklch(0 0 0 / 0.05), inset 0 1px 0 oklch(1 0 0 / 0.5); } - + .dark .glass-card { background: oklch(0.18 0.02 280 / 0.7); border: 1px solid oklch(1 0 0 / 0.08); - box-shadow: + box-shadow: 0 4px 6px -1px oklch(0 0 0 / 0.15), 0 10px 15px -3px oklch(0 0 0 / 0.20), 0 20px 25px -5px oklch(0 0 0 / 0.15), inset 0 1px 0 oklch(1 0 0 / 0.05); } - + /* Gradient Text */ .text-gradient { background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.60 0.20 200)); @@ -207,42 +205,42 @@ -webkit-text-fill-color: transparent; background-clip: text; } - + .dark .text-gradient { background: linear-gradient(135deg, oklch(0.75 0.20 264), oklch(0.70 0.18 200)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } - + /* Gradient Button */ .btn-gradient { background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.50 0.22 280)); - box-shadow: + box-shadow: 0 4px 14px 0 oklch(0.55 0.24 264 / 0.35), inset 0 1px 0 oklch(1 0 0 / 0.15); transition: all 0.3s ease; } - + .btn-gradient:hover { - box-shadow: + box-shadow: 0 6px 20px 0 oklch(0.55 0.24 264 / 0.45), inset 0 1px 0 oklch(1 0 0 / 0.2); transform: translateY(-1px); } - + .btn-gradient:active { transform: translateY(0); - box-shadow: + box-shadow: 0 2px 8px 0 oklch(0.55 0.24 264 / 0.30), inset 0 1px 0 oklch(1 0 0 / 0.15); } - + /* Card Glow Border */ .card-glow { position: relative; } - + .card-glow::before { content: ''; position: absolute; @@ -253,55 +251,67 @@ opacity: 0; transition: opacity 0.3s ease; } - + .card-glow:hover::before { opacity: 1; } - + /* Progress Bar Gradient */ .progress-gradient { background: linear-gradient(90deg, oklch(0.55 0.24 264), oklch(0.60 0.20 200), oklch(0.65 0.18 160)); background-size: 200% 100%; animation: progress-shimmer 2s ease infinite; } - + @keyframes progress-shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } } - + /* Subtle Float Animation */ .float-subtle { animation: float-subtle 6s ease-in-out infinite; } - + @keyframes float-subtle { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-5px); } + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-5px); + } } - + /* Input Focus Glow */ .input-glow:focus-within { box-shadow: 0 0 0 3px oklch(0.55 0.24 264 / 0.15); } - + /* Step Indicator */ .step-active { background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.50 0.22 280)); color: white; box-shadow: 0 2px 8px oklch(0.55 0.24 264 / 0.3); } - + .step-completed { background: oklch(0.65 0.18 160); color: white; } - + .step-pending { background: oklch(0.90 0.02 280); color: oklch(0.50 0.02 280); } - + .dark .step-pending { background: oklch(0.25 0.02 280); color: oklch(0.60 0.02 280); diff --git a/app/map/page.tsx b/app/map/page.tsx index 4bea2b9..568ad7d 100644 --- a/app/map/page.tsx +++ b/app/map/page.tsx @@ -9,13 +9,6 @@ export default function MapPage() { {/* Navigation Menu */} - {/* Decorative background elements */} -
-
-
-
-
-
{/* Header */}
diff --git a/app/new-analysis/page.tsx b/app/new-analysis/page.tsx new file mode 100644 index 0000000..5744d1f --- /dev/null +++ b/app/new-analysis/page.tsx @@ -0,0 +1,50 @@ +"use client" + +import { useRouter } from "next/navigation" +import { ProjectSelectionSection } from "@/components/project-selection-section" +import { SidebarNavigation } from "@/components/sidebar-navigation" +import { type SessionContext, saveSession } from "@/lib/api" + +export default function NewAnalysisPage() { + const router = useRouter() + + const handleSelectionComplete = (session: SessionContext) => { + // Save session to storage and navigate to upload page + saveSession(session) + router.push("/upload") + } + + return ( +
+ {/* Sidebar Navigation */} + + + {/* Main Content */} +
+ +
+ {/* Premium Header */} +
+
+

+ VisionRoad Detection System +

+
+
+ + {/* Project Selection Section */} +
+ +
+ + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
+
+
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx index 53cd430..0d4a4ac 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,56 +1,5 @@ -"use client" +import { redirect } from "next/navigation" -import { useRouter } from "next/navigation" -import { ProjectSelectionSection } from "@/components/project-selection-section" -import { SidebarNavigation } from "@/components/sidebar-navigation" -import { type SessionContext, saveSession } from "@/lib/api" - -export default function SelectionPage() { - const router = useRouter() - - const handleSelectionComplete = (session: SessionContext) => { - // Save session to storage and navigate to upload page - saveSession(session) - router.push("/upload") - } - - return ( -
- {/* Sidebar Navigation */} - - - {/* Main Content */} -
- {/* Decorative background elements */} -
-
-
-
-
- -
- {/* Premium Header */} -
-
-

- VisionRoad Detection System -

-
-
- - {/* Project Selection Section */} -
- -
- - {/* Footer */} -
-

- Sentient Geeks Pvt. Ltd. -

-
-
-
-
- ) +export default function HomePage() { + redirect("/dashboard") } diff --git a/app/results/page.tsx b/app/results/page.tsx index eeb89dc..9cd62ec 100644 --- a/app/results/page.tsx +++ b/app/results/page.tsx @@ -95,7 +95,7 @@ export default function ResultsPage() { const videoData = loadVideoData() if (!isSessionValid(storedSession) || !videoData) { - router.replace("/") + router.replace("/new-analysis") return } @@ -143,7 +143,7 @@ export default function ResultsPage() { } } clearSession() - router.push("/") + router.push("/new-analysis") } const handleBackToUpload = () => { @@ -158,7 +158,7 @@ export default function ResultsPage() { if (isLoading) { return ( -
+

Loading detection results...

@@ -169,7 +169,7 @@ export default function ResultsPage() { if (error) { return ( -
+

{error}

@@ -179,7 +179,7 @@ export default function ResultsPage() { } return ( -
+
{/* Sidebar Navigation */} @@ -199,7 +199,7 @@ export default function ResultsPage() { {/* Session Info Bar */} {session && (
-
+
@@ -209,11 +209,11 @@ export default function ResultsPage() { {session.projectName}
-
+
Package: - {session.packageName} + {session.packageName}
diff --git a/app/upload/page.tsx b/app/upload/page.tsx index 04a18bc..4b2b1bd 100644 --- a/app/upload/page.tsx +++ b/app/upload/page.tsx @@ -44,7 +44,7 @@ export default function UploadPage() { useEffect(() => { const storedSession = loadSession() if (!isSessionValid(storedSession)) { - router.replace("/") + router.replace("/new-analysis") return } setSession(storedSession) @@ -147,7 +147,7 @@ export default function UploadPage() { const handleBackToSelection = () => { clearSession() - router.push("/") + router.push("/new-analysis") } const getTitle = () => { @@ -158,7 +158,7 @@ export default function UploadPage() { if (isLoading) { return ( -
+
@@ -167,17 +167,12 @@ export default function UploadPage() { } return ( -
+
{/* Sidebar Navigation */} {/* Main Content */}
- {/* Decorative background elements */} -
-
-
-
{/* Compact Header */} @@ -224,7 +219,7 @@ export default function UploadPage() { )} {/* Upload Card */} - + diff --git a/components/dashboard/dashboard-map.tsx b/components/dashboard/dashboard-map.tsx index 930098f..ff71558 100644 --- a/components/dashboard/dashboard-map.tsx +++ b/components/dashboard/dashboard-map.tsx @@ -21,30 +21,59 @@ const DashboardMapContent = dynamic( interface DashboardMapProps { className?: string + selectedProjectId?: string | null + selectedPackageId?: string | null + selectedLocationId?: string | null + projectSummary?: any } -export function DashboardMap({ className }: DashboardMapProps) { +export function DashboardMap({ + className, + selectedProjectId, + selectedPackageId, + selectedLocationId, + projectSummary +}: DashboardMapProps) { const [detections, setDetections] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { - const loadDetections = async () => { - try { - setIsLoading(true) - setError(null) - const data = await fetchAllDetections() - setDetections(data) - } catch (err) { - console.error("Failed to load detections:", err) - setError("Failed to load detection data") - } finally { - setIsLoading(false) - } + if (!projectSummary) { + setDetections([]) + setIsLoading(false) + return } - loadDetections() - }, []) + try { + setIsLoading(true) + setError(null) + + const filteredDetections: Detection[] = [] + + const packagesToProcess = selectedPackageId && selectedPackageId !== "all" + ? { [selectedPackageId]: projectSummary.packages[selectedPackageId] } + : projectSummary.packages || {} + + for (const [pkgName, pkg] of Object.entries(packagesToProcess)) { + const locationsToProcess = selectedLocationId && selectedLocationId !== "all" + ? { [selectedLocationId]: (pkg as any).locations[selectedLocationId] } + : (pkg as any).locations || {} + + for (const [locName, loc] of Object.entries(locationsToProcess)) { + const locationDetections = (loc as any).detections || [] + filteredDetections.push(...locationDetections) + } + } + + setDetections(filteredDetections) + } catch (err) { + console.error("Failed to extract detections:", err) + setError("Failed to load detection data") + } finally { + setIsLoading(false) + } + }, [projectSummary, selectedPackageId, selectedLocationId]) // Filter detections with valid GPS coordinates const validDetections = detections.filter(d => d.latitude && d.longitude) @@ -57,18 +86,16 @@ export function DashboardMap({ className }: DashboardMapProps) { const signboardCount = validDetections.length - potholeCount return ( - - + +
-
-
- +
+
+
- - - Detection Map - + + Detection Map

{isLoading ? "Loading..." : `${validDetections.length} detections with GPS coordinates`} diff --git a/components/dashboard/detection-chart.tsx b/components/dashboard/detection-chart.tsx index d8664cf..bf97433 100644 --- a/components/dashboard/detection-chart.tsx +++ b/components/dashboard/detection-chart.tsx @@ -14,9 +14,9 @@ export function DetectionChart({ potholes, signboards, isLoading }: DetectionCha const signboardPercent = total > 0 ? (signboards / total) * 100 : 50 return ( - + - Detection Distribution + Detection Distribution {isLoading ? ( @@ -73,8 +73,8 @@ export function DetectionChart({ potholes, signboards, isLoading }: DetectionCha - - + + @@ -98,9 +98,9 @@ export function DetectionChart({ potholes, signboards, isLoading }: DetectionCha

{potholePercent.toFixed(0)}%

-
+
-
+
Signboards
diff --git a/components/dashboard/detection-donut-chart.tsx b/components/dashboard/detection-donut-chart.tsx index 8b48499..da04296 100644 --- a/components/dashboard/detection-donut-chart.tsx +++ b/components/dashboard/detection-donut-chart.tsx @@ -40,16 +40,16 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti ] return ( -
+
@@ -57,7 +57,8 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti ))} @@ -91,7 +92,7 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti className="w-3 h-3 rounded-full" style={{ backgroundColor: entry.color }} /> - + {entry.value}: {data[index].value}
@@ -104,8 +105,8 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti {/* Center label */}
-

{total}

-

Total

+

{total}

+

Total

diff --git a/components/dashboard/filter-selector.tsx b/components/dashboard/filter-selector.tsx new file mode 100644 index 0000000..bc0625f --- /dev/null +++ b/components/dashboard/filter-selector.tsx @@ -0,0 +1,126 @@ +"use client" + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { FolderOpen, Package, MapPin } from "lucide-react" +import { type Project } from "@/lib/api" + +interface FilterSelectorProps { + projects: Project[] + selectedProjectId: string | null + selectedPackageId: string | null + selectedLocationId: string | null + onProjectChange: (projectId: string) => void + onPackageChange: (packageId: string) => void + onLocationChange: (locationId: string) => void + packages: Array<{ id: string; name: string }> + locations: Array<{ id: string; name: string }> + isLoading?: boolean +} + +export function FilterSelector({ + projects, + selectedProjectId, + selectedPackageId, + selectedLocationId, + onProjectChange, + onPackageChange, + onLocationChange, + packages, + locations, + isLoading = false +}: FilterSelectorProps) { + return ( +
+ {/* Project Dropdown */} +
+
+ +
+ +
+ + {/* Divider */} + {selectedProjectId && packages.length > 0 && ( +
+ )} + + {/* Package Dropdown */} + {selectedProjectId && packages.length > 0 && ( +
+
+ +
+ +
+ )} + + {/* Divider */} + {selectedPackageId && selectedPackageId !== "all" && locations.length > 0 && ( +
+ )} + + {/* Location Dropdown */} + {selectedPackageId && selectedPackageId !== "all" && locations.length > 0 && ( +
+
+ +
+ +
+ )} +
+ ) +} diff --git a/components/dashboard/gradient-stats-card.tsx b/components/dashboard/gradient-stats-card.tsx index ba324af..aa9b6a7 100644 --- a/components/dashboard/gradient-stats-card.tsx +++ b/components/dashboard/gradient-stats-card.tsx @@ -14,38 +14,38 @@ interface GradientStatsCardProps { const gradientStyles = { green: { - background: "bg-gradient-to-br from-emerald-50 via-emerald-50 to-teal-100 dark:from-emerald-950/40 dark:via-emerald-950/30 dark:to-teal-950/40", - border: "border-l-4 border-l-emerald-500", - iconBg: "bg-gradient-to-br from-emerald-400 to-teal-500", - iconShadow: "shadow-lg shadow-emerald-500/30", - valueGradient: "bg-gradient-to-r from-emerald-600 via-teal-500 to-emerald-600 dark:from-emerald-400 dark:via-teal-400 dark:to-emerald-400" + background: "bg-card", + border: "border-l-4 border-l-[var(--border)]", + iconBg: "bg-[#60a5fa]", + iconShadow: "shadow-lg shadow-[#60a5fa]/20", + valueGradient: "text-gray-900 dark:text-white" }, coral: { - background: "bg-gradient-to-br from-red-50 via-red-50 to-orange-100 dark:from-red-950/40 dark:via-red-950/30 dark:to-orange-950/40", - border: "border-l-4 border-l-red-500", - iconBg: "bg-gradient-to-br from-red-400 to-orange-500", - iconShadow: "shadow-lg shadow-red-500/30", - valueGradient: "bg-gradient-to-r from-red-600 via-orange-500 to-red-600 dark:from-red-400 dark:via-orange-400 dark:to-red-400" + background: "bg-card", + border: "border-l-4 border-l-[var(--border)]", + iconBg: "bg-[#60a5fa]", + iconShadow: "shadow-lg shadow-[#60a5fa]/20", + valueGradient: "text-gray-900 dark:text-white" }, blue: { - background: "bg-gradient-to-br from-blue-50 via-blue-50 to-indigo-100 dark:from-blue-950/40 dark:via-blue-950/30 dark:to-indigo-950/40", - border: "border-l-4 border-l-blue-500", - iconBg: "bg-gradient-to-br from-blue-400 to-indigo-500", - iconShadow: "shadow-lg shadow-blue-500/30", - valueGradient: "bg-gradient-to-r from-blue-600 via-indigo-500 to-blue-600 dark:from-blue-400 dark:via-indigo-400 dark:to-blue-400" + background: "bg-card", + border: "border-l-4 border-l-[var(--border)]", + iconBg: "bg-[#60a5fa]", + iconShadow: "shadow-lg shadow-[#60a5fa]/20", + valueGradient: "text-gray-900 dark:text-white" }, purple: { - background: "bg-gradient-to-br from-purple-50 via-purple-50 to-pink-100 dark:from-purple-950/40 dark:via-purple-950/30 dark:to-pink-950/40", - border: "border-l-4 border-l-purple-500", - iconBg: "bg-gradient-to-br from-purple-400 to-pink-500", - iconShadow: "shadow-lg shadow-purple-500/30", - valueGradient: "bg-gradient-to-r from-purple-600 via-pink-500 to-purple-600 dark:from-purple-400 dark:via-pink-400 dark:to-purple-400" + background: "bg-card", + border: "border-l-4 border-l-[var(--border)]", + iconBg: "bg-[#60a5fa]", + iconShadow: "shadow-lg shadow-[#60a5fa]/20", + valueGradient: "text-gray-900 dark:text-white" } } export function GradientStatsCard({ title, - subtitle = "Work level distribution", + subtitle, value, icon: Icon, gradient, @@ -54,41 +54,37 @@ export function GradientStatsCard({ const styles = gradientStyles[gradient] return ( - - -
- {/* Large gradient icon */} -
- -
- - {/* Content */} -
-

- {title} -

-

- {subtitle} -

- + + +
+

+ {title} +

+
{isLoading ? ( -
+
) : ( -

- {value} -

+ <> +

+ {value} +

+ {subtitle && ( +

+ {subtitle} +

+ )} + )}
+ +
+ +
) diff --git a/components/dashboard/location-bar-chart.tsx b/components/dashboard/location-bar-chart.tsx index 5f52813..2b9bc26 100644 --- a/components/dashboard/location-bar-chart.tsx +++ b/components/dashboard/location-bar-chart.tsx @@ -39,13 +39,23 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) { } return ( -
+
+ + + + + + + + + + { if (active && payload && payload.length) { return (
-

{label}

+

{label}

{payload.map((entry, index) => ( -
+
(
{payload?.map((entry, index) => (
- + {entry.value}
@@ -111,13 +121,13 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) { diff --git a/components/dashboard/recent-analyses-table.tsx b/components/dashboard/recent-analyses-table.tsx index 53725a1..00a5c8b 100644 --- a/components/dashboard/recent-analyses-table.tsx +++ b/components/dashboard/recent-analyses-table.tsx @@ -43,13 +43,13 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent if (type === "pothole-detection") { return Pothole } - return Signboard + return Signboard } return ( - + - Recent Analyses + Recent Analyses {isLoading ? ( diff --git a/components/data-table.tsx b/components/data-table.tsx new file mode 100644 index 0000000..c8a15c6 --- /dev/null +++ b/components/data-table.tsx @@ -0,0 +1,102 @@ +"use client" + +import { Plus } from "lucide-react" +import { Button } from "@/components/ui/button" + +interface Column { + key: string + header: string + render?: (item: T) => React.ReactNode +} + +interface DataTableProps { + title: string + data: T[] + columns: Column[] + onAddNew: () => void + addButtonText: string + isLoading?: boolean +} + +export function DataTable>({ + title, + data, + columns, + onAddNew, + addButtonText, + isLoading = false +}: DataTableProps) { + return ( +
+ {/* Header with Title and Add Button */} +
+
+

{title}

+

Total items: {data.length}

+
+ +
+ + {/* Table */} +
+ + + + {columns.map((col) => ( + + ))} + + + + {isLoading ? ( + + + + ) : data.length === 0 ? ( + + + + ) : ( + data.map((item, idx) => ( + + {columns.map((col) => ( + + ))} + + )) + )} + +
+ {col.header} +
+
+
+ Loading records... +
+
+
+
+ +
+

No data available

+

Click "{addButtonText}" to get started

+
+
+ {col.render ? col.render(item) : item[col.key] || "—"} +
+
+
+ ) +} diff --git a/components/navigation-menu.tsx b/components/navigation-menu.tsx index 16d7ebf..97102b9 100644 --- a/components/navigation-menu.tsx +++ b/components/navigation-menu.tsx @@ -14,7 +14,10 @@ import { Menu, LayoutDashboard, ChevronRight, - Home, + PlusCircle, + FolderPlus, + Package, + MapPin, Map } from "lucide-react" @@ -26,18 +29,36 @@ interface NavItem { } const navItems: NavItem[] = [ - { - title: "Home", - description: "Project selection and analysis setup", - href: "/", - icon: Home - }, { title: "Dashboard", description: "View analytics, statistics, and recent analyses", href: "/dashboard", icon: LayoutDashboard }, + { + title: "New Analysis", + description: "Start a new road analysis project", + href: "/new-analysis", + icon: PlusCircle + }, + { + title: "Create Project", + description: "Create a new infrastructure project", + href: "/create-project", + icon: FolderPlus + }, + { + title: "Create Package", + description: "Add a package under an existing project", + href: "/create-package", + icon: Package + }, + { + title: "Create Location", + description: "Add a location under a project & package", + href: "/create-location", + icon: MapPin + }, { title: "Show Map", description: "View all detections on an interactive map", @@ -94,8 +115,8 @@ export function NavigationMenu() { }`} >
diff --git a/components/sidebar-navigation.tsx b/components/sidebar-navigation.tsx index e169d84..e35bc80 100644 --- a/components/sidebar-navigation.tsx +++ b/components/sidebar-navigation.tsx @@ -1,7 +1,7 @@ "use client" import { useRouter, usePathname } from "next/navigation" -import { Home, LayoutDashboard, User } from "lucide-react" +import { LayoutDashboard, Plus, User, FolderPlus, Package, MapPin } from "lucide-react" interface NavItem { title: string @@ -12,18 +12,30 @@ interface NavItem { } const navItems: NavItem[] = [ - { - title: "Home", - href: "/", - icon: Home, - gradient: "from-emerald-400 to-teal-500" - }, { title: "Dashboard", href: "/dashboard", icon: LayoutDashboard, gradient: "from-blue-400 to-indigo-500" }, + { + title: "Create Project", + href: "/create-project", + icon: FolderPlus, + gradient: "from-blue-400 to-blue-600" + }, + { + title: "Create Package", + href: "/create-package", + icon: Package, + gradient: "from-violet-400 to-purple-500" + }, + { + title: "Create Location", + href: "/create-location", + icon: MapPin, + gradient: "from-amber-400 to-orange-500" + }, { title: "Account", href: "/account", @@ -42,54 +54,112 @@ export function SidebarNavigation() { router.push(item.href) } + const isNewAnalysisActive = pathname === "/new-analysis" + return ( -