refactor: remove unused packages
This commit is contained in:
78
src/components/dashboard/compact-project-selector.tsx
Normal file
78
src/components/dashboard/compact-project-selector.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { FolderOpen, MapPin, Building2 } from "lucide-react"
|
||||
import { type Project } from "@/lib/api"
|
||||
|
||||
interface CompactProjectSelectorProps {
|
||||
projects: Project[]
|
||||
selectedProjectId: string | null
|
||||
onProjectChange: (projectId: string) => void
|
||||
selectedProject: Project | undefined
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export function CompactProjectSelector({
|
||||
projects,
|
||||
selectedProjectId,
|
||||
onProjectChange,
|
||||
selectedProject,
|
||||
isLoading = false
|
||||
}: CompactProjectSelectorProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3 p-3 rounded-xl bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50">
|
||||
{/* Project Dropdown */}
|
||||
<div className="flex flex-col min-w-[120px]">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Project</span>
|
||||
<Select
|
||||
value={selectedProjectId || ""}
|
||||
onValueChange={onProjectChange}
|
||||
disabled={isLoading || projects.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-auto p-0 border-0 shadow-none focus:ring-0 text-sm font-bold text-gray-900 dark:text-white bg-transparent text-left">
|
||||
<SelectValue placeholder="Select project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projects.map((project) => (
|
||||
<SelectItem key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Corridor Badge */}
|
||||
{selectedProject?.corridor_name && (
|
||||
<>
|
||||
<div className="h-8 w-px bg-gray-200 dark:bg-gray-700 mx-2" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Corridor</span>
|
||||
<span className="text-xs font-bold text-gray-600 dark:text-gray-400">
|
||||
{selectedProject.corridor_name}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* State Badge */}
|
||||
{selectedProject?.state && (
|
||||
<>
|
||||
<div className="h-8 w-px bg-gray-200 dark:bg-gray-700 mx-2" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">State</span>
|
||||
<span className="text-xs font-bold text-gray-600 dark:text-gray-400">
|
||||
{selectedProject.state}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
137
src/components/dashboard/dashboard-map-content.tsx
Normal file
137
src/components/dashboard/dashboard-map-content.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { MapContainer, TileLayer, CircleMarker, Popup, useMap } from "react-leaflet"
|
||||
import { LatLngBounds, LatLng } from "leaflet"
|
||||
import "leaflet/dist/leaflet.css"
|
||||
import { type Detection } from "@/lib/api"
|
||||
|
||||
interface DashboardMapContentProps {
|
||||
detections: Detection[]
|
||||
}
|
||||
|
||||
// Component to auto-fit map bounds to show all markers
|
||||
function FitBounds({ bounds }: { bounds: LatLngBounds }) {
|
||||
const map = useMap()
|
||||
|
||||
useEffect(() => {
|
||||
if (bounds.isValid()) {
|
||||
map.fitBounds(bounds, { padding: [50, 50] })
|
||||
}
|
||||
}, [bounds, map])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default function DashboardMapContent({ detections }: DashboardMapContentProps) {
|
||||
if (detections.length === 0) {
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center bg-muted/20">
|
||||
<p className="text-muted-foreground">No detections to display</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate bounds to fit all markers
|
||||
const firstDetection = detections[0]
|
||||
const bounds = new LatLngBounds(
|
||||
new LatLng(firstDetection.latitude!, firstDetection.longitude!),
|
||||
new LatLng(firstDetection.latitude!, firstDetection.longitude!)
|
||||
)
|
||||
|
||||
detections.forEach(d => {
|
||||
if (d.latitude && d.longitude) {
|
||||
bounds.extend(new LatLng(d.latitude, d.longitude))
|
||||
}
|
||||
})
|
||||
|
||||
// Center point
|
||||
const center: [number, number] = [
|
||||
(bounds.getNorth() + bounds.getSouth()) / 2,
|
||||
(bounds.getEast() + bounds.getWest()) / 2
|
||||
]
|
||||
|
||||
// Get marker color based on detection type
|
||||
const getMarkerColor = (type: string) => {
|
||||
const t = type.toLowerCase()
|
||||
if (t === "pothole") {
|
||||
return { fill: "#ef4444", stroke: "#b91c1c" } // Red
|
||||
} else if (t === "defected_sign_board") {
|
||||
return { fill: "#3b82f6", stroke: "#1d4ed8" } // Blue
|
||||
} else if (t === "road_crack") {
|
||||
return { fill: "#f59e0b", stroke: "#b45309" } // Orange
|
||||
} else if (t === "damaged_road_marking") {
|
||||
return { fill: "#6366f1", stroke: "#4338ca" } // Indigo
|
||||
} else if (t === "good_sign_board") {
|
||||
return { fill: "#10b981", stroke: "#047857" } // Emerald
|
||||
}
|
||||
return { fill: "#64748b", stroke: "#475569" } // Default Slate
|
||||
}
|
||||
|
||||
// Get display name for detection type
|
||||
const getTypeName = (type: string) => {
|
||||
return type.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
|
||||
}
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
center={center}
|
||||
zoom={13}
|
||||
className="h-full w-full"
|
||||
scrollWheelZoom={true}
|
||||
zoomAnimation={false}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
|
||||
{/* Detection markers */}
|
||||
{detections.map((detection, idx) => {
|
||||
const colors = getMarkerColor(detection.type)
|
||||
const typeName = getTypeName(detection.type)
|
||||
|
||||
return (
|
||||
<CircleMarker
|
||||
key={`${detection.id}-${idx}`}
|
||||
center={[detection.latitude!, detection.longitude!]}
|
||||
radius={10}
|
||||
fillColor={colors.fill}
|
||||
color={colors.stroke}
|
||||
weight={2}
|
||||
opacity={1}
|
||||
fillOpacity={0.8}
|
||||
>
|
||||
<Popup>
|
||||
<div className="text-sm space-y-2 min-w-[180px]">
|
||||
<div className="font-bold text-base border-b pb-1">
|
||||
{typeName}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Class:</span>
|
||||
<span className="font-medium">{detection.class.replace(/_/g, " ")}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Confidence:</span>
|
||||
<span className="font-medium">{(detection.confidence * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 border-t">
|
||||
<div className="text-xs text-muted-foreground mb-1">Coordinates</div>
|
||||
<div className="font-mono text-xs bg-muted/30 p-2 rounded">
|
||||
<div>Lat: {detection.latitude!.toFixed(6)}</div>
|
||||
<div>Lng: {detection.longitude!.toFixed(6)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</CircleMarker>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Auto-fit bounds */}
|
||||
<FitBounds bounds={bounds} />
|
||||
</MapContainer>
|
||||
)
|
||||
}
|
||||
158
src/components/dashboard/dashboard-map.tsx
Normal file
158
src/components/dashboard/dashboard-map.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Loader2, MapPin, AlertTriangle, RectangleHorizontal } from "lucide-react"
|
||||
import { fetchAllDetections, type Detection } from "@/lib/api"
|
||||
|
||||
// Dynamically import the map to avoid SSR issues with Leaflet
|
||||
const DashboardMapContent = dynamic(
|
||||
() => import("./dashboard-map-content"),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
interface DashboardMapProps {
|
||||
className?: string
|
||||
selectedProjectId?: string | null
|
||||
selectedPackageId?: string | null
|
||||
selectedLocationId?: string | null
|
||||
projectSummary?: any
|
||||
}
|
||||
|
||||
export function DashboardMap({
|
||||
className,
|
||||
selectedProjectId,
|
||||
selectedPackageId,
|
||||
selectedLocationId,
|
||||
projectSummary
|
||||
}: DashboardMapProps) {
|
||||
const [detections, setDetections] = useState<Detection[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectSummary) {
|
||||
setDetections([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
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)) {
|
||||
if (!loc) continue
|
||||
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)
|
||||
|
||||
// Count detections by category
|
||||
const counts = {
|
||||
defected_sign_board: validDetections.filter(d => d.type?.toLowerCase() === "defected_sign_board").length,
|
||||
pothole: validDetections.filter(d => d.type?.toLowerCase() === "pothole").length,
|
||||
road_crack: validDetections.filter(d => d.type?.toLowerCase() === "road_crack").length,
|
||||
damaged_road_marking: validDetections.filter(d => d.type?.toLowerCase() === "damaged_road_marking").length,
|
||||
good_sign_board: validDetections.filter(d => d.type?.toLowerCase() === "good_sign_board").length
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={`overflow-hidden pb-0 bg-white/60 rounded-b-none shadow-none border-none ${className}`}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-md bg-linear-to-b from-[#225999] to-[#56A5FF] flex items-center justify-center ">
|
||||
<MapPin className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-bold text-[#2563eb]">
|
||||
Detection Map
|
||||
</CardTitle>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{isLoading ? "Loading..." : `${validDetections.length} detections with GPS coordinates`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compact Color Legend */}
|
||||
<div className="flex flex-wrap items-center justify-end gap-x-4 gap-y-1 text-[10px] max-w-[60%]">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#ef4444] shadow-sm shadow-[#ef4444]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Potholes ({counts.pothole})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#3b82f6] shadow-sm shadow-[#3b82f6]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Defected Signs ({counts.defected_sign_board})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#f59e0b] shadow-sm shadow-[#f59e0b]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Cracks ({counts.road_crack})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#6366f1] shadow-sm shadow-[#6366f1]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Markings ({counts.damaged_road_marking})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#10b981] shadow-sm shadow-[#10b981]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Good Signs ({counts.good_sign_board})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="h-[500px] p-2 w-full relative">
|
||||
{isLoading ? (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
|
||||
<Loader2 className="h-8 w-8 text-indigo-500" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
|
||||
<p className="text-gray-500">{error}</p>
|
||||
</div>
|
||||
) : validDetections.length === 0 ? (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500">No detections with GPS coordinates found</p>
|
||||
<p className="text-sm text-gray-400 mt-1">Process some videos to see detections on the map</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<DashboardMapContent detections={validDetections} />
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
59
src/components/dashboard/dashboard-skeleton.tsx
Normal file
59
src/components/dashboard/dashboard-skeleton.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||
|
||||
export function DashboardSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats Cards Skeleton */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<Card key={i} className="bg-white/60 backdrop-blur-lg border-none shadow-none overflow-hidden py-8 px-6">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-10 w-20" />
|
||||
<Skeleton className="h-3 w-40" />
|
||||
</div>
|
||||
<Skeleton className="w-16 h-16 rounded-lg shrink-0" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts Row Skeleton */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<Card key={i} className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
|
||||
<CardHeader className="pb-2 border-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="w-10 h-10 rounded-md" />
|
||||
<Skeleton className="h-6 w-48" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 h-[350px] flex items-center justify-center">
|
||||
<Skeleton className="h-[300px] w-full max-w-[300px] rounded-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Map Skeleton */}
|
||||
<Card className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="w-10 h-10 rounded-md" />
|
||||
<Skeleton className="h-6 w-48" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-32 rounded-full" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2">
|
||||
<Skeleton className="h-[500px] w-full rounded-md" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
159
src/components/dashboard/detection-donut-chart.tsx
Normal file
159
src/components/dashboard/detection-donut-chart.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"
|
||||
import { Loader2 } from "lucide-react"
|
||||
|
||||
interface DetectionDonutChartProps {
|
||||
defectedSignboard: number
|
||||
pothole: number
|
||||
roadCrack: number
|
||||
damagedRoadMarking: number
|
||||
goodSignboard: number
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
defectedSignboard: "#3b82f6", // Blue
|
||||
pothole: "#ef4444", // Red
|
||||
roadCrack: "#f59e0b", // Amber/Orange
|
||||
damagedRoadMarking: "#6366f1", // Indigo
|
||||
goodSignboard: "#10b981" // Emerald
|
||||
}
|
||||
|
||||
export function DetectionDonutChart({
|
||||
defectedSignboard,
|
||||
pothole,
|
||||
roadCrack,
|
||||
damagedRoadMarking,
|
||||
goodSignboard,
|
||||
isLoading = false
|
||||
}: DetectionDonutChartProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-[250px] flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 text-primary/50" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const total = defectedSignboard + pothole + roadCrack + damagedRoadMarking + goodSignboard
|
||||
|
||||
if (total === 0) {
|
||||
return (
|
||||
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
|
||||
<p className="text-sm">No detections found</p>
|
||||
<p className="text-xs mt-1">Process videos to see data</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const data = [
|
||||
{ name: "Defected Signboards", value: defectedSignboard, color: COLORS.defectedSignboard },
|
||||
{ name: "Potholes", value: pothole, color: COLORS.pothole },
|
||||
{ name: "Road Cracks", value: roadCrack, color: COLORS.roadCrack },
|
||||
{ name: "Damaged Markings", value: damagedRoadMarking, color: COLORS.damagedRoadMarking },
|
||||
{ name: "Good Signboards", value: goodSignboard, color: COLORS.goodSignboard }
|
||||
].filter(item => item.value > 0)
|
||||
|
||||
return (
|
||||
<div className="h-[220px] relative">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<defs>
|
||||
<linearGradient id="gradPothole" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#ff8a8a" />
|
||||
<stop offset="100%" stopColor="#ef4444" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradDefectedSign" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#60a5fa" />
|
||||
<stop offset="100%" stopColor="#3b82f6" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradCrack" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#fbbf24" />
|
||||
<stop offset="100%" stopColor="#f59e0b" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradMarking" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#818cf8" />
|
||||
<stop offset="100%" stopColor="#6366f1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradGoodSign" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#34d399" />
|
||||
<stop offset="100%" stopColor="#10b981" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="45%"
|
||||
innerRadius={50}
|
||||
outerRadius={70}
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
strokeWidth={0}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
{data.map((entry, index) => {
|
||||
const gradId = entry.name === "Potholes" ? "gradPothole" :
|
||||
entry.name === "Defected Signboards" ? "gradDefectedSign" :
|
||||
entry.name === "Road Cracks" ? "gradCrack" :
|
||||
entry.name === "Damaged Markings" ? "gradMarking" : "gradGoodSign"
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={`url(#${gradId})`}
|
||||
fillOpacity={1}
|
||||
className="hover:fill-opacity-80"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0]
|
||||
return (
|
||||
<div className="bg-background/95 backdrop-blur-sm border border-border rounded-lg px-3 py-2 shadow-lg">
|
||||
<p className="font-medium text-sm">{data.name}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Count: <span className="font-semibold text-foreground">{data.value}</span>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{((Number(data.value) / total) * 100).toFixed(1)}% of total
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
height={40}
|
||||
content={({ payload }) => (
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 mt-2">
|
||||
{payload?.map((entry, index) => (
|
||||
<div key={`legend-${index}`} className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="w-2.5 h-2.5 rounded-full"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-[9px] text-muted-foreground whitespace-nowrap">
|
||||
{entry.value}: {data[index].value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
{/* Center label */}
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none" style={{ marginTop: '-55px' }}>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-extrabold text-[#2563eb]">{total}</p>
|
||||
<p className="text-[9px] uppercase tracking-wider text-muted-foreground">Total</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
125
src/components/dashboard/filter-selector.tsx
Normal file
125
src/components/dashboard/filter-selector.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"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 (
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
{/* Project Dropdown */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-md bg-blue-100 dark:bg-blue-900/50">
|
||||
<FolderOpen className="h-3.5 w-3.5 text-blue-600" />
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Project</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedProjectId || ""}
|
||||
onValueChange={onProjectChange}
|
||||
disabled={isLoading || projects.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||
<SelectValue placeholder="Select project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border-slate-200">
|
||||
{projects.map((project) => (
|
||||
<SelectItem key={project.id} value={project.id} className="rounded-lg">
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Package Dropdown */}
|
||||
{selectedProjectId && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-md bg-emerald-100 dark:bg-emerald-900/50">
|
||||
<Package className="h-3.5 w-3.5 text-emerald-600" />
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Package</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedPackageId || "all"}
|
||||
onValueChange={onPackageChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||
<SelectValue placeholder="All packages" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border-slate-200">
|
||||
<SelectItem value="all" className="rounded-lg">All Packages</SelectItem>
|
||||
{packages.map((pkg) => (
|
||||
<SelectItem key={pkg.id} value={pkg.id} className="rounded-lg">
|
||||
{pkg.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Location Dropdown */}
|
||||
{selectedPackageId && selectedPackageId !== "all" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-md bg-purple-100 dark:bg-purple-900/50">
|
||||
<MapPin className="h-3.5 w-3.5 text-purple-600" />
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Location</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedLocationId || "all"}
|
||||
onValueChange={onLocationChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||
<SelectValue placeholder="All locations" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border-slate-200">
|
||||
<SelectItem value="all" className="rounded-lg">All Locations</SelectItem>
|
||||
{locations.map((loc) => (
|
||||
<SelectItem key={loc.id} value={loc.id} className="rounded-lg">
|
||||
{loc.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
107
src/components/dashboard/gradient-stats-card.tsx
Normal file
107
src/components/dashboard/gradient-stats-card.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { LucideIcon } from "lucide-react"
|
||||
|
||||
interface GradientStatsCardProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
value: number | string
|
||||
icon: LucideIcon
|
||||
gradient: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald"
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const gradientStyles = {
|
||||
green: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
coral: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
blue: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
purple: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
orange: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
indigo: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
emerald: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function GradientStatsCard({
|
||||
title,
|
||||
subtitle,
|
||||
value,
|
||||
icon: Icon,
|
||||
gradient,
|
||||
isLoading = false
|
||||
}: GradientStatsCardProps) {
|
||||
const styles = gradientStyles[gradient]
|
||||
|
||||
return (
|
||||
<Card className="bg-white/60 backdrop-blur-lg border-none shadow-none hover:shadow-none overflow-hidden py-8 px-6">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<h3 className="text-sm font-bold text-blue-600/70 dark:text-blue-400/70 uppercase tracking-widest">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex flex-col">
|
||||
{isLoading ? (
|
||||
<div className="h-14 w-24 bg-gray-100 dark:bg-gray-800 rounded-xl mt-2" />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-4xl font-bold heading-blue-gradient">
|
||||
{value}
|
||||
</p>
|
||||
{subtitle && (
|
||||
<p className="text-xs font-medium text-gray-500 mt-1">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #225999 0%, #56A5FF 100%)",
|
||||
}}
|
||||
className={`
|
||||
w-16 h-16 self-start rounded-lg flex items-center justify-center
|
||||
`}>
|
||||
<Icon className="h-10 w-10 text-white" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
179
src/components/dashboard/location-bar-chart.tsx
Normal file
179
src/components/dashboard/location-bar-chart.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
"use client"
|
||||
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||
import { Loader2 } from "lucide-react"
|
||||
|
||||
interface LocationData {
|
||||
name: string
|
||||
defected_sign_board: number
|
||||
pothole: number
|
||||
road_crack: number
|
||||
damaged_road_marking: number
|
||||
good_sign_board: number
|
||||
total: number
|
||||
}
|
||||
|
||||
interface LocationBarChartProps {
|
||||
data: LocationData[]
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
defected_sign_board: "#60a5fa", // Lighter Blue
|
||||
pothole: "#ff8a8a", // Lighter Red
|
||||
road_crack: "#fbbf24", // Lighter Orange
|
||||
damaged_road_marking: "#818cf8", // Lighter Indigo
|
||||
good_sign_board: "#34d399" // Lighter Emerald
|
||||
}
|
||||
|
||||
export function LocationBarChart({ data, isLoading = false }: LocationBarChartProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-[300px] flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 text-primary/50" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="h-[300px] flex flex-col items-center justify-center text-muted-foreground">
|
||||
<p className="text-sm">No location data available</p>
|
||||
<p className="text-xs mt-1">Process videos to see detections by location</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={data}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 20 }}
|
||||
barCategoryGap="10%"
|
||||
barGap={2}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="barPothole" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#ff8a8a" />
|
||||
<stop offset="100%" stopColor="#ef4444" />
|
||||
</linearGradient>
|
||||
<linearGradient id="barDefectedSign" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#60a5fa" />
|
||||
<stop offset="100%" stopColor="#3b82f6" />
|
||||
</linearGradient>
|
||||
<linearGradient id="barCrack" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#fbbf24" />
|
||||
<stop offset="100%" stopColor="#f59e0b" />
|
||||
</linearGradient>
|
||||
<linearGradient id="barMarking" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#818cf8" />
|
||||
<stop offset="100%" stopColor="#6366f1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="barGoodSign" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#34d399" />
|
||||
<stop offset="100%" stopColor="#10b981" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="hsl(var(--muted-foreground) / 0.1)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={50}
|
||||
interval={0}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={25}
|
||||
/>
|
||||
<Tooltip
|
||||
content={({ active, payload, label }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-background/95 backdrop-blur-sm border border-border rounded-lg px-3 py-2 shadow-lg">
|
||||
<p className="font-medium text-xs mb-1">{label}</p>
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-xs">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-muted-foreground">{(entry.name as string).replace(/_/g, ' ')}:</span>
|
||||
<span className="font-semibold">{entry.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="top"
|
||||
height={30}
|
||||
content={({ payload }) => (
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 mb-2">
|
||||
{payload?.map((entry, index) => (
|
||||
<div key={`legend-${index}`} className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="w-2.5 h-2.5 rounded-sm"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-[9px] text-muted-foreground whitespace-nowrap">
|
||||
{(entry.value as string).replace(/_/g, ' ')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="pothole"
|
||||
name="Pothole"
|
||||
fill="url(#barPothole)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="defected_sign_board"
|
||||
name="Defected Signboard"
|
||||
fill="url(#barDefectedSign)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="road_crack"
|
||||
name="Road Crack"
|
||||
fill="url(#barCrack)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="damaged_road_marking"
|
||||
name="Damaged Marking"
|
||||
fill="url(#barMarking)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="good_sign_board"
|
||||
name="Good Signboard"
|
||||
fill="url(#barGoodSign)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user