Untitled
unknown
plain_text
a month ago
55 kB
3
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