All checks were successful
Build Frontend / build (push) Successful in 1m19s
Refactored GrowthAnalyticsChart to use Area for 'orders' with gradient fill and improved dot handling. Enhanced PredictionsChart with consistent null checks for predictions data, improved tooltip rendering, and adjusted chart margins and axis styles. Updated RevenueChart to add activeDot styling for better interactivity.
847 lines
35 KiB
TypeScript
847 lines
35 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]);
|
|
|
|
// 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">14 days</SelectItem>
|
|
<SelectItem value="30">30 days</SelectItem>
|
|
<SelectItem value="60">60 days</SelectItem>
|
|
<SelectItem value="90">90 days</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>
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<CardHeader className="pb-3 flex flex-row items-center justify-between space-y-0">
|
|
<CardTitle className="text-sm font-medium">
|
|
Daily Revenue Forecast
|
|
</CardTitle>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex flex-col items-end">
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
Simulate Traffic:{" "}
|
|
<span className={simulationFactor > 0 ? "text-green-600" : simulationFactor < 0 ? "text-red-600" : ""}>
|
|
{simulationFactor > 0 ? "+" : ""}
|
|
{simulationFactor}%
|
|
</span>
|
|
</span>
|
|
<Slider
|
|
value={[simulationFactor]}
|
|
min={-50}
|
|
max={50}
|
|
step={10}
|
|
onValueChange={(val) => setSimulationFactor(val[0])}
|
|
onValueCommit={(val) => setCommittedSimulationFactor(val[0])}
|
|
className="w-[150px] mt-1.5"
|
|
/>
|
|
</div>
|
|
{simulationFactor !== 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
onClick={() => {
|
|
setSimulationFactor(0);
|
|
setCommittedSimulationFactor(0);
|
|
}}
|
|
title="Reset simulation"
|
|
>
|
|
<RefreshCw className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={handleExportCSV} title="Export to CSV">
|
|
<Download className="mr-2 h-4 w-4" />
|
|
Export
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-80 w-full mt-4 relative">
|
|
{isSimulating && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-background/50 z-10">
|
|
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
)}
|
|
<ResponsiveContainer 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.6}
|
|
/>
|
|
<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.8}
|
|
/>
|
|
<stop
|
|
offset="95%"
|
|
stopColor="#10b981"
|
|
stopOpacity={0}
|
|
/>
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
|
<XAxis
|
|
dataKey="formattedDate"
|
|
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickFormatter={(value) => `£${value}`}
|
|
/>
|
|
<RechartsTooltip
|
|
cursor={{ fill: "transparent", stroke: "hsl(var(--muted-foreground))", strokeDasharray: "3 3" }}
|
|
content={({ active, payload }) => {
|
|
if (active && payload?.length) {
|
|
const data = payload[0].payload;
|
|
return (
|
|
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
|
<p className="font-medium mb-2">{data.formattedDate}</p>
|
|
<p className="text-sm text-purple-600">
|
|
Baseline: <span className="font-semibold">{formatGBP(data.baseline)}</span>
|
|
</p>
|
|
{committedSimulationFactor !== 0 && (
|
|
<p className="text-sm text-green-600">
|
|
Simulated: <span className="font-semibold">{formatGBP(data.simulated)}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
}}
|
|
/>
|
|
{/* Always show baseline as solid line */}
|
|
<Area
|
|
type="monotone"
|
|
dataKey="baseline"
|
|
stroke="#8884d8"
|
|
fillOpacity={committedSimulationFactor !== 0 ? 0.3 : 1}
|
|
fill="url(#colorBaseline)"
|
|
strokeWidth={committedSimulationFactor !== 0 ? 1 : 2}
|
|
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
/>
|
|
{/* Show simulated line when simulation is active */}
|
|
{committedSimulationFactor !== 0 && (
|
|
<Area
|
|
type="monotone"
|
|
dataKey="simulated"
|
|
stroke="#10b981"
|
|
fillOpacity={0.6}
|
|
fill="url(#colorSimulated)"
|
|
strokeDasharray="5 5"
|
|
strokeWidth={2}
|
|
activeDot={{ r: 5, strokeWidth: 0 }}
|
|
/>
|
|
)}
|
|
</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 >
|
|
);
|
|
}
|