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.
This commit is contained in:
@@ -75,6 +75,10 @@ const GrowthAnalyticsChart = dynamic(() => import("./GrowthAnalyticsChart"), {
|
|||||||
loading: () => <ChartSkeleton />,
|
loading: () => <ChartSkeleton />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const PredictionsChart = dynamic(() => import("./PredictionsChart"), {
|
||||||
|
loading: () => <ChartSkeleton />,
|
||||||
|
});
|
||||||
|
|
||||||
// Chart loading skeleton
|
// Chart loading skeleton
|
||||||
function ChartSkeleton() {
|
function ChartSkeleton() {
|
||||||
return (
|
return (
|
||||||
@@ -315,7 +319,7 @@ export default function AnalyticsDashboard({
|
|||||||
{/* Analytics Tabs */}
|
{/* Analytics Tabs */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Tabs defaultValue="growth" className="space-y-6">
|
<Tabs defaultValue="growth" className="space-y-6">
|
||||||
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 lg:grid-cols-6">
|
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 lg:grid-cols-7">
|
||||||
<TabsTrigger value="growth" className="flex items-center gap-2">
|
<TabsTrigger value="growth" className="flex items-center gap-2">
|
||||||
<Activity className="h-4 w-4" />
|
<Activity className="h-4 w-4" />
|
||||||
Growth
|
Growth
|
||||||
@@ -340,6 +344,10 @@ export default function AnalyticsDashboard({
|
|||||||
<BarChart3 className="h-4 w-4" />
|
<BarChart3 className="h-4 w-4" />
|
||||||
Orders
|
Orders
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="predictions" className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-4 w-4" />
|
||||||
|
Predictions
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="growth" className="space-y-6">
|
<TabsContent value="growth" className="space-y-6">
|
||||||
@@ -415,6 +423,12 @@ export default function AnalyticsDashboard({
|
|||||||
<OrderAnalyticsChart timeRange={timeRange} />
|
<OrderAnalyticsChart timeRange={timeRange} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="predictions" className="space-y-6">
|
||||||
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
|
<PredictionsChart timeRange={parseInt(timeRange)} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
428
components/analytics/PredictionsChart.tsx
Normal file
428
components/analytics/PredictionsChart.tsx
Normal file
@@ -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<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 "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 (
|
||||||
|
<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>
|
||||||
|
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">
|
||||||
|
<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">
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{formatGBP(predictions.sales.predicted)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
className={getConfidenceColor(
|
||||||
|
predictions.sales.confidence,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{predictions.sales.confidence} confidence
|
||||||
|
</Badge>
|
||||||
|
<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.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>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Package className="h-4 w-4" />
|
||||||
|
Demand Prediction
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{predictions.demand.predictedDaily !== null ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{predictions.demand.predictedDaily.toFixed(1)} units/day
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
className={getConfidenceColor(
|
||||||
|
predictions.demand.confidence,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{predictions.demand.confidence} confidence
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{predictions.demand.predictedWeekly && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
~{predictions.demand.predictedWeekly.toFixed(0)} units/week
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{predictions.demand.predictedMonthly && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
~{predictions.demand.predictedMonthly.toFixed(0)} units/month
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{predictions.demand.message ||
|
||||||
|
"Insufficient data for prediction"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</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="space-y-2">
|
||||||
|
{predictions.sales.dailyPredictions.map((day) => (
|
||||||
|
<div
|
||||||
|
key={day.day}
|
||||||
|
className="flex items-center justify-between p-2 rounded-lg border"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
Day {day.day}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{format(new Date(day.date), "MMM d, yyyy")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold">
|
||||||
|
{formatGBP(day.predicted)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</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
|
||||||
|
? `${prediction.prediction.daysUntilOutOfStock} days`
|
||||||
|
: "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{prediction.prediction.estimatedDate
|
||||||
|
? format(
|
||||||
|
new Date(
|
||||||
|
prediction.prediction.estimatedDate,
|
||||||
|
),
|
||||||
|
"MMM d, yyyy",
|
||||||
|
)
|
||||||
|
: "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
className={getConfidenceColor(
|
||||||
|
prediction.prediction.confidence,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -298,3 +298,202 @@ export function formatGBP(value: number) {
|
|||||||
maximumFractionDigits: 2,
|
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<string, number>;
|
||||||
|
month: Record<string, number>;
|
||||||
|
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<SalesPrediction> => {
|
||||||
|
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<SalesPrediction>(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<DemandPrediction> => {
|
||||||
|
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<DemandPrediction>(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<StockPredictionsResponse> => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
period: period.toString(),
|
||||||
|
});
|
||||||
|
if (storeId) params.append("storeId", storeId);
|
||||||
|
|
||||||
|
const url = `/analytics/predictions/stock?${params.toString()}`;
|
||||||
|
return clientFetch<StockPredictionsResponse>(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<PredictionsOverview> => {
|
||||||
|
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<PredictionsOverview>(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper functions with automatic storeId handling
|
||||||
|
export const getSalesPredictionsWithStore = async (
|
||||||
|
daysAhead: number = 7,
|
||||||
|
period: number = 30,
|
||||||
|
): Promise<SalesPrediction> => {
|
||||||
|
const storeId = getStoreIdForUser();
|
||||||
|
return getSalesPredictions(daysAhead, period, storeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDemandPredictionsWithStore = async (
|
||||||
|
productId?: string,
|
||||||
|
daysAhead: number = 7,
|
||||||
|
period: number = 30,
|
||||||
|
): Promise<DemandPrediction> => {
|
||||||
|
const storeId = getStoreIdForUser();
|
||||||
|
return getDemandPredictions(productId, daysAhead, period, storeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockPredictionsWithStore = async (
|
||||||
|
period: number = 30,
|
||||||
|
): Promise<StockPredictionsResponse> => {
|
||||||
|
const storeId = getStoreIdForUser();
|
||||||
|
return getStockPredictions(period, storeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPredictionsOverviewWithStore = async (
|
||||||
|
daysAhead: number = 7,
|
||||||
|
period: number = 30,
|
||||||
|
): Promise<PredictionsOverview> => {
|
||||||
|
const storeId = getStoreIdForUser();
|
||||||
|
return getPredictionsOverview(daysAhead, period, storeId);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user