Untitled

 avatar
unknown
plain_text
3 months ago
15 kB
4
Indexable
"use client"

import { useState, useEffect, useMemo } from "react"
import { format, parseISO, subDays, startOfDay, endOfDay } from "date-fns"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Scatter } from "recharts"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { ChartLegend, ChartLegendContent, ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart"
import { useMetrics } from "@/hooks/useMetrics"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Button } from "@/components/ui/button"
import { CalendarIcon, X } from "lucide-react"
import { Calendar } from "@/components/ui/calendar"
import { useRouter, useSearchParams } from "next/navigation"
import { useTranslation, type SupportedLanguages } from "@/utils/translations"

const binColors = {
  bin1: "hsl(var(--chart-1))",
  bin2: "hsl(var(--chart-2))",
  bin3: "hsl(var(--chart-3))",
  bin4: "hsl(var(--chart-4))",
}

const timeRanges = [
  { label: "allTime", value: 9999999 },
  { label: "last24h", value: 1 },
  { label: "lastWeek", value: 7 },
  { label: "lastMonth", value: 30 },
  { label: "lastYear", value: 365 },
  { label: "custom", value: "custom" },
]

export function HistoricalFillChart() {
  const [selectedBin, setSelectedBin] = useState("all")
  const [selectedTimeRange, setSelectedTimeRange] = useState<number | "custom">(9999999)
  const [customDateRange, setCustomDateRange] = useState<{ from?: Date; to?: Date }>({})
  const [isCalendarOpen, setIsCalendarOpen] = useState(false)
  const router = useRouter()
  const searchParams = useSearchParams()
  const { metrics, helpers, loading, error } = useMetrics()
  const { t } = useTranslation()

  const activeBins = useMemo(() => {
    const bins = helpers?.getActiveBinIds() || []
    return [...bins].sort((a, b) => {
      const numA = parseInt(a.replace('bin', ''))
      const numB = parseInt(b.replace('bin', ''))
      return numA - numB
    })
  }, [helpers])

  useEffect(() => {
    const binFromUrl = searchParams.get("bin")
    if (!binFromUrl || binFromUrl === "all") {
      setSelectedBin("all")
    } else if (activeBins.includes(binFromUrl)) {
      setSelectedBin(binFromUrl)
    }
  }, [searchParams, activeBins])

  const formatBinName = (binId: string) => {
    const binNumber = parseInt(binId.replace('bin', ''))
    return `${t('historicalFillChart.binPrefix')} ${binNumber}`
  }

  const handleBinChange = (value: string) => {
    setSelectedBin(value)
    if (value === "all") {
      router.push("?bin=all")
    } else {
      router.push(`?bin=${value}`)
    }
  }

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

    let startDate = new Date()
    let endDate = new Date()

    if (selectedTimeRange === "custom" && customDateRange.from && customDateRange.to) {
      startDate = startOfDay(customDateRange.from)
      endDate = endOfDay(customDateRange.to)
    } else if (typeof selectedTimeRange === "number") {
      startDate = subDays(endDate, selectedTimeRange)
    }

    const timeFilter = { start: startDate, end: endDate }
    const emptyingEvents = helpers.getEmptyingEvents(selectedBin === "all" ? undefined : selectedBin, timeFilter)

    if (selectedBin === "all") {
      const allFillData = activeBins.flatMap(bin => 
        helpers.getFillLevelHistory(bin, timeFilter).map(item => ({
          date: parseISO(item.timestamp),
          binId: bin,
          fillLevel: item.fill_level,
          isEmptied: emptyingEvents.some(e => 
            e.bin_id === bin && 
            parseISO(e.timestamp).getTime() === parseISO(item.timestamp).getTime()
          )
        }))
      )

      allFillData.sort((a, b) => a.date.getTime() - b.date.getTime())

      const lastValues = activeBins.reduce((acc, bin) => {
        acc[bin] = null
        return acc
      }, {} as Record<string, number | null>)

      const normalizedData: Record<number, any> = {}

      allFillData.forEach(item => {
        const timestamp = item.date.getTime()
        if (!normalizedData[timestamp]) {
          normalizedData[timestamp] = {
            date: item.date,
            ...activeBins.reduce((acc, bin) => {
              acc[bin] = lastValues[bin] !== null ? lastValues[bin] : null
              return acc
            }, {} as Record<string, any>)
          }
        }
        normalizedData[timestamp][item.binId] = item.fillLevel
        lastValues[item.binId] = item.fillLevel
      })

      const sortedTimestamps = Object.keys(normalizedData)
        .map(Number)
        .sort((a, b) => a - b)

      return sortedTimestamps.map(t => normalizedData[t])
    }

    const fillData = helpers.getFillLevelHistory(selectedBin, timeFilter)
    return fillData.map(item => ({
      date: parseISO(item.timestamp),
      fillLevel: item.fill_level,
      emptied: emptyingEvents.some(e => 
        e.bin_id === selectedBin && 
        parseISO(e.timestamp).getTime() === parseISO(item.timestamp).getTime()
      )
    }))
  }, [helpers, selectedBin, selectedTimeRange, customDateRange, activeBins])

  const chartConfig: ChartConfig = selectedBin === "all" ? {
    bin1: { label: t('historicalFillChart.binPrefix') + " 1", color: binColors.bin1 },
    bin2: { label: t('historicalFillChart.binPrefix') + " 2", color: binColors.bin2 },
    bin3: { label: t('historicalFillChart.binPrefix') + " 3", color: binColors.bin3 },
    bin4: { label: t('historicalFillChart.binPrefix') + " 4", color: binColors.bin4 },
  } : {
    fillLevel: { 
      label: formatBinName(selectedBin), 
      color: binColors[selectedBin as keyof typeof binColors] 
    },
  }

  const handleTimeRangeChange = (value: string) => {
    const newRange = value === "custom" ? "custom" : Number.parseInt(value)
    setSelectedTimeRange(newRange)
    if (newRange === "custom") setTimeout(() => setIsCalendarOpen(true), 0)
  }

  if (loading) return <div>{t('historicalFillChart.loading')}</div>
  if (error) return <div>{t('historicalFillChart.error')}{error}</div>

  return (
    <Card>
      <CardHeader>
        <CardTitle>{t('historicalFillChart.title')}</CardTitle>
        <CardDescription>{t('historicalFillChart.description')}</CardDescription>
      </CardHeader>
      <CardContent>
        <div className="flex flex-wrap gap-4 mb-4">
          <Select value={selectedBin} onValueChange={handleBinChange}>
            <SelectTrigger className="w-[140px]">
              <SelectValue placeholder={t('historicalFillChart.selectBin')}>
                {selectedBin === 'all' ? t('historicalFillChart.allBins') : formatBinName(selectedBin)}
              </SelectValue>
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">{t('historicalFillChart.allBins')}</SelectItem>
              {activeBins.map(bin => (
                <SelectItem key={bin} value={bin}>
                  {formatBinName(bin)}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          <Select value={selectedTimeRange.toString()} onValueChange={handleTimeRangeChange}>
            <SelectTrigger className="w-[140px]">
              <SelectValue placeholder={t('timeRanges.allTime')} />
            </SelectTrigger>
            <SelectContent>
              {timeRanges.map(range => (
                <SelectItem key={range.value} value={range.value.toString()}>
                  {t(`timeRanges.${range.label}`)}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          {selectedTimeRange === "custom" && (
            <Popover 
              open={isCalendarOpen} 
              onOpenChange={(open) => {
                if (!open && selectedTimeRange === "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>{t('estimatechart.pickDateRange')}</span>
                      )}
                    </div>
                    {selectedTimeRange === "custom" && (customDateRange.from || customDateRange.to) && (
                      <div 
                        className="ml-2"
                        onClick={(e) => {
                          e.stopPropagation()
                          setSelectedTimeRange(9999999)
                          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>
      </CardContent>

      <CardContent>
        <ChartContainer config={chartConfig} className="aspect-auto h-[400px] w-full">
          <ResponsiveContainer width="100%" height="100%">
            <AreaChart data={chartData}>
              <CartesianGrid vertical={false} />
              <XAxis
                dataKey="date"
                tickFormatter={(date) => format(date, "MMM dd")}
                minTickGap={40}
                axisLine={false}
                tickLine={false}
                tickMargin={8}
              />
              <YAxis
                domain={[0, 100]}
                axisLine={false}
                tickLine={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")
                const entries = selectedBin === "all" 
                  ? activeBins.map(bin => ({
                      dataKey: bin,
                      value: payload[0].payload[bin],
                      color: binColors[bin as keyof typeof binColors]
                    })).filter(e => e.value !== null)
                  : payload.filter(p => p.dataKey === "fillLevel")

                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>
                      {entries.map((entry) => (
                        <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>
                      ))}
                    </div>
                  </div>
                )
              }} />

              <ChartLegend content={<ChartLegendContent />} />

              <defs>
                {activeBins.map(bin => (
                  <linearGradient key={bin} id={`fill${bin}`} x1="0" y1="0" x2="0" y2="1">
                    <stop offset="5%" stopColor={`var(--color-${bin})`} stopOpacity={0.8} />
                    <stop offset="95%" stopColor={`var(--color-${bin})`} stopOpacity={0.1} />
                  </linearGradient>
                ))}
                <linearGradient id="fillSingle" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor={`var(--color-${selectedBin})`} stopOpacity={0.8} />
                  <stop offset="95%" stopColor={`var(--color-${selectedBin})`} stopOpacity={0.1} />
                </linearGradient>
              </defs>

              {selectedBin === "all" ? (
                activeBins.map(bin => (
                  <Area
                    key={bin}
                    type="monotone"
                    dataKey={bin}
                    stroke={binColors[bin as keyof typeof binColors]}
                    fill={`url(#fill${bin})`}
                    fillOpacity={0.4}
                    strokeWidth={2}
                    dot={false}
                    connectNulls
                  />
                ))
              ) : (
                <>
                  <Area
                    type="monotone"
                    dataKey="fillLevel"
                    stroke={binColors[selectedBin as keyof typeof binColors]}
                    fill={`url(#fillSingle)`}
                    fillOpacity={0.4}
                    strokeWidth={2}
                    dot={false}
                    connectNulls
                  />
                  <Scatter
                    data={chartData.filter((d: any) => d.emptied)}
                    dataKey="fillLevel"
                    fill="var(--chart-5)"
                    shape={(props) => (
                      <circle {...props} r={6} strokeWidth={2} stroke="var(--background)" />
                    )}
                  />
                </>
              )}
            </AreaChart>
          </ResponsiveContainer>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
Editor is loading...
Leave a Comment