From c69367e6da4f1791e6e10ca3836ac3bf9e057e58 Mon Sep 17 00:00:00 2001 From: g Date: Sat, 10 Jan 2026 01:47:38 +0000 Subject: [PATCH] Add predictions and forecasting analytics Introduces a new PredictionsChart component and integrates it into the AnalyticsDashboard with a new tab for AI-powered sales, demand, and stock forecasting. Expands analytics-service with types and service functions for predictions, enabling comprehensive future insights for revenue, demand, and inventory. --- components/analytics/AnalyticsDashboard.tsx | 16 +- components/analytics/PredictionsChart.tsx | 428 ++++++++++++++++++++ lib/services/analytics-service.ts | 199 +++++++++ 3 files changed, 642 insertions(+), 1 deletion(-) create mode 100644 components/analytics/PredictionsChart.tsx diff --git a/components/analytics/AnalyticsDashboard.tsx b/components/analytics/AnalyticsDashboard.tsx index fb4b352..1a7c12a 100644 --- a/components/analytics/AnalyticsDashboard.tsx +++ b/components/analytics/AnalyticsDashboard.tsx @@ -75,6 +75,10 @@ const GrowthAnalyticsChart = dynamic(() => import("./GrowthAnalyticsChart"), { loading: () => , }); +const PredictionsChart = dynamic(() => import("./PredictionsChart"), { + loading: () => , +}); + // Chart loading skeleton function ChartSkeleton() { return ( @@ -315,7 +319,7 @@ export default function AnalyticsDashboard({ {/* Analytics Tabs */}
- + Growth @@ -340,6 +344,10 @@ export default function AnalyticsDashboard({ Orders + + + Predictions + @@ -415,6 +423,12 @@ export default function AnalyticsDashboard({ + + + }> + + +
diff --git a/components/analytics/PredictionsChart.tsx b/components/analytics/PredictionsChart.tsx new file mode 100644 index 0000000..1d85572 --- /dev/null +++ b/components/analytics/PredictionsChart.tsx @@ -0,0 +1,428 @@ +"use client"; + +import { useState, useEffect } 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, +} 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"; + +interface PredictionsChartProps { + timeRange?: number; +} + +export default function PredictionsChart({ + timeRange = 30, +}: PredictionsChartProps) { + const [predictions, setPredictions] = useState( + null, + ); + const [stockPredictions, setStockPredictions] = + useState(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 "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"; + } + }; + + if (loading) { + return ( + + + + + + +
+ + + +
+
+
+ ); + } + + if (!predictions) { + return ( + + + Predictions + + Forecast future sales, demand, and stock levels + + + +

+ No prediction data available. Need more historical data. +

+
+
+ ); + } + + return ( + + +
+
+ + + Predictions & Forecasting + + + AI-powered predictions for sales, demand, and inventory + +
+
+ + +
+
+
+ +
+ + +
+ + {activeTab === "overview" && ( +
+ {/* Sales Predictions */} +
+ + + + + Revenue Prediction + + + + {predictions.sales.predicted !== null ? ( +
+
+ {formatGBP(predictions.sales.predicted)} +
+
+ + {predictions.sales.confidence} confidence + + + Next {daysAhead} days + +
+ {predictions.sales.predictedOrders && ( +
+ ~{Math.round(predictions.sales.predictedOrders)}{" "} + orders +
+ )} + {predictions.sales.minPrediction && + predictions.sales.maxPrediction && ( +
+ Range: {formatGBP(predictions.sales.minPrediction)} -{" "} + {formatGBP(predictions.sales.maxPrediction)} +
+ )} +
+ ) : ( +
+ {predictions.sales.message || + "Insufficient data for prediction"} +
+ )} +
+
+ + + + + + Demand Prediction + + + + {predictions.demand.predictedDaily !== null ? ( +
+
+ {predictions.demand.predictedDaily.toFixed(1)} units/day +
+
+ + {predictions.demand.confidence} confidence + +
+ {predictions.demand.predictedWeekly && ( +
+ ~{predictions.demand.predictedWeekly.toFixed(0)} units/week +
+ )} + {predictions.demand.predictedMonthly && ( +
+ ~{predictions.demand.predictedMonthly.toFixed(0)} units/month +
+ )} +
+ ) : ( +
+ {predictions.demand.message || + "Insufficient data for prediction"} +
+ )} +
+
+
+ + {/* Daily Predictions Chart */} + {predictions.sales.dailyPredictions && + predictions.sales.dailyPredictions.length > 0 && ( + + + + Daily Revenue Forecast + + + +
+ {predictions.sales.dailyPredictions.map((day) => ( +
+
+ +
+
+ Day {day.day} +
+
+ {format(new Date(day.date), "MMM d, yyyy")} +
+
+
+
+ {formatGBP(day.predicted)} +
+
+ ))} +
+
+
+ )} +
+ )} + + {activeTab === "stock" && ( +
+ {stockPredictions && stockPredictions.predictions.length > 0 ? ( + <> +
+
+

+ {stockPredictions.totalProducts} products tracked +

+ {stockPredictions.productsNeedingRestock > 0 && ( +

+ {stockPredictions.productsNeedingRestock} products + need restocking soon +

+ )} +
+
+ +
+ + + + Product + Current Stock + Days Until Out + Estimated Date + Confidence + Status + + + + {stockPredictions.predictions.map((prediction) => ( + + + {prediction.productName} + + + {prediction.currentStock} {prediction.unitType} + + + {prediction.prediction.daysUntilOutOfStock !== null + ? `${prediction.prediction.daysUntilOutOfStock} days` + : "N/A"} + + + {prediction.prediction.estimatedDate + ? format( + new Date( + prediction.prediction.estimatedDate, + ), + "MMM d, yyyy", + ) + : "N/A"} + + + + {prediction.prediction.confidence} + + + + {prediction.needsRestock ? ( + + + Restock Soon + + ) : ( + OK + )} + + + ))} + +
+
+ + ) : ( +
+ +

No stock predictions available

+

+ Enable stock tracking on products to get predictions +

+
+ )} +
+ )} +
+
+ ); +} diff --git a/lib/services/analytics-service.ts b/lib/services/analytics-service.ts index 1118c49..d16fc56 100644 --- a/lib/services/analytics-service.ts +++ b/lib/services/analytics-service.ts @@ -298,3 +298,202 @@ export function formatGBP(value: number) { maximumFractionDigits: 2, }); } + +// Prediction Types +export interface SalesPrediction { + predicted: number | null; + predictedOrders: number | null; + dailyPredictions?: Array<{ + day: number; + predicted: number; + date: string; + }>; + confidence: "high" | "medium" | "low"; + method: string; + methods?: { + movingAverage?: number | null; + exponentialAverage?: number | null; + linearRegression?: number | null; + trendAverage?: number | null; + }; + variance?: number; + minPrediction?: number; + maxPrediction?: number; + seasonality?: { + dayOfWeek: Record; + month: Record; + confidence: string; + }; + historicalPeriod: number; + predictionPeriod: number; + message?: string; +} + +export interface DemandPrediction { + predictedDaily: number | null; + predictedWeekly: number | null; + predictedMonthly: number | null; + confidence: "high" | "medium" | "low"; + averageDaily?: number; + trendFactor?: number; + stdDev?: number; + historicalPeriod: number; + predictionPeriod: number; + productId?: string | null; + message?: string; +} + +export interface StockPrediction { + productId: string; + productName: string; + currentStock: number; + lowStockThreshold: number; + unitType: string; + prediction: { + daysUntilOutOfStock: number | null; + estimatedDate: string | null; + confidence: "high" | "medium" | "low"; + averageDailySales?: number; + stdDev?: number; + message?: string; + }; + needsRestock: boolean; +} + +export interface StockPredictionsResponse { + predictions: StockPrediction[]; + historicalPeriod: number; + totalProducts: number; + productsNeedingRestock: number; +} + +export interface PredictionsOverview { + sales: SalesPrediction; + demand: DemandPrediction; + stock: { + totalProducts: number; + message?: string; + }; + historicalPeriod: number; + predictionPeriod: number; + message?: string; +} + +// Prediction Service Functions + +/** + * Get sales/revenue predictions + * @param daysAhead Number of days to predict ahead (default: 7) + * @param period Historical period in days (default: 30) + * @param storeId Optional storeId for staff users + */ +export const getSalesPredictions = async ( + daysAhead: number = 7, + period: number = 30, + storeId?: string, +): Promise => { + const params = new URLSearchParams({ + daysAhead: daysAhead.toString(), + period: period.toString(), + }); + if (storeId) params.append("storeId", storeId); + + const url = `/analytics/predictions/sales?${params.toString()}`; + return clientFetch(url); +}; + +/** + * Get product demand predictions + * @param productId Optional product ID for specific product prediction + * @param daysAhead Number of days to predict ahead (default: 7) + * @param period Historical period in days (default: 30) + * @param storeId Optional storeId for staff users + */ +export const getDemandPredictions = async ( + productId?: string, + daysAhead: number = 7, + period: number = 30, + storeId?: string, +): Promise => { + const params = new URLSearchParams({ + daysAhead: daysAhead.toString(), + period: period.toString(), + }); + if (productId) params.append("productId", productId); + if (storeId) params.append("storeId", storeId); + + const url = `/analytics/predictions/demand?${params.toString()}`; + return clientFetch(url); +}; + +/** + * Get stock depletion predictions + * @param period Historical period in days (default: 30) + * @param storeId Optional storeId for staff users + */ +export const getStockPredictions = async ( + period: number = 30, + storeId?: string, +): Promise => { + const params = new URLSearchParams({ + period: period.toString(), + }); + if (storeId) params.append("storeId", storeId); + + const url = `/analytics/predictions/stock?${params.toString()}`; + return clientFetch(url); +}; + +/** + * Get comprehensive predictions overview + * @param daysAhead Number of days to predict ahead (default: 7) + * @param period Historical period in days (default: 30) + * @param storeId Optional storeId for staff users + */ +export const getPredictionsOverview = async ( + daysAhead: number = 7, + period: number = 30, + storeId?: string, +): Promise => { + const params = new URLSearchParams({ + daysAhead: daysAhead.toString(), + period: period.toString(), + }); + if (storeId) params.append("storeId", storeId); + + const url = `/analytics/predictions/overview?${params.toString()}`; + return clientFetch(url); +}; + +// Helper functions with automatic storeId handling +export const getSalesPredictionsWithStore = async ( + daysAhead: number = 7, + period: number = 30, +): Promise => { + const storeId = getStoreIdForUser(); + return getSalesPredictions(daysAhead, period, storeId); +}; + +export const getDemandPredictionsWithStore = async ( + productId?: string, + daysAhead: number = 7, + period: number = 30, +): Promise => { + const storeId = getStoreIdForUser(); + return getDemandPredictions(productId, daysAhead, period, storeId); +}; + +export const getStockPredictionsWithStore = async ( + period: number = 30, +): Promise => { + const storeId = getStoreIdForUser(); + return getStockPredictions(period, storeId); +}; + +export const getPredictionsOverviewWithStore = async ( + daysAhead: number = 7, + period: number = 30, +): Promise => { + const storeId = getStoreIdForUser(); + return getPredictionsOverview(daysAhead, period, storeId); +};