Files
ember-market-frontend/components/analytics/PredictionsChart.tsx
g a05787a091
All checks were successful
Build Frontend / build (push) Successful in 1m11s
Revamp analytics dashboard UI and charts
Enhanced the AnalyticsDashboard layout with a premium glassmorphism UI, improved toolbar, and reorganized tabs for better clarity. MetricsCard now features dynamic color coding and trend badges. PredictionsChart received scenario simulation UI upgrades, disabled future ranges based on available history, and improved chart tooltips and visuals. ProfitAnalyticsChart added error handling for product images and minor UI refinements. Updated globals.css with new premium utility classes and improved dark mode color variables.
2026-01-12 05:44:54 +00:00

925 lines
41 KiB
TypeScript

"use client";
import { useState, useEffect, memo, useMemo, useCallback } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
TrendingUp,
TrendingDown,
Package,
DollarSign,
AlertTriangle,
RefreshCw,
Calendar,
BarChart3,
Brain,
Layers,
Zap,
Info,
Download,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton";
import CountUp from "react-countup";
import {
getPredictionsOverviewWithStore,
getStockPredictionsWithStore,
type PredictionsOverview,
type StockPredictionsResponse,
} from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { format } from "date-fns";
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
ResponsiveContainer,
} from "recharts";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Slider } from "@/components/ui/slider";
interface PredictionsChartProps {
timeRange?: number;
}
export default function PredictionsChart({
timeRange = 90,
}: PredictionsChartProps) {
const [predictions, setPredictions] = useState<PredictionsOverview | null>(
null,
);
const [baselinePredictions, setBaselinePredictions] = useState<PredictionsOverview | null>(
null,
);
// Batch data holds all pre-cached predictions for instant switching
const [batchData, setBatchData] = useState<{
[horizon: string]: {
[simulationFactor: string]: PredictionsOverview;
};
} | null>(null);
const [stockPredictions, setStockPredictions] =
useState<StockPredictionsResponse | null>(null);
const [loading, setLoading] = useState(true);
const [isSimulating, setIsSimulating] = useState(false);
const [daysAhead, setDaysAhead] = useState(7);
const [activeTab, setActiveTab] = useState<"overview" | "stock">("overview");
const [simulationFactor, setSimulationFactor] = useState(0);
const [committedSimulationFactor, setCommittedSimulationFactor] = useState(0);
const { toast } = useToast();
// Fetch all predictions in batch (for instant client-side switching)
const fetchBatchData = async () => {
try {
setLoading(true);
const { getBatchPredictionsWithStore } = await import("@/lib/services/analytics-service");
const [batchResponse, stock] = await Promise.all([
getBatchPredictionsWithStore(timeRange),
getStockPredictionsWithStore(timeRange),
]);
if (batchResponse.success && batchResponse.predictions) {
setBatchData(batchResponse.predictions);
// Set initial predictions from batch
const horizonData = batchResponse.predictions[daysAhead.toString()];
if (horizonData) {
const baseline = horizonData["0"];
if (baseline) {
setBaselinePredictions(baseline);
setPredictions(baseline);
}
}
} else {
// Fallback to single request if batch not available
const overview = await getPredictionsOverviewWithStore(daysAhead, timeRange, 0);
setBaselinePredictions(overview);
setPredictions(overview);
}
setStockPredictions(stock);
} catch (error) {
console.error("Error fetching predictions:", error);
toast({
title: "Error",
description: "Failed to load predictions",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
// Switch predictions from batch data (no API call!)
const switchPredictions = useCallback((horizon: number, simFactor: number) => {
if (!batchData) return;
const horizonData = batchData[horizon.toString()];
if (!horizonData) return;
// Simulation factor is stored as decimal (e.g., 0.1 for 10%)
const simKey = (simFactor / 100).toString();
const newPrediction = horizonData[simKey];
if (newPrediction) {
setPredictions(newPrediction);
if (simFactor === 0) {
setBaselinePredictions(newPrediction);
}
}
}, [batchData]);
// Fetch batch data on initial load or when timeRange changes
useEffect(() => {
fetchBatchData();
setCommittedSimulationFactor(0);
setSimulationFactor(0);
}, [timeRange]);
// Auto-adjust daysAhead if it exceeds historical timeRange
useEffect(() => {
if (daysAhead > timeRange) {
setDaysAhead(timeRange);
}
}, [timeRange, daysAhead]);
// Switch predictions when daysAhead changes (instant, from batch)
useEffect(() => {
if (batchData) {
switchPredictions(daysAhead, committedSimulationFactor);
}
}, [daysAhead, batchData, switchPredictions]);
// Switch predictions when simulation factor changes (instant, from batch)
useEffect(() => {
if (batchData) {
switchPredictions(daysAhead, committedSimulationFactor);
}
}, [committedSimulationFactor, batchData, switchPredictions]);
const getConfidenceColor = (confidence: string) => {
switch (confidence) {
case "very_high":
return "bg-emerald-600/20 text-emerald-700 dark:text-emerald-400 border-emerald-600/30";
case "high":
return "bg-green-500/10 text-green-700 dark:text-green-400";
case "medium":
return "bg-yellow-500/10 text-yellow-700 dark:text-yellow-400";
case "low":
return "bg-red-500/10 text-red-700 dark:text-red-400";
default:
return "bg-gray-500/10 text-gray-700 dark:text-gray-400";
}
};
const getConfidenceLabel = (confidence: string) => {
switch (confidence) {
case "very_high":
return "Very High";
case "high":
return "High";
case "medium":
return "Medium";
case "low":
return "Low";
default:
return confidence;
}
};
// Combine baseline and simulated data for overlay chart
const chartData = useMemo(() => {
if (!predictions?.sales?.dailyPredictions) return [];
const baselineData = baselinePredictions?.sales?.dailyPredictions || [];
const simulatedDailyData = predictions?.sales?.dailyPredictions || [];
return simulatedDailyData.map((d: any, idx: number) => ({
...d,
formattedDate: format(new Date(d.date), "MMM d"),
simulated: d.predicted,
baseline: baselineData[idx]?.predicted ?? d.predicted,
orders: d.predictedOrders || 0,
}));
}, [predictions, baselinePredictions]);
// Keep simulatedData for export compatibility
const simulatedData = chartData;
const handleExportCSV = () => {
if (!simulatedData.length) return;
// Create CSV headers
const headers = ["Date", "Baseline Revenue", "Simulated Revenue", "Orders", "Confidence"];
// Create CSV rows
const rows = simulatedData.map(d => [
format(new Date(d.date), "yyyy-MM-dd"),
d.baseline?.toFixed(2) || "",
d.simulated?.toFixed(2) || "",
d.orders || "",
predictions?.sales?.confidence || "unknown"
]);
// Combine headers and rows
const csvContent = [
headers.join(","),
...rows.map(row => row.join(","))
].join("\n");
// Create download link
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `predictions_export_${format(new Date(), "yyyyMMdd")}.csv`);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
if (loading) {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-72" />
</CardHeader>
<CardContent>
<div className="space-y-4">
<Skeleton className="h-32 w-full" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-32 w-full" />
</div>
</CardContent>
</Card>
);
}
if (!predictions) {
return (
<Card>
<CardHeader>
<CardTitle>Predictions</CardTitle>
<CardDescription>
Forecast future sales, demand, and stock levels
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-center py-8">
No prediction data available. Need more historical data.
</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
Predictions & Forecasting
</CardTitle>
<CardDescription>
{predictions?.sales?.aiModel?.used
? "AI neural network + statistical models for sales, demand, and inventory"
: "AI-powered predictions for sales, demand, and inventory"}
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Select
value={daysAhead.toString()}
onValueChange={(value) => setDaysAhead(parseInt(value))}
>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">7 days</SelectItem>
<SelectItem value="14" disabled={timeRange < 14}>
14 days {timeRange < 14 && "(Needs 14d history)"}
</SelectItem>
<SelectItem value="30" disabled={timeRange < 30}>
30 days {timeRange < 30 && "(Needs 30d history)"}
</SelectItem>
<SelectItem value="60" disabled={timeRange < 60}>
60 days {timeRange < 60 && "(Needs 60d history)"}
</SelectItem>
<SelectItem value="90" disabled={timeRange < 90}>
90 days {timeRange < 90 && "(Needs 90d history)"}
</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
onClick={fetchBatchData}
disabled={loading || isSimulating}
>
<RefreshCw
className={`h-4 w-4 ${loading || isSimulating ? "animate-spin" : ""}`}
/>
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex gap-2 mb-6">
<Button
variant={activeTab === "overview" ? "default" : "outline"}
onClick={() => setActiveTab("overview")}
size="sm"
>
Overview
</Button>
<Button
variant={activeTab === "stock" ? "default" : "outline"}
onClick={() => setActiveTab("stock")}
size="sm"
>
Stock Predictions
</Button>
</div>
{activeTab === "overview" && (
<div className="space-y-6">
{/* Sales Predictions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<TooltipProvider delayDuration={0}>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<DollarSign className="h-4 w-4" />
Revenue Prediction
</CardTitle>
</CardHeader>
<CardContent>
{predictions?.sales?.predicted !== null && predictions?.sales?.predicted !== undefined ? (
<div className="space-y-2">
<Tooltip>
<TooltipTrigger asChild>
<div className="text-2xl font-bold w-fit cursor-help">
<CountUp
end={predictions?.sales?.predicted || 0}
duration={1.5}
separator=","
decimals={2}
prefix="£"
/>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="z-[100]">
<p>Predicted daily average revenue for the next {daysAhead} days</p>
</TooltipContent>
</Tooltip>
<div className="flex items-center gap-2 flex-wrap">
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex cursor-help">
<Badge
className={getConfidenceColor(
predictions?.sales?.confidence || "low",
)}
>
{getConfidenceLabel(predictions?.sales?.confidence || "low")} Confidence
{predictions?.sales?.confidenceScore !== undefined && (
<span className="ml-1 opacity-75">
({Math.round((predictions?.sales?.confidenceScore || 0) * 100)}%)
</span>
)}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="z-[100]">
<p>Based on data consistency, historical accuracy, and model agreement</p>
</TooltipContent>
</Tooltip>
{predictions?.sales?.aiModel?.used && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex cursor-help">
<Badge variant="outline" className="bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-500/30">
🤖 AI Powered
{predictions?.sales?.aiModel?.modelAccuracy !== undefined && (
<span className="ml-1 opacity-75">
({Math.round((predictions?.sales?.aiModel?.modelAccuracy || 0) * 100)}%)
</span>
)}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="z-[100]">
<p>Predictions generated using a Deep Learning Ensemble Model</p>
</TooltipContent>
</Tooltip>
)}
{predictions?.sales?.trend && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex cursor-help">
<Badge
variant="outline"
className={
predictions?.sales?.trend?.direction === "up"
? "text-green-600 border-green-600"
: predictions?.sales?.trend?.direction === "down"
? "text-red-600 border-red-600"
: ""
}
>
{predictions?.sales?.trend?.direction === "up" && (
<TrendingUp className="h-3 w-3 mr-1" />
)}
{predictions?.sales?.trend?.direction === "down" && (
<TrendingDown className="h-3 w-3 mr-1" />
)}
{predictions?.sales?.trend?.direction === "up"
? "Trending Up"
: predictions?.sales?.trend?.direction === "down"
? "Trending Down"
: "Stable"}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="z-[100]">
<p>Direction of the recent sales trend (slope analysis)</p>
</TooltipContent>
</Tooltip>
)}
<span className="text-xs text-muted-foreground">
Next {daysAhead} days
</span>
</div>
{predictions?.sales?.predictedOrders && (
<div className="text-sm text-muted-foreground">
~{Math.round(predictions?.sales?.predictedOrders || 0)}{" "}
orders
</div>
)}
{!predictions?.sales?.confidenceIntervals &&
predictions?.sales?.minPrediction &&
predictions?.sales?.maxPrediction && (
<div className="text-xs text-muted-foreground">
Range: {formatGBP(predictions?.sales?.minPrediction || 0)} -{" "}
{formatGBP(predictions?.sales?.maxPrediction || 0)}
</div>
)}
</div>
) : (
<div className="text-sm text-muted-foreground">
{predictions?.sales?.message ||
"Insufficient data for prediction"}
</div>
)}
</CardContent>
</Card>
{/* Model Intelligence Card */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Brain className="h-4 w-4" />
Model Intelligence
<Tooltip>
<TooltipTrigger>
<Info className="h-3 w-3 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="bottom" className="z-[100]">
<p>Technical details about the active prediction model</p>
</TooltipContent>
</Tooltip>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Architecture</span>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-sm font-medium flex items-center gap-1 cursor-help">
<Layers className="h-3 w-3 text-purple-500" />
Hybrid Ensemble (Deep Learning)
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="z-[100]">
<p>Combines LSTM Neural Networks with Statistical Methods (Holt-Winters, ARIMA)</p>
</TooltipContent>
</Tooltip>
</div>
{stockPredictions?.predictions && (
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Features</span>
<span className="text-xs font-medium bg-secondary px-2 py-1 rounded-md">
Multi-Feature Enabled
</span>
</div>
)}
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Optimization</span>
<span className="text-sm font-medium flex items-center gap-1">
<Zap className="h-3 w-3 text-amber-500" />
Performance Tuned
</span>
</div>
<div className="pt-2 border-t text-xs text-muted-foreground">
Model automatically retrains with new sales data.
</div>
</div>
</CardContent>
</Card>
</TooltipProvider>
</div>
<Alert className="bg-yellow-500/10 border-yellow-500/30 text-yellow-700 dark:text-yellow-400">
<AlertTriangle className="h-4 w-4 stroke-yellow-700 dark:stroke-yellow-400" />
<AlertTitle>Prediction Accuracy Warning</AlertTitle>
<AlertDescription>
These predictions are estimates based on historical sales data. Actual results may vary due to external factors, market conditions, and unforeseen events. Use these insights as a guide, not a guarantee.
</AlertDescription>
</Alert>
{/* Daily Predictions Chart */}
{predictions?.sales?.dailyPredictions &&
predictions?.sales?.dailyPredictions.length > 0 && (
<Card className="glass-morphism border-primary/10 overflow-hidden">
<CardHeader className="pb-6 bg-muted/5">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<CardTitle className="text-xl font-bold flex items-center gap-2 tracking-tight">
<Zap className="h-5 w-5 text-amber-500 fill-amber-500/20" />
Scenario Lab
</CardTitle>
<CardDescription className="text-muted-foreground/80 font-medium">
Adjust variables to see how traffic shifts impact your bottom line.
</CardDescription>
</div>
<div className="flex items-center gap-4 bg-black/40 p-2.5 rounded-2xl border border-white/5 shadow-2xl backdrop-blur-md">
<div className="flex flex-col items-start min-w-[150px]">
<div className="flex items-center gap-1.5 mb-1 ml-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-primary/40">
Traffic Simulation
</span>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3 w-3 text-primary/30 cursor-help" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[200px] z-[110] bg-black border-white/10 text-white p-2">
<p className="text-[11px] leading-relaxed">
Simulate traffic growth or decline to see how it might impact your future revenue and order volume.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex items-center gap-3 w-full">
<Slider
value={[simulationFactor]}
min={-50}
max={50}
step={10}
onValueChange={(val) => setSimulationFactor(val[0])}
onValueCommit={(val) => setCommittedSimulationFactor(val[0])}
className="w-full flex-1"
/>
<Badge variant="outline" className={`ml-2 min-w-[50px] text-center font-bold border-2 ${simulationFactor > 0 ? "text-emerald-400 border-emerald-500/30 bg-emerald-500/10" : simulationFactor < 0 ? "text-rose-400 border-rose-500/30 bg-rose-500/10" : "text-primary/60"}`}>
{simulationFactor > 0 ? "+" : ""}{simulationFactor}%
</Badge>
</div>
</div>
{(simulationFactor !== 0 || committedSimulationFactor !== 0) && (
<Button
variant="ghost"
size="icon"
className="h-9 w-9 hover:bg-white/10 rounded-xl transition-all"
onClick={() => {
setSimulationFactor(0);
setCommittedSimulationFactor(0);
}}
title="Reset Scenario"
>
<RefreshCw className="h-4 w-4 text-primary/70" />
</Button>
)}
</div>
<Button variant="outline" size="sm" onClick={handleExportCSV} className="rounded-xl border-white/10 hover:bg-white/5 font-bold px-4">
<Download className="mr-2 h-4 w-4" />
Export Forecast
</Button>
</div>
</CardHeader>
<CardContent className="pt-8">
{/* Legend / Key */}
<div className="flex items-center gap-8 mb-8 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60">
<div className="flex items-center gap-3">
<div className="w-2.5 h-2.5 rounded-full bg-[#8884d8]" />
Baseline Forecast
</div>
{committedSimulationFactor !== 0 && (
<div className="flex items-center gap-3">
<div className="w-2.5 h-2.5 rounded-full bg-[#10b981]" />
Simulated Scenario
</div>
)}
</div>
<div className="h-80 w-full relative">
{isSimulating && (
<div className="absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm z-20 transition-all rounded-xl">
<div className="flex flex-col items-center gap-3">
<div className="relative">
<RefreshCw className="h-10 w-10 animate-spin text-primary" />
<Zap className="h-4 w-4 text-amber-500 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
<span className="text-xs font-bold uppercase tracking-wider text-primary animate-pulse">Running Neural Simulation...</span>
</div>
</div>
)}
<ResponsiveContainer key={`${daysAhead}-${timeRange}`} width="100%" height="100%">
<AreaChart
data={chartData}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<defs>
<linearGradient id="colorBaseline" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="#8884d8"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="#8884d8"
stopOpacity={0}
/>
</linearGradient>
<linearGradient id="colorSimulated" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="#10b981"
stopOpacity={0.5}
/>
<stop
offset="95%"
stopColor="#10b981"
stopOpacity={0}
/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border) / 0.4)" />
<XAxis
dataKey="formattedDate"
tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }}
tickLine={false}
axisLine={false}
dy={15}
/>
<YAxis
tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `£${value}`}
/>
<RechartsTooltip
cursor={{ fill: "transparent", stroke: "hsl(var(--primary) / 0.05)", strokeWidth: 40 }}
content={({ active, payload }) => {
if (active && payload?.length) {
const data = payload[0].payload;
return (
<div className="bg-[#050505] p-5 rounded-2xl shadow-2xl border border-white/10 backdrop-blur-2xl ring-1 ring-white/5">
<p className="font-bold text-[10px] uppercase tracking-wide text-muted-foreground mb-4 border-b border-white/5 pb-3 px-1">{data.formattedDate}</p>
<div className="space-y-3">
<div className="flex items-center justify-between gap-10">
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-wide">Baseline:</span>
<span className="text-sm font-bold text-[#8884d8] tabular-nums">{formatGBP(data.baseline)}</span>
</div>
{committedSimulationFactor !== 0 && (
<div className="flex items-center justify-between gap-10">
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-wide">Simulated:</span>
<div className="flex flex-col items-end">
<span className="text-sm font-bold text-emerald-400 tabular-nums">{formatGBP(data.simulated)}</span>
<span className={`text-[10px] font-bold mt-0.5 ${data.simulated > data.baseline ? 'text-emerald-500' : 'text-rose-500'}`}>
{data.simulated > data.baseline ? '▴' : '▾'} {Math.abs(((data.simulated / data.baseline - 1) * 100)).toFixed(1)}%
</span>
</div>
</div>
)}
<div className="flex items-center justify-between gap-10 pt-3 border-t border-white/5">
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-widest">Est. Orders:</span>
<span className="text-sm font-bold tabular-nums">
{Math.round(data.orders)}</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
{/* Always show baseline */}
<Area
type="monotone"
dataKey="baseline"
stroke="#8884d8"
fillOpacity={1}
fill="url(#colorBaseline)"
strokeWidth={3}
dot={false}
activeDot={{ r: 4, strokeWidth: 0, fill: "#8884d8" }}
/>
{/* Show simulated line when simulation is active */}
{committedSimulationFactor !== 0 && (
<Area
type="monotone"
dataKey="simulated"
stroke="#10b981"
fillOpacity={1}
fill="url(#colorSimulated)"
strokeWidth={3}
strokeDasharray="5 5"
dot={false}
activeDot={{ r: 6, strokeWidth: 3, stroke: "#fff", fill: "#10b981" }}
/>
)}
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)}
</div>
)}
{activeTab === "stock" && (
<div className="space-y-4">
{stockPredictions && stockPredictions.predictions.length > 0 ? (
<>
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">
{stockPredictions.totalProducts} products tracked
</p>
{stockPredictions.productsNeedingRestock > 0 && (
<p className="text-sm font-medium text-orange-600 dark:text-orange-400 mt-1">
{stockPredictions.productsNeedingRestock} products
need restocking soon
</p>
)}
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead>Current Stock</TableHead>
<TableHead>Days Until Out</TableHead>
<TableHead>Estimated Date</TableHead>
<TableHead>Confidence</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{stockPredictions.predictions.map((prediction) => (
<TableRow key={prediction.productId}>
<TableCell className="font-medium">
{prediction.productName}
</TableCell>
<TableCell>
{prediction.currentStock} {prediction.unitType}
</TableCell>
<TableCell>
{prediction.prediction.daysUntilOutOfStock !== null ? (
<div className="space-y-1">
<div className="font-medium">
{prediction.prediction.daysUntilOutOfStock} days
</div>
{prediction.prediction.optimisticDays !== null &&
prediction.prediction.pessimisticDays && (
<div className="text-xs text-muted-foreground">
{prediction.prediction.optimisticDays} -{" "}
{prediction.prediction.pessimisticDays} days
</div>
)}
</div>
) : (
"N/A"
)}
</TableCell>
<TableCell>
{prediction.prediction.estimatedDate ? (
<div className="space-y-1">
<div>
{format(
new Date(
prediction.prediction.estimatedDate,
),
"MMM d, yyyy",
)}
</div>
{prediction.prediction.optimisticDate &&
prediction.prediction.pessimisticDate && (
<div className="text-xs text-muted-foreground">
{format(
new Date(
prediction.prediction.optimisticDate,
),
"MMM d",
)}{" "}
-{" "}
{format(
new Date(
prediction.prediction.pessimisticDate,
),
"MMM d",
)}
</div>
)}
</div>
) : (
"N/A"
)}
</TableCell>
<TableCell>
<Badge
className={getConfidenceColor(
prediction.prediction.confidence,
)}
>
{getConfidenceLabel(prediction.prediction.confidence)}
</Badge>
</TableCell>
<TableCell>
{prediction.needsRestock ? (
<Badge
variant="destructive"
className="flex items-center gap-1 w-fit"
>
<AlertTriangle className="h-3 w-3" />
Restock Soon
</Badge>
) : (
<Badge variant="outline">OK</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</>
) : (
<div className="text-center py-8 text-muted-foreground">
<Package className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No stock predictions available</p>
<p className="text-sm mt-1">
Enable stock tracking on products to get predictions
</p>
</div>
)}
</div>
)}
</CardContent>
</Card >
);
}