Untitled

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

import { useState, useEffect, useMemo } from "react"
import { format, parseISO, subDays, addDays, isAfter } from "date-fns"
import { CartesianGrid, Area, AreaChart, XAxis, YAxis, ResponsiveContainer } 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, type ChartConfig } from "@/components/ui/chart"
import { useMetrics } from "@/hooks/useMetrics"
import { useFillPredictions } from "@/hooks/useFillPredictions"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Button } from "@/components/ui/button"
import { CalendarIcon, X } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
import { useTranslation, type SupportedLanguages } from "@/utils/translations"
import { useSettings } from "@/hooks/useSettings"

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

export function EstimateFillLevelChart() {
  const { settings } = useSettings()
  const { t } = useTranslation(settings?.language as SupportedLanguages || 'EN')
  
  const timeRanges = [
    { label: t('timeRanges.allTime'), value: 9999999 },
    { label: t('timeRanges.last24h'), value: 1 },
    { label: t('timeRanges.lastWeek'), value: 7 },
    { label: t('timeRanges.lastMonth'), value: 30 },
    { label: t('timeRanges.lastYear'), value: 365 },
    { label: t('timeRanges.custom'), value: "custom" },
  ]

  const [isCalendarOpen, setIsCalendarOpen] = useState(false)
  const router = useRouter()
  const searchParams = useSearchParams()
  const [selectedBin, setSelectedBin] = useState<string | null>(null)
  const [selectedTimeRange, setSelectedTimeRange] = useState<number | "custom">(9999999)
  const [customDateRange, setCustomDateRange] = useState<{ from: Date | undefined; to: Date | undefined }>({
    from: undefined,
    to: undefined,
  })
  const { metrics, helpers, loading, error } = useMetrics()
  const { predictions } = useFillPredictions()

  const activeBinIds = useMemo(() => {
    if (!helpers) return []
    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 && activeBinIds.includes(binFromUrl)) {
      setSelectedBin(binFromUrl)
    } else if (activeBinIds.length > 0) {
      setSelectedBin(activeBinIds[0])
    }
  }, [searchParams, activeBinIds])

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

    let startDate: Date
    let endDate = new Date()

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

    const historicalData = helpers.getFillLevelHistory(selectedBin, { start: startDate, end: endDate })
    const prediction = predictions.find((p) => p.binId === selectedBin)

    const data = historicalData.map((item) => ({
      date: parseISO(item.timestamp),
      fillLevel: item.fill_level,
    }))

    if (prediction && data.length > 0) {
      const lastDataPoint = data[data.length - 1]
      const predictedFullDate = new Date(prediction.predictedFullTime)

      if (isAfter(predictedFullDate, lastDataPoint.date)) {
        const daysUntilFull = (predictedFullDate.getTime() - lastDataPoint.date.getTime()) / (1000 * 60 * 60 * 24)
        const fillRatePerDay = (100 - lastDataPoint.fillLevel) / daysUntilFull

        for (let i = 1; i <= daysUntilFull; i++) {
          const predictedDate = addDays(lastDataPoint.date, i)
          const predictedFillLevel = Math.min(lastDataPoint.fillLevel + fillRatePerDay * i, 100)
          data.push({
            date: predictedDate,
            predictedFillLevel: predictedFillLevel,
          })
        }
      }
    }

    return data
  }, [helpers, selectedBin, selectedTimeRange, customDateRange, predictions])

  const chartConfig: ChartConfig = useMemo(() => ({
    fillLevel: {
      label: selectedBin ? `${formatBinName(selectedBin, t('estimatechart.binPrefix'))} - ${t('estimatechart.current')}` : t('estimatechart.fillLevel'),
      color: "hsl(var(--chart-1))",
    },
    predictedFillLevel: {
      label: selectedBin ? `${formatBinName(selectedBin, t('estimatechart.binPrefix'))} - ${t('estimatechart.predicted')}` : t('estimatechart.fillLevel'),
      color: "hsl(var(--chart-2))",
    },
  }), [selectedBin, t])

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

  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('estimatechart.title')}</CardTitle>
        <CardDescription>{t('estimatechart.description')}</CardDescription>
      </CardHeader>
      <CardContent>
        <div className="flex flex-wrap gap-4 mb-4">
          <Select value={selectedBin || undefined} onValueChange={handleBinChange}>
            <SelectTrigger className="w-[140px]">
              <SelectValue placeholder={t('estimatechart.selectBin')}>
                {selectedBin ? formatBinName(selectedBin, t('estimatechart.binPrefix')) : t('estimatechart.selectBin')}
              </SelectValue>
            </SelectTrigger>
            <SelectContent>
              {activeBinIds.map((binId) => (
                <SelectItem key={binId} value={binId}>
                  {formatBinName(binId, t('estimatechart.binPrefix'))}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Select value={selectedTimeRange.toString()} onValueChange={handleTimeRangeChange}>
            <SelectTrigger className="w-[140px]">
              <SelectValue placeholder={t('estimatechart.selectTimeRange')} />
            </SelectTrigger>
            <SelectContent>
              {timeRanges.map((range) => (
                <SelectItem key={range.value} value={range.value.toString()}>
                  {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>
        <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={(value) => format(value, "MMM dd")} axisLine={false} tickLine={false} tickMargin={8} minTickGap={32} />
              <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")
                  
                  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) => (
                          <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>
                <linearGradient id="fillLevel" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor="var(--color-fillLevel)" stopOpacity={0.8} />
                  <stop offset="95%" stopColor="var(--color-fillLevel)" stopOpacity={0.1} />
                </linearGradient>
                <linearGradient id="predictedFillLevel" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor="var(--color-predictedFillLevel)" stopOpacity={0.8} />
                  <stop offset="95%" stopColor="var(--color-predictedFillLevel)" stopOpacity={0.1} />
                </linearGradient>
              </defs>
              <Area
                type="monotone"
                dataKey="fillLevel"
                stroke="var(--color-fillLevel)"
                fill="url(#fillLevel)"
                fillOpacity={0.4}
                strokeWidth={2}
                dot={false}
                name="Fill Level"
                connectNulls={false}
              />
              <Area
                type="monotone"
                dataKey="predictedFillLevel"
                stroke="var(--color-predictedFillLevel)"
                fill="url(#predictedFillLevel)"
                fillOpacity={0.4}
                strokeWidth={2}
                strokeDasharray="10 10"
                dot={false}
                name="Predicted Fill Level"
                connectNulls
              />
            </AreaChart>
          </ResponsiveContainer>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
Editor is loading...
Leave a Comment