Untitled
unknown
plain_text
9 months ago
55 kB
12
Indexable
"use client"
import { useEffect, useState } from "react"
import {
Bar,
BarChart,
Line,
LineChart,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
PieChart,
Pie,
Cell
} from "recharts"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { AppSidebar } from "@/components/app-sidebar"
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { useTranslation, type SupportedLanguages } from "@/utils/translations"
import { useSettings } from "@/hooks/useSettings"
import { useBinConfig } from "@/hooks/use-bin-config"
import { useMetrics } from "@/hooks/useMetrics"
import { useFillPredictions } from "@/hooks/useFillPredictions"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
type ChartConfig
} from "@/components/ui/chart"
import { Badge } from "@/components/ui/badge"
import { CalendarIcon, BarChart2Icon, LineChartIcon, PieChartIcon, RefreshCwIcon } from "lucide-react"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { format, parseISO, startOfMonth, endOfMonth, sub, isWithinInterval } from "date-fns"
export default function Page() {
const { settings } = useSettings()
const { t } = useTranslation((settings?.language as SupportedLanguages) || "EN")
const { metrics, loading, error, helpers } = useMetrics()
const { bins } = useBinConfig()
const { predictions } = useFillPredictions()
const [timeRange, setTimeRange] = useState("all")
const [activeTab, setActiveTab] = useState("distribution")
if (loading || !metrics) {
return (
<SidebarProvider>
<div className="flex h-screen w-full">
<AppSidebar />
<div className="flex-1 overflow-auto">
<header className="flex shrink-0 items-center gap-2 border-b px-4 py-6">
<SidebarTrigger />
<Separator orientation="vertical" className="mx-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden sm:block">
<BreadcrumbLink href="/">{t('navigation.home')}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden sm:block" />
<BreadcrumbItem>
<BreadcrumbPage>{t('navigation.patterns')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<main className="container mx-auto p-4">
<div className="flex items-center justify-center h-64">
<div className="flex flex-col items-center gap-4">
<RefreshCwIcon className="h-12 w-12 animate-spin text-muted-foreground" />
<p className="text-lg text-muted-foreground">Loading metrics...</p>
</div>
</div>
</main>
</div>
</div>
</SidebarProvider>
)
}
if (error) {
return (
<SidebarProvider>
<div className="flex h-screen w-full">
<AppSidebar />
<div className="flex-1 overflow-auto">
<header className="flex shrink-0 items-center gap-2 border-b px-4 py-6">
<SidebarTrigger />
<Separator orientation="vertical" className="mx-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden sm:block">
<BreadcrumbLink href="/">{t('navigation.home')}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden sm:block" />
<BreadcrumbItem>
<BreadcrumbPage>{t('navigation.patterns')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<main className="container mx-auto p-4">
<div className="flex items-center justify-center h-64">
<div className="flex flex-col items-center gap-4">
<p className="text-lg text-destructive">Error loading metrics: {error}</p>
</div>
</div>
</main>
</div>
</div>
</SidebarProvider>
)
}
// Prepare data for different charts
const getFilteredData = () => {
let startDate, endDate
const now = new Date()
switch (timeRange) {
case "week":
startDate = sub(now, { days: 7 })
endDate = now
break
case "month":
startDate = sub(now, { months: 1 })
endDate = now
break
case "quarter":
startDate = sub(now, { months: 3 })
endDate = now
break
case "year":
startDate = sub(now, { years: 1 })
endDate = now
break
default:
// All time - use first date in data
const firstTimestamp = metrics.basic_metrics.classification_results[0]?.timestamp
startDate = firstTimestamp ? parseISO(firstTimestamp) : sub(now, { years: 10 })
endDate = now
}
return { startDate, endDate }
}
// Distribution data
const getItemsPerBin = () => {
const data = []
Object.entries(metrics.basic_metrics.items_sorted_per_bin).forEach(([binId, count]) => {
const bin = bins.find(b => b.id === binId)
data.push({
bin: bin?.name || binId,
count,
fill: bin ? (settings?.theme === "dark" ? bin.color_dark : bin.color) : "#888888"
})
})
return data.sort((a, b) => b.count - a.count)
}
// Prepare chart config for items per bin
const getItemsPerBinConfig = () => {
const config = {}
Object.entries(metrics.basic_metrics.items_sorted_per_bin).forEach(([binId, count]) => {
const bin = bins.find(b => b.id === binId)
config[binId] = {
label: bin?.name || binId,
color: bin ? (settings?.theme === "dark" ? bin.color_dark : bin.color) : "#888888"
}
})
return config
}
// Classification methods data
const getClassificationMethods = () => {
const { api, local } = metrics.basic_metrics.api_vs_local_usage
return [
{ name: "API", value: api, fill: settings?.theme === "dark" ? "#3b82f6" : "#2563eb" },
{ name: "Local", value: local, fill: settings?.theme === "dark" ? "#10b981" : "#059669" }
]
}
// Create config for classification methods
const getClassificationMethodsConfig = () => {
return {
api: {
label: "API",
color: settings?.theme === "dark" ? "#3b82f6" : "#2563eb"
},
local: {
label: "Local",
color: settings?.theme === "dark" ? "#10b981" : "#059669"
}
}
}
// Time trends data
const getTimeTrendsData = () => {
const { startDate, endDate } = getFilteredData()
const timeRange = { start: startDate, end: endDate }
// Filter classification results by time range
const results = metrics.basic_metrics.classification_results.filter(result => {
const date = parseISO(result.timestamp)
return isWithinInterval(date, { start: startDate, end: endDate })
})
// Group by day
const dailyData = {}
results.forEach(result => {
const day = format(parseISO(result.timestamp), 'yyyy-MM-dd')
dailyData[day] = (dailyData[day] || 0) + 1
})
// Convert to array format for chart
return Object.entries(dailyData).map(([date, count]) => ({
date,
count,
formattedDate: format(parseISO(date), 'MMM dd')
})).sort((a, b) => parseISO(a.date).getTime() - parseISO(b.date).getTime())
}
// Config for time trends
const timeTrendsConfig = {
count: {
label: "Items Sorted",
color: settings?.theme === "dark" ? "#3b82f6" : "#2563eb"
}
}
// Time of day patterns
const getTimeOfDayData = () => {
const hourData = metrics.time_metrics.time_of_day_patterns
return hourData.map((count, hour) => ({
hour: `${hour}:00`,
count,
fill: count > 0 ? (settings?.theme === "dark" ? "#3b82f6" : "#2563eb") : "#d1d5db"
}))
}
// Config for time of day
const timeOfDayConfig = {
count: {
label: "Items Sorted",
color: settings?.theme === "dark" ? "#3b82f6" : "#2563eb"
}
}
// Most common waste types
const getWasteTypesData = () => {
const wasteTypes = {}
// Collect all waste types from bin specialization
Object.values(metrics.bin_specialization).forEach(bin => {
Object.entries(bin.items_by_type).forEach(([type, count]) => {
wasteTypes[type] = (wasteTypes[type] || 0) + count
})
})
// Convert to array and sort by count
return Object.entries(wasteTypes)
.map(([type, count]) => ({ type, count }))
.sort((a, b) => b.count - a.count)
}
// Config for waste types
const wasteTypesConfig = {
count: {
label: "Items",
color: settings?.theme === "dark" ? "#3b82f6" : "#2563eb"
}
}
// Fill predictions data
const getBinFillPredictions = () => {
if (!predictions || predictions.length === 0) return []
return predictions.map(pred => {
const bin = bins.find(b => b.id === pred.binId)
return {
bin: bin?.name || pred.binId,
currentLevel: pred.currentLevel,
timeUntilFull: Math.round(pred.timeUntilFull * 10) / 10,
predictedDate: format(pred.predictedFullTime, 'MMM dd'),
fillRate: Math.round(pred.fillRate * 100) / 100,
color: bin ? (settings?.theme === "dark" ? bin.color_dark : bin.color) : "#888888"
}
})
}
// Monthly usage data
const getMonthlyData = () => {
return Object.entries(metrics.time_metrics.monthly_usage_counts)
.map(([month, count]) => ({
month: month.substring(5),
count,
fill: settings?.theme === "dark" ? "#3b82f6" : "#2563eb"
}))
.sort((a, b) => a.month.localeCompare(b.month))
}
// Config for monthly data
const monthlyConfig = {
count: {
label: "Items Sorted",
color: settings?.theme === "dark" ? "#3b82f6" : "#2563eb"
}
}
// Fill level history data for line chart
const getFillLevelHistoryConfig = () => {
const config = {}
// Get all unique bin IDs from classification results
const binIds = [...new Set(metrics.basic_metrics.classification_results
.filter(result => result.fill_level > 0)
.map(r => r.bin_id)
)]
// Create config entry for each bin
binIds.forEach(binId => {
const bin = bins.find(b => b.id === binId)
config[binId] = {
label: bin?.name || binId,
color: bin ? (settings?.theme === "dark" ? bin.color_dark : bin.color) : "#888888"
}
})
return config
}
// Environmental impact chart config
const environmentalImpactConfig = {
paper: {
label: "Paper",
color: "#4f46e5"
},
plastic: {
label: "Plastic",
color: "#06b6d4"
},
organic: {
label: "Organic",
color: "#10b981"
}
}
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82ca9d'];
return (
<SidebarProvider>
<div className="flex h-screen w-full">
<AppSidebar />
<div className="flex-1 overflow-auto">
<header className="flex shrink-0 items-center gap-2 border-b px-4 py-6">
<SidebarTrigger />
<Separator orientation="vertical" className="mx-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden sm:block">
<BreadcrumbLink href="/">{t('navigation.home')}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden sm:block" />
<BreadcrumbItem>
<BreadcrumbPage>{t('navigation.patterns')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<main className="container mx-auto py-6 px-4">
<div className="flex flex-col gap-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight">Sorting Analytics</h1>
<p className="text-muted-foreground">Analyze sorting trends and patterns in detail</p>
</div>
<div className="flex items-center gap-2">
<CalendarIcon className="h-4 w-4" />
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-36">
<SelectValue placeholder="Time Range" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Time</SelectItem>
<SelectItem value="week">Last Week</SelectItem>
<SelectItem value="month">Last Month</SelectItem>
<SelectItem value="quarter">Last Quarter</SelectItem>
<SelectItem value="year">Last Year</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Total Items Sorted</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<BarChart2Icon className="h-10 w-10 text-primary" />
<div>
<div className="text-3xl font-bold">{metrics.basic_metrics.total_items_sorted}</div>
<p className="text-xs text-muted-foreground">
Across {Object.keys(metrics.basic_metrics.items_sorted_per_bin).length} bins
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Environmental Impact</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<div className="h-10 w-10 flex items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<span className="text-green-700 dark:text-green-300 text-xl">🌱</span>
</div>
<div>
<div className="text-2xl font-bold">
{metrics.environmental_impact.co2_saved.toFixed(1)} kg COâ‚‚
</div>
<p className="text-xs text-muted-foreground">
{metrics.environmental_impact.trees_saved.toFixed(1)} trees saved
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Next Bin to Fill</CardTitle>
</CardHeader>
<CardContent>
{predictions && predictions.length > 0 ? (
<div className="flex items-center gap-4">
<div
className="h-10 w-10 flex items-center justify-center rounded-full"
style={{
backgroundColor: getBinFillPredictions()[0]?.color || '#888888',
color: 'white'
}}
>
<span className="text-xl font-bold">
{getBinFillPredictions()[0]?.bin.charAt(0).toUpperCase()}
</span>
</div>
<div>
<div className="text-xl font-bold">
{getBinFillPredictions()[0]?.bin}
</div>
<p className="text-xs text-muted-foreground">
{getBinFillPredictions()[0]?.timeUntilFull.toFixed(1)} hours remaining
</p>
</div>
</div>
) : (
<div className="text-muted-foreground">No predictions available</div>
)}
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="mb-4">
<TabsTrigger value="distribution">
<BarChart2Icon className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Sorting Distribution</span>
<span className="sm:hidden">Distribution</span>
</TabsTrigger>
<TabsTrigger value="trends">
<LineChartIcon className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Sorting Trends</span>
<span className="sm:hidden">Trends</span>
</TabsTrigger>
<TabsTrigger value="classifications">
<PieChartIcon className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Top Classifications</span>
<span className="sm:hidden">Classifications</span>
</TabsTrigger>
</TabsList>
<TabsContent value="distribution" className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Items per Bin Type</CardTitle>
<CardDescription>Distribution of sorted items across different bins</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={getItemsPerBinConfig()} className="min-h-[300px]">
<BarChart accessibilityLayer data={getItemsPerBin()}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="bin"
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<YAxis
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Bar
dataKey="count"
nameKey="bin"
radius={[4, 4, 0, 0]}
fill="fill"
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Classification Methods</CardTitle>
<CardDescription>Distribution between API and local classification</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={getClassificationMethodsConfig()} className="min-h-[300px]">
<PieChart>
<Pie
data={getClassificationMethods()}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
dataKey="value"
nameKey="name"
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
>
{getClassificationMethods().map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
<Tooltip
formatter={(value, name) => [`${value} items`, name]}
contentStyle={{
backgroundColor: 'var(--background)',
borderColor: 'var(--border)',
borderRadius: '0.5rem'
}}
/>
</PieChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>Bin Predictions</CardTitle>
<CardDescription>Current fill levels and time until full</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{getBinFillPredictions().map((prediction, index) => (
<div key={index} className="flex flex-col p-4 rounded-lg border">
<div className="flex items-center gap-3 mb-3">
<div
className="h-8 w-8 flex items-center justify-center rounded-full"
style={{
backgroundColor: prediction.color || '#888888',
color: 'white'
}}
>
<span className="text-lg font-bold">
{prediction.bin.charAt(0).toUpperCase()}
</span>
</div>
<span className="font-medium">{prediction.bin}</span>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Current Level</span>
<span className="font-medium">{prediction.currentLevel.toFixed(1)}%</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Time Until Full</span>
<span className="font-medium">{prediction.timeUntilFull.toFixed(1)} hours</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Predicted Date</span>
<span className="font-medium">{prediction.predictedDate}</span>
</div>
<div className="w-full bg-muted rounded-full h-2 mt-2">
<div
className="h-2 rounded-full"
style={{
width: `${prediction.currentLevel}%`,
backgroundColor: prediction.color || '#888888'
}}
></div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="trends" className="space-y-6">
<div className="grid grid-cols-1 gap-6">
<Card>
<CardHeader>
<CardTitle>Daily Sorting Trends</CardTitle>
<CardDescription>Number of items sorted over time</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={timeTrendsConfig} className="min-h-[300px]">
<LineChart accessibilityLayer data={getTimeTrendsData()}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="formattedDate"
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<YAxis
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<ChartTooltip
content={
<ChartTooltipContent
labelKey="date"
nameKey="date"
/>
}
/>
<Line
type="monotone"
dataKey="count"
name="Items Sorted"
stroke={settings?.theme === "dark" ? "#3b82f6" : "#2563eb"}
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Time of Day Patterns</CardTitle>
<CardDescription>When items are most frequently sorted</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={timeOfDayConfig} className="min-h-[300px]">
<BarChart accessibilityLayer data={getTimeOfDayData()}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="hour"
axisLine={false}
tickLine={false}
tickMargin={8}
tick={{ fontSize: 12 }}
interval={3}
/>
<YAxis
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Bar
dataKey="count"
fill="fill"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Monthly Usage</CardTitle>
<CardDescription>Items sorted by month</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={monthlyConfig} className="min-h-[300px]">
<BarChart accessibilityLayer data={getMonthlyData()}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<YAxis
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Bar
dataKey="count"
fill="fill"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="classifications" className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Most Common Waste Types</CardTitle>
<CardDescription>Distribution of waste classifications</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={wasteTypesConfig} className="min-h-[300px]">
<BarChart
accessibilityLayer
data={getWasteTypesData()}
layout="vertical"
margin={{ left: 80 }}
>
<CartesianGrid horizontal={false} />
<XAxis
type="number"
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<YAxis
type="category"
dataKey="type"
axisLine={false}
tickLine={false}
tickMargin={8}
width={80}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Bar
dataKey="count"
nameKey="type"
fill={settings?.theme === "dark" ? "#3b82f6" : "#2563eb"}
radius={[0, 4, 4, 0]}
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Bin Specialization</CardTitle>
<CardDescription>Primary waste type for each bin</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-4">
{Object.entries(metrics.bin_specialization).map(([binId, data]) => {
const bin = bins.find(b => b.id === binId)
return (
<div key={binId} className="flex items-center gap-4 p-3 border rounded-lg">
<div
className="h-10 w-10 flex-shrink-0 flex items-center justify-center rounded-full"
style={{
backgroundColor: bin ? (settings?.theme === "dark" ? bin.color_dark : bin.color) : '#888888',
color: 'white'
}}
>
<span className="text-lg font-bold">
{(bin?.name || binId).charAt(0).toUpperCase()}
</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex flex-wrap justify-between items-center mb-1">
<p className="font-medium truncate">{bin?.name || binId}</p>
<Badge variant="outline" className="ml-2">
{data.total_items} items
</Badge>
</div>
<div className="text-sm text-muted-foreground">
Most common: {data.most_common_type}
</div>
</div>
</div>
)
})}
</div>
</CardContent>
</Card>
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>Emptying Events</CardTitle>
<CardDescription>History of bin emptying by fill level</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr>
<th className="text-left py-3 px-4 font-medium text-muted-foreground">Bin</th>
<th className="text-left py-3 px-4 font-medium text-muted-foreground">Total Empties</th>
<th className="text-left py-3 px-4 font-medium text-muted-foreground">Last Emptied</th>
<th className="text-left py-3 px-4 font-medium text-muted-foreground">Avg. Fill at Empty</th>
<th className="text-left py-3 px-4 font-medium text-muted-foreground">Proactive %</th>
</tr>
</thead>
<tbody className="divide-y">
{Object.entries(metrics.fill_level_history || {}).map(([binId, data]) => {
const bin = bins.find(b => b.id === binId)
const emptyCount = data.emptying_timestamps.length
const lastEmptied = data.emptying_timestamps.length > 0 ?
format(parseISO(data.emptying_timestamps[data.emptying_timestamps.length - 1]), 'MMM dd, yyyy') :
'Never'
// Calculate average fill level at emptying
const avgFill = data.fill_levels_at_empty.length > 0 ?
data.fill_levels_at_empty.reduce((sum, level) => sum + level, 0) / data.fill_levels_at_empty.length :
0
// Calculate % of proactive empties (below 80% full)
const proactiveEmpties = data.fill_levels_at_empty.filter(level => level < 80).length
const proactivePercent = emptyCount > 0 ? (proactiveEmpties / emptyCount) * 100 : 0
return (
<tr key={binId} className="hover:bg-muted/50">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<div
className="h-6 w-6 flex items-center justify-center rounded-full"
style={{
backgroundColor: bin ? (settings?.theme === "dark" ? bin.color_dark : bin.color) : '#888888',
color: 'white'
}}
>
<span className="text-xs font-bold">
{(bin?.name || binId).charAt(0).toUpperCase()}
</span>
</div>
<span>{bin?.name || binId}</span>
</div>
</td>
<td className="py-3 px-4">{emptyCount}</td>
<td className="py-3 px-4">{lastEmptied}</td>
<td className="py-3 px-4">{avgFill.toFixed(1)}%</td>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<span>{proactivePercent.toFixed(0)}%</span>
<div className="w-16 bg-muted rounded-full h-2">
<div
className="h-2 rounded-full"
style={{
width: `${proactivePercent}%`,
backgroundColor: proactivePercent > 50 ? 'rgb(34, 197, 94)' : 'rgb(234, 179, 8)'
}}
></div>
</div>
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
{/* Interactive Features Section */}
<div className="space-y-4 mt-6">
<h2 className="text-2xl font-bold tracking-tight">Interactive Features</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Fill Level History</CardTitle>
<CardDescription>Track fill level changes over time for each bin</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={getFillLevelHistoryConfig()} className="min-h-[300px]">
<LineChart accessibilityLayer data={
// Get the last 20 entries for visibility
metrics.basic_metrics.classification_results
.filter(result => result.fill_level > 0)
.sort((a, b) => parseISO(a.timestamp).getTime() - parseISO(b.timestamp).getTime())
.slice(-20)
.map(result => {
const bin = bins.find(b => b.id === result.bin_id)
return {
date: format(parseISO(result.timestamp), 'MMM dd'),
[result.bin_id]: result.fill_level,
fill: bin ? (settings?.theme === "dark" ? bin.color_dark : bin.color) : '#888888',
binName: bin?.name || result.bin_id
}
})
}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<YAxis
axisLine={false}
tickLine={false}
tickMargin={8}
domain={[0, 100]}
label={{ value: 'Fill Level (%)', angle: -90, position: 'insideLeft', dy: 40 }}
/>
<ChartTooltip
content={
<div className="p-2 bg-background border rounded-md shadow-md">
{({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="p-2">
<p className="font-medium">{label}</p>
{payload.map((entry, index) => (
<div key={`item-${index}`} className="flex items-center gap-2 mt-1">
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: entry.payload.fill }}
/>
<span className="text-sm">
{entry.payload.binName}: {entry.value.toFixed(1)}%
</span>
</div>
))}
</div>
)
}
return null
}}
</div>
}
/>
{/* Add lines dynamically for each bin that has data */}
{[...new Set(metrics.basic_metrics.classification_results
.filter(result => result.fill_level > 0)
.map(r => r.bin_id))]
.map(binId => {
const bin = bins.find(b => b.id === binId)
return (
<Line
key={binId}
type="monotone"
dataKey={binId}
name={bin?.name || binId}
stroke={bin ? (settings?.theme === "dark" ? bin.color_dark : bin.color) : '#888888'}
strokeWidth={2}
dot={{ r: 3 }}
activeDot={{ r: 6 }}
/>
)
})
}
<Legend />
</LineChart>
</ChartContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Environmental Impact</CardTitle>
<CardDescription>Breakdown of environmental benefits by material</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-4">
<div className="flex flex-col p-4 border rounded-lg">
<div className="mb-3">
<span className="text-lg font-medium">Total COâ‚‚ Saved</span>
<div className="text-3xl font-bold mt-1">
{metrics.environmental_impact.co2_saved.toFixed(1)} kg
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">Trees Saved</span>
<span className="font-medium">{metrics.environmental_impact.trees_saved.toFixed(1)}</span>
</div>
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">Paper Recycled</span>
<span className="font-medium">{metrics.environmental_impact.paper_weight_recycled.toFixed(1)} kg</span>
</div>
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">Plastic Recycled</span>
<span className="font-medium">{metrics.environmental_impact.plastic_weight_recycled.toFixed(1)} kg</span>
</div>
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">Organic Processed</span>
<span className="font-medium">{metrics.environmental_impact.organic_weight_processed.toFixed(1)} kg</span>
</div>
</div>
</div>
<ChartContainer config={environmentalImpactConfig} className="min-h-[200px]">
<PieChart>
<Pie
data={[
{ name: 'Paper', value: metrics.environmental_impact.paper_weight_recycled, fill: '#4f46e5' },
{ name: 'Plastic', value: metrics.environmental_impact.plastic_weight_recycled, fill: '#06b6d4' },
{ name: 'Organic', value: metrics.environmental_impact.organic_weight_processed, fill: '#10b981' }
]}
cx="50%"
cy="50%"
outerRadius={80}
innerRadius={50}
dataKey="value"
nameKey="name"
paddingAngle={2}
>
{[
{ name: 'Paper', value: metrics.environmental_impact.paper_weight_recycled, fill: '#4f46e5' },
{ name: 'Plastic', value: metrics.environmental_impact.plastic_weight_recycled, fill: '#06b6d4' },
{ name: 'Organic', value: metrics.environmental_impact.organic_weight_processed, fill: '#10b981' }
].map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
<Tooltip
formatter={(value, name) => [`${value.toFixed(1)} kg`, name]}
contentStyle={{
backgroundColor: 'var(--background)',
borderColor: 'var(--border)',
borderRadius: '0.5rem'
}}
/>
<Legend />
</PieChart>
</ChartContainer>
</div>
</CardContent>
</Card>
{/* Achievements card */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>Achievements Progress</CardTitle>
<CardDescription>Track progress towards unlocking achievements</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
{Object.entries(metrics.achievements)
.sort((a, b) => {
// Sort completed first, then by progress percentage
if (a[1].status === 'completed' && b[1].status !== 'completed') return -1;
if (a[1].status !== 'completed' && b[1].status === 'completed') return 1;
return (b[1].progress / b[1].target) - (a[1].progress / a[1].target);
})
.map(([id, achievement]) => {
const progressPercent = Math.min(100, (achievement.progress / achievement.target) * 100);
const isCompleted = achievement.status === 'completed';
return (
<div key={id} className={`p-4 border rounded-lg ${isCompleted ? 'bg-primary/5' : ''}`}>
<div className="flex justify-between items-start mb-2">
<h4 className="font-medium">{achievement.name}</h4>
{achievement.tier && (
<Badge variant={
achievement.tier === 'gold' ? 'default' :
achievement.tier === 'silver' ? 'secondary' : 'outline'
}>
{achievement.tier}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground mb-3">{achievement.description}</p>
<div className="flex items-center justify-between mt-2 text-sm">
<div>
{isCompleted ? (
<Badge className="bg-green-600">Completed</Badge>
) : (
<span>{achievement.progress} / {achievement.target}</span>
)}
</div>
{achievement.unlock_date && !isCompleted && (
<span className="text-xs text-muted-foreground">
Unlocked: {format(parseISO(achievement.unlock_date), 'MMM dd, yyyy')}
</span>
)}
</div>
<div className="w-full bg-muted rounded-full h-2 mt-2">
<div
className={`h-2 rounded-full ${isCompleted ? 'bg-green-600' : 'bg-primary'}`}
style={{ width: `${progressPercent}%` }}
></div>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</main>
</div>
</div>
</SidebarProvider>
)
}Editor is loading...
Leave a Comment