Untitled

 avatar
unknown
plain_text
2 months ago
11 kB
3
Indexable
"use client"

import { useState, useMemo } from "react"
import { format } from "date-fns"
import { CalendarIcon, X } from "lucide-react"
import { CartesianGrid, Line, LineChart, XAxis, YAxis, ResponsiveContainer } from "recharts"
import { useMetrics } from "@/hooks/useMetrics"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
} from "@/components/ui/chart"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"

const periods = [
  { label: "All Time", value: "all" },
  { label: "Last 24h", value: "24h" },
  { label: "Last Week", value: "week" },
  { label: "Last Month", value: "month" },
  { label: "Last Year", value: "year" },
  { label: "Custom", value: "custom" },
]

export function HistoricalFillLevelChart() {
  const { metrics, helpers } = useMetrics()
  const [selectedPeriod, setSelectedPeriod] = useState("all")
  const [isCalendarOpen, setIsCalendarOpen] = useState(false)
  const [customDateRange, setCustomDateRange] = useState<{ from: Date | undefined; to: Date | undefined }>({
    from: undefined,
    to: undefined,
  })

  const handlePeriodChange = (value: string) => {
    setSelectedPeriod(value)
    if (value === "custom") {
      setTimeout(() => {
        setIsCalendarOpen(true)
      }, 0)
    }
  }

  const chartData = useMemo(() => {
    if (!metrics || !helpers) return []

    const now = new Date()
    let timeFilter

    if (selectedPeriod === "all") {
      timeFilter = undefined
    } else if (selectedPeriod === "custom") {
      timeFilter = customDateRange.from && customDateRange.to 
        ? { start: customDateRange.from, end: customDateRange.to }
        : undefined
    } else {
      let startDate = new Date(now)
      switch (selectedPeriod) {
        case "24h":
          startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000)
          break
        case "week":
          startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
          break
        case "month":
          startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate())
          break
        case "year":
          startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
          break
      }
      timeFilter = { start: startDate, end: now }
    }

    // Get historical data for all bins
    const bins = helpers.getActiveBinIds()
    const allData: Record<string, Array<{ date: Date; fillLevel: number; emptied?: boolean }>> = {}

    bins.forEach(binId => {
      const history = helpers.getFillLevelHistory(binId, timeFilter)
      const emptyingEvents = helpers.getEmptyingEvents(binId, timeFilter)

      allData[binId] = history.map(item => ({
        date: new Date(item.timestamp),
        fillLevel: item.fill_level,
        emptied: emptyingEvents.some(event => 
          Math.abs(new Date(event.timestamp).getTime() - new Date(item.timestamp).getTime()) < 1000 * 60 * 5 // Within 5 minutes
        )
      }))
    })

    // Combine all data points into a single array with timestamps as keys
    const combinedData: Array<{ date: Date } & Record<string, number | null>> = []
    const allDates = new Set<string>()

    Object.entries(allData).forEach(([binId, data]) => {
      data.forEach(point => {
        allDates.add(point.date.toISOString())
      })
    })

    Array.from(allDates).sort().forEach(dateStr => {
      const dataPoint: any = { date: new Date(dateStr) }
      bins.forEach(binId => {
        const point = allData[binId].find(p => p.date.toISOString() === dateStr)
        if (point) {
          dataPoint[binId] = point.fillLevel
          if (point.emptied) {
            dataPoint[`${binId}Emptied`] = point.fillLevel
          }
        }
      })
      combinedData.push(dataPoint)
    })

    return combinedData
  }, [metrics, selectedPeriod, helpers, customDateRange])

  const chartConfig: ChartConfig = {
    bin1: {
      label: "Organic",
      color: "hsl(var(--chart-1))",
    },
    bin2: {
      label: "Recycling",
      color: "hsl(var(--chart-2))",
    },
    bin3: {
      label: "Paper",
      color: "hsl(var(--chart-3))",
    },
    bin4: {
      label: "Residual",
      color: "hsl(var(--chart-4))",
    },
  }

  return (
    <Card>
      <CardHeader className="flex flex-row items-center justify-between gap-4">
        <div>
          <CardTitle>Historical Fill Levels</CardTitle>
          <CardDescription>Fill level trends over time for all bins</CardDescription>
        </div>
        <div className="flex gap-2">
          <Select value={selectedPeriod} onValueChange={handlePeriodChange}>
            <SelectTrigger className="w-[120px]">
              <SelectValue placeholder="Period" />
            </SelectTrigger>
            <SelectContent>
              {periods.map((period) => (
                <SelectItem key={period.value} value={period.value}>
                  {period.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          {selectedPeriod === "custom" && (
            <Popover 
              open={isCalendarOpen} 
              onOpenChange={(open) => {
                if (!open && selectedPeriod === "custom") return
                setIsCalendarOpen(open)
              }}
            >              
              <PopoverTrigger asChild>
                <Button variant="outline" className="w-[280px] justify-start text-left font-normal">
                  <div className="flex items-center justify-between w-full">
                    <div className="flex items-center">
                      <CalendarIcon className="mr-2 h-4 w-4" />
                      {customDateRange.from ? (
                        customDateRange.to ? (
                          <>
                            {format(customDateRange.from, "LLL dd, y")} - {format(customDateRange.to, "LLL dd, y")}
                          </>
                        ) : (
                          format(customDateRange.from, "LLL dd, y")
                        )
                      ) : (
                        <span>Pick a date range</span>
                      )}
                    </div>
                    {selectedPeriod === "custom" && (customDateRange.from || customDateRange.to) && (
                      <div 
                        className="ml-2"
                        onClick={(e) => {
                          e.stopPropagation()
                          setSelectedPeriod("all")
                          setCustomDateRange({ from: undefined, to: undefined })
                        }}
                      >
                        <X className="h-4 w-4 text-muted-foreground hover:text-foreground" />
                      </div>
                    )}
                  </div>
                </Button>
              </PopoverTrigger>
              <PopoverContent className="w-auto p-0" align="start">
                <Calendar
                  initialFocus
                  mode="range"
                  defaultMonth={customDateRange.from}
                  selected={customDateRange}
                  onSelect={(range) => {
                    setCustomDateRange(range)
                    if (range?.from && range?.to) {
                      setIsCalendarOpen(false)
                    }
                  }}
                  numberOfMonths={2}
                />
              </PopoverContent>
            </Popover>
          )}
        </div>
      </CardHeader>

      <CardContent>
        <ChartContainer config={chartConfig} className="aspect-auto h-[400px] w-full">
          <ResponsiveContainer width="100%" height="100%">
            <LineChart data={chartData}>
              <CartesianGrid vertical={false} />
              <XAxis
                dataKey="date"
                tickLine={false}
                axisLine={false}
                tickMargin={8}
                tickFormatter={(value) => format(value, "MMM dd")}
                minTickGap={32}
              />
              <YAxis
                domain={[0, 100]}
                tickLine={false}
                axisLine={false}
                tickFormatter={(value) => `${value}%`}
              />
              <ChartTooltip
                content={({ active, payload }) => {
                  if (!active || !payload?.length) return null
                  
                  const date = format(payload[0].payload.date, "MMM dd, yyyy HH:mm")
                  
                  return (
                    <div className="rounded-lg border bg-background p-2 shadow-sm">
                      <div className="grid gap-2">
                        <div className="text-sm font-medium">{date}</div>
                        {payload.map((entry) => {
                          if (!entry.dataKey.includes("Emptied")) {
                            return (
                              <div key={entry.dataKey} className="flex items-center gap-2">
                                <div 
                                  className="h-2 w-2 rounded-full" 
                                  style={{ background: entry.color }} 
                                />
                                <span className="text-sm text-muted-foreground">
                                  {chartConfig[entry.dataKey].label}:
                                </span>
                                <span className="text-sm font-medium">
                                  {entry.value?.toFixed(0)}%
                                </span>
                              </div>
                            )
                          }
                          return null
                        })}
                      </div>
                    </div>
                  )
                }}
              />
              <ChartLegend content={<ChartLegendContent />} />

              {Object.keys(chartConfig).map((binId) => (
                <Line
                  key={binId}
                  type="monotone"
                  dataKey={binId}
                  stroke={`var(--color-${binId})`}
                  strokeWidth={2}
                  dot={false}
                  connectNulls
                />
              ))}

              {/* Emptying event markers */}
              {Object.keys(chartConfig).map((binId) => (
                <Line
                  key={`${binId}Emptied`}
                  type="monotone"
                  dataKey={`${binId}Emptied`}
                  stroke={`var(--color-${binId})`}
                  strokeWidth={0}
                  dot={{
                    r: 4,
                    fill: "var(--background)",
                    stroke: `var(--color-${binId})`,
                    strokeWidth: 2,
                  }}
                  activeDot={false}
                  connectNulls={false}
                />
              ))}
            </LineChart>
          </ResponsiveContainer>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
Editor is loading...
Leave a Comment