625 lines
25 KiB
TypeScript
625 lines
25 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,
|
|
} from "lucide-react";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
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";
|
|
|
|
interface PredictionsChartProps {
|
|
timeRange?: number;
|
|
}
|
|
|
|
export default function PredictionsChart({
|
|
timeRange = 90,
|
|
}: PredictionsChartProps) {
|
|
const [predictions, setPredictions] = useState<PredictionsOverview | null>(
|
|
null,
|
|
);
|
|
const [stockPredictions, setStockPredictions] =
|
|
useState<StockPredictionsResponse | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [daysAhead, setDaysAhead] = useState(7);
|
|
const [activeTab, setActiveTab] = useState<"overview" | "stock">("overview");
|
|
const { toast } = useToast();
|
|
|
|
const fetchPredictions = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [overview, stock] = await Promise.all([
|
|
getPredictionsOverviewWithStore(daysAhead, timeRange),
|
|
getStockPredictionsWithStore(timeRange),
|
|
]);
|
|
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);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchPredictions();
|
|
}, [daysAhead, timeRange]);
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
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>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={fetchPredictions}
|
|
disabled={loading}
|
|
>
|
|
<RefreshCw
|
|
className={`h-4 w-4 ${loading ? "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>
|
|
<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 ? (
|
|
<div className="space-y-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="text-2xl font-bold w-fit cursor-help">
|
|
{formatGBP(predictions.sales.predicted)}
|
|
</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>
|
|
<div className="inline-flex">
|
|
<Badge
|
|
className={getConfidenceColor(
|
|
predictions.sales.confidence,
|
|
)}
|
|
>
|
|
{getConfidenceLabel(predictions.sales.confidence)} Confidence
|
|
{predictions.sales.confidenceScore !== undefined && (
|
|
<span className="ml-1 opacity-75">
|
|
({Math.round(predictions.sales.confidenceScore * 100)}%)
|
|
</span>
|
|
)}
|
|
</Badge>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Based on data consistency, historical accuracy, and model agreement</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
{predictions.sales.aiModel?.used && (
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<div className="inline-flex">
|
|
<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 * 100)}%)
|
|
</span>
|
|
)}
|
|
</Badge>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Predictions generated using a Deep Learning Ensemble Model (TensorFlow.js)</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
{predictions.sales.trend && (
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<div className="inline-flex">
|
|
<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>
|
|
</div>
|
|
</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)}{" "}
|
|
orders
|
|
</div>
|
|
)}
|
|
{!predictions.sales.confidenceIntervals &&
|
|
predictions.sales.minPrediction &&
|
|
predictions.sales.maxPrediction && (
|
|
<div className="text-xs text-muted-foreground">
|
|
Range: {formatGBP(predictions.sales.minPrediction)} -{" "}
|
|
{formatGBP(predictions.sales.maxPrediction)}
|
|
</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>
|
|
|
|
{/* Daily Predictions Chart */}
|
|
{predictions.sales.dailyPredictions &&
|
|
predictions.sales.dailyPredictions.length > 0 && (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium">
|
|
Daily Revenue Forecast
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px] w-full mt-4">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart
|
|
data={predictions.sales.dailyPredictions.map(d => ({
|
|
...d,
|
|
formattedDate: format(new Date(d.date), "MMM d"),
|
|
value: d.predicted
|
|
}))}
|
|
margin={{
|
|
top: 5,
|
|
right: 10,
|
|
left: 0,
|
|
bottom: 0,
|
|
}}
|
|
>
|
|
<defs>
|
|
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
|
|
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
|
<XAxis
|
|
dataKey="formattedDate"
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
/>
|
|
<YAxis
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickFormatter={(value) => `£${value}`}
|
|
/>
|
|
<RechartsTooltip
|
|
contentStyle={{
|
|
backgroundColor: "hsl(var(--background))",
|
|
borderColor: "hsl(var(--border))",
|
|
borderRadius: "var(--radius)",
|
|
}}
|
|
formatter={(value: number) => [formatGBP(value), "Revenue"]}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="value"
|
|
stroke="#8884d8"
|
|
fillOpacity={1}
|
|
fill="url(#colorValue)"
|
|
/>
|
|
</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>
|
|
);
|
|
}
|