Untitled

 avatar
unknown
plain_text
9 days ago
9.9 kB
2
Indexable
"use client"

import { useState, useEffect, useMemo } from "react"
import { format, parseISO, subDays, addDays, isAfter } from "date-fns"
import { CartesianGrid, Line, LineChart, 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, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart"
import { ChartEmptyState } from "@/components/ui/chart-empty-state"
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 } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"

const timeRanges = [
  { label: "Last 7 days", value: 7 },
  { label: "Last 30 days", value: 30 },
  { label: "Last 90 days", value: 90 },
  { label: "Last 180 days", value: 180 },
  { label: "Last 365 days", value: 365 },
  { label: "Custom", value: "custom" },
]

export function EstimateFillLevelChart() {
  const router = useRouter()
  const searchParams = useSearchParams()
  const [selectedBin, setSelectedBin] = useState<string | null>(null)
  const [selectedTimeRange, setSelectedTimeRange] = useState<number | "custom">(30)
  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 []
    return helpers.getActiveBinIds()
  }, [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) // Default to 30 days if something goes wrong
    }

    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

        // Add the prediction starting from the last historical point
        data.push({
          date: lastDataPoint.date,
          fillLevel: lastDataPoint.fillLevel,
          predictedFillLevel: lastDataPoint.fillLevel
        })

        // Then add the future predictions
        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 = {
    fillLevel: {
      label: "Fill Level",
      color: "hsl(var(--chart-1))",
    },
    predictedFillLevel: {
      label: "Predicted Fill Level",
      color: "hsl(var(--chart-2))",
    },
  }

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

  const handleTimeRangeChange = (value: string) => {
    setSelectedTimeRange(value === "custom" ? "custom" : Number.parseInt(value))
  }

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error}</div>

  const hasHistoricalData = chartData.some(data => data.fillLevel !== undefined)
  const hasPredictionData = chartData.some(data => data.predictedFillLevel !== undefined)

  const renderChartContent = () => {
    if (!selectedBin) {
      return (
        <ChartEmptyState
          title="No Bin Selected"
          description="Please select a bin to view its fill level data and predictions."
        />
      )
    }

    if (!hasHistoricalData && !hasPredictionData) {
      return (
        <ChartEmptyState
          title="No Data Available"
          description="There is no historical data or predictions available for the selected time range."
        />
      )
    }

    if (!hasHistoricalData) {
      return (
        <ChartEmptyState
          title="No Historical Data"
          description="There is no historical fill level data available for the selected time range."
        />
      )
    }

    if (!hasPredictionData) {
      return (
        <ChartEmptyState
          title="No Prediction Data"
          description="There are no fill level predictions available for this bin."
        />
      )
    }

    return (
      <ResponsiveContainer width="100%" height="100%">
        <LineChart 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={<ChartTooltipContent />} />
          <ChartLegend content={<ChartLegendContent />} />
          <Line
            type="monotone"
            dataKey="fillLevel"
            stroke="var(--color-fillLevel)"
            strokeWidth={2}
            dot={false}
            name="Fill Level"
            connectNulls={false}
          />
          <Line
            type="monotone"
            dataKey="predictedFillLevel"
            stroke="var(--color-predictedFillLevel)"
            strokeWidth={2}
            strokeDasharray="10 10"
            dot={false}
            name="Predicted Fill Level"
            connectNulls
          />
        </LineChart>
      </ResponsiveContainer>
    )
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle>Estimate Fill Level Chart</CardTitle>
        <CardDescription>Historical and predicted fill levels</CardDescription>
      </CardHeader>
      <CardContent>
        <div className="flex flex-wrap gap-4 mb-4">
          <Select value={selectedBin || undefined} onValueChange={handleBinChange}>
            <SelectTrigger className="w-[180px]">
              <SelectValue placeholder="Select a bin" />
            </SelectTrigger>
            <SelectContent>
              {activeBinIds.map((binId) => (
                <SelectItem key={binId} value={binId}>
                  {binId}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Select value={selectedTimeRange.toString()} onValueChange={handleTimeRangeChange}>
            <SelectTrigger className="w-[180px]">
              <SelectValue placeholder="Select time range" />
            </SelectTrigger>
            <SelectContent>
              {timeRanges.map((range) => (
                <SelectItem key={range.value} value={range.value.toString()}>
                  {range.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          {selectedTimeRange === "custom" && (
            <Popover>
              <PopoverTrigger asChild>
                <Button variant="outline" className="w-[280px] justify-start text-left font-normal">
                  <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>
                  )}
                </Button>
              </PopoverTrigger>
              <PopoverContent className="w-auto p-0" align="start">
                <Calendar
                  initialFocus
                  mode="range"
                  defaultMonth={customDateRange.from}
                  selected={customDateRange}
                  onSelect={setCustomDateRange}
                  numberOfMonths={2}
                />
              </PopoverContent>
            </Popover>
          )}
        </div>
        <ChartContainer config={chartConfig} className="aspect-auto h-[400px] w-full">
          {renderChartContent()}
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
Leave a Comment