Untitled

 avatar
unknown
plain_text
14 days ago
8.8 kB
1
Indexable
"use client"

import { useState, useEffect, useMemo } from "react"
import { format, parseISO, subDays, addDays, isAfter, differenceInDays } 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, 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 } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton"
import { cn } from "@/lib/utils"

const DEFAULT_RANGE = 30
const TIME_RANGES = [
  { label: "Week", value: 7 },
  { label: "Month", value: 30 },
  { label: "Quarter", value: 90 },
  { label: "Year", value: 365 },
  { label: "Custom", value: "custom" },
]

export function EstimateFillLevelChart() {
  const { metrics, helpers } = useMetrics()
  const { predictions } = useFillPredictions()
  const [dateRange, setDateRange] = useState<{ start?: Date; end?: Date }>({})
  const [selectedBin, setSelectedBin] = useState<string>()
  const [selectedRange, setSelectedRange] = useState<number | 'custom'>(DEFAULT_RANGE)

  // Bin selection handling
  const bins = useMemo(() => helpers?.getActiveBinIds() || [], [helpers])
  useEffect(() => setSelectedBin(prev => bins.includes(prev!) ? prev : bins[0]), [bins])

  // Date range calculation
  useEffect(() => {
    if (selectedRange === 'custom') return
    setDateRange({
      start: subDays(new Date(), selectedRange),
      end: new Date()
    })
  }, [selectedRange])

  // Chart data processing
  const chartData = useMemo(() => {
    if (!selectedBin || !helpers || !dateRange.start || !dateRange.end) return []

    const historical = helpers.getFillLevelHistory(selectedBin, dateRange)
    const prediction = predictions.find(p => p.binId === selectedBin)
    if (!historical.length) return []

    const data = historical.map(d => ({
      date: parseISO(d.timestamp),
      value: d.fill_level,
      type: 'historical' as const
    }))

    // Add prediction if available
    if (prediction) {
      const lastPoint = data[data.length - 1]
      const predDate = new Date(prediction.predictedFullTime)
      const daysRemaining = differenceInDays(predDate, lastPoint.date)
      
      if (daysRemaining > 0) {
        const dailyIncrease = (100 - lastPoint.value) / daysRemaining
        for (let i = 1; i <= daysRemaining; i++) {
          data.push({
            date: addDays(lastPoint.date, i),
            value: Math.min(lastPoint.value + dailyIncrease * i, 100),
            type: 'prediction' as const
          })
        }
      }
    }

    return data
  }, [selectedBin, helpers, predictions, dateRange])

  // Chart configuration
  const chartConfig: ChartConfig = {
    historical: { label: "Actual Fill Level", color: "hsl(var(--chart-1))" },
    prediction: { label: "Predicted Fill Level", color: "hsl(var(--chart-2))" }
  }

  if (!helpers || !selectedBin) {
    return (
      <Card>
        <CardHeader>
          <Skeleton className="h-6 w-[200px]" />
          <Skeleton className="h-4 w-[300px]" />
        </CardHeader>
        <CardContent>
          <Skeleton className="h-[400px] w-full" />
        </CardContent>
      </Card>
    )
  }

  return (
    <Card>
      <CardHeader className="pb-2">
        <div className="flex items-center justify-between gap-4">
          <div>
            <CardTitle>Fill Level Projection</CardTitle>
            <CardDescription>
              {selectedBin} - Historical and predicted fill levels
            </CardDescription>
          </div>
          
          <div className="flex gap-2">
            <Select value={selectedBin} onValueChange={setSelectedBin}>
              <SelectTrigger className="w-[140px]">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {bins.map(bin => (
                  <SelectItem key={bin} value={bin}>{bin}</SelectItem>
                ))}
              </SelectContent>
            </Select>

            <Select value={selectedRange.toString()} onValueChange={v => setSelectedRange(v === 'custom' ? v : +v)}>
              <SelectTrigger className="w-[120px]">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {TIME_RANGES.map(({ label, value }) => (
                  <SelectItem key={value} value={value.toString()}>{label}</SelectItem>
                ))}
              </SelectContent>
            </Select>

            {selectedRange === 'custom' && (
              <Popover>
                <PopoverTrigger asChild>
                  <Button variant="outline" className="w-[240px] justify-start">
                    <CalendarIcon className="mr-2 h-4 w-4" />
                    {dateRange.start && dateRange.end ? (
                      `${format(dateRange.start, 'MMM dd')} - ${format(dateRange.end, 'MMM dd')}`
                    ) : 'Select date range'}
                  </Button>
                </PopoverTrigger>
                <PopoverContent className="w-auto p-0">
                  <Calendar
                    mode="range"
                    selected={{ from: dateRange.start, to: dateRange.end }}
                    onSelect={(v) => v && setDateRange({ start: v.from, end: v.to })}
                    numberOfMonths={2}
                  />
                </PopoverContent>
              </Popover>
            )}
          </div>
        </div>
      </CardHeader>

      <CardContent>
        <ChartContainer config={chartConfig} className="h-[400px]">
          <ResponsiveContainer width="100%" height="100%">
            <LineChart data={chartData}>
              <CartesianGrid vertical={false} strokeDasharray="3 3" />
              <XAxis
                dataKey="date"
                tickFormatter={v => format(v, 'MMM dd')}
                axisLine={false}
                tickLine={false}
                minTickGap={40}
              />
              <YAxis
                domain={[0, 100]}
                axisLine={false}
                tickLine={false}
                tickFormatter={v => `${v}%`}
                width={60}
              />

              <ChartTooltip content={({ active, payload }) => (
                <div className={cn(
                  "rounded-lg border bg-popover p-3 shadow-sm",
                  active && payload?.length ? 'opacity-100' : 'opacity-0'
                )}>
                  {payload?.map((entry) => (
                    <div key={entry.name} className="flex items-center gap-2">
                      <div 
                        className="h-2 w-2 rounded-full" 
                        style={{ backgroundColor: entry.color }} 
                      />
                      <div className="flex-1">
                        <div className="text-sm font-medium">
                          {format(entry.payload.date, 'MMM dd, yyyy')}
                        </div>
                        <div className="text-sm">
                          {entry.dataKey === 'prediction' && 'Projected '}
                          Fill Level: {entry.value?.toFixed(1)}%
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              )}/>

              <Line
                type="monotone"
                dataKey="value"
                stroke="var(--color-historical)"
                strokeWidth={2}
                dot={false}
                name="Historical"
                connectNulls
                isAnimationActive={false}
              />

              <Line
                type="monotone"
                dataKey="value"
                stroke="var(--color-prediction)"
                strokeWidth={2}
                strokeDasharray="4 4"
                dot={false}
                name="Prediction"
                connectNulls
                isAnimationActive={false}
                filter={({ type }) => type === 'prediction'}
              />
            </LineChart>
          </ResponsiveContainer>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
Editor is loading...
Leave a Comment