This commit is contained in:
@@ -79,24 +79,28 @@ export default function PredictionsChart({
|
|||||||
const [predictions, setPredictions] = useState<PredictionsOverview | null>(
|
const [predictions, setPredictions] = useState<PredictionsOverview | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [baselinePredictions, setBaselinePredictions] = useState<PredictionsOverview | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [stockPredictions, setStockPredictions] =
|
const [stockPredictions, setStockPredictions] =
|
||||||
useState<StockPredictionsResponse | null>(null);
|
useState<StockPredictionsResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isSimulating, setIsSimulating] = useState(false);
|
||||||
const [daysAhead, setDaysAhead] = useState(7);
|
const [daysAhead, setDaysAhead] = useState(7);
|
||||||
const [activeTab, setActiveTab] = useState<"overview" | "stock">("overview");
|
const [activeTab, setActiveTab] = useState<"overview" | "stock">("overview");
|
||||||
const [simulationFactor, setSimulationFactor] = useState(0);
|
const [simulationFactor, setSimulationFactor] = useState(0);
|
||||||
const [committedSimulationFactor, setCommittedSimulationFactor] = useState(0);
|
const [committedSimulationFactor, setCommittedSimulationFactor] = useState(0);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const fetchPredictions = async () => {
|
// Fetch baseline predictions (simulation factor = 0)
|
||||||
|
const fetchBaseline = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Convert percentage (e.g. 50) to factor (e.g. 0.5)
|
|
||||||
const factor = committedSimulationFactor / 100;
|
|
||||||
const [overview, stock] = await Promise.all([
|
const [overview, stock] = await Promise.all([
|
||||||
getPredictionsOverviewWithStore(daysAhead, timeRange, factor),
|
getPredictionsOverviewWithStore(daysAhead, timeRange, 0),
|
||||||
getStockPredictionsWithStore(timeRange),
|
getStockPredictionsWithStore(timeRange),
|
||||||
]);
|
]);
|
||||||
|
setBaselinePredictions(overview);
|
||||||
setPredictions(overview);
|
setPredictions(overview);
|
||||||
setStockPredictions(stock);
|
setStockPredictions(stock);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -111,9 +115,43 @@ export default function PredictionsChart({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch simulated predictions (without full reload)
|
||||||
|
const fetchSimulation = async (factor: number) => {
|
||||||
|
if (factor === 0) {
|
||||||
|
// Use cached baseline
|
||||||
|
setPredictions(baselinePredictions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSimulating(true);
|
||||||
|
const overview = await getPredictionsOverviewWithStore(daysAhead, timeRange, factor / 100);
|
||||||
|
setPredictions(overview);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching simulation:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load simulation",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSimulating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch baseline on initial load or when daysAhead/timeRange changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPredictions();
|
fetchBaseline();
|
||||||
}, [daysAhead, timeRange, committedSimulationFactor]);
|
setCommittedSimulationFactor(0);
|
||||||
|
setSimulationFactor(0);
|
||||||
|
}, [daysAhead, timeRange]);
|
||||||
|
|
||||||
|
// Fetch simulation when committed slider value changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (baselinePredictions) {
|
||||||
|
fetchSimulation(committedSimulationFactor);
|
||||||
|
}
|
||||||
|
}, [committedSimulationFactor]);
|
||||||
|
|
||||||
const getConfidenceColor = (confidence: string) => {
|
const getConfidenceColor = (confidence: string) => {
|
||||||
switch (confidence) {
|
switch (confidence) {
|
||||||
@@ -145,27 +183,37 @@ export default function PredictionsChart({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const simulatedData = useMemo(() => {
|
// Combine baseline and simulated data for overlay chart
|
||||||
|
const chartData = useMemo(() => {
|
||||||
if (!predictions?.sales?.dailyPredictions) return [];
|
if (!predictions?.sales?.dailyPredictions) return [];
|
||||||
return predictions.sales.dailyPredictions.map((d: any) => ({
|
|
||||||
|
const baselineData = baselinePredictions?.sales?.dailyPredictions || [];
|
||||||
|
const simulatedDailyData = predictions.sales.dailyPredictions;
|
||||||
|
|
||||||
|
return simulatedDailyData.map((d: any, idx: number) => ({
|
||||||
...d,
|
...d,
|
||||||
formattedDate: format(new Date(d.date), "MMM d"),
|
formattedDate: format(new Date(d.date), "MMM d"),
|
||||||
value: d.predicted,
|
simulated: d.predicted,
|
||||||
orders: d.predictedOrders || 0, // Ensure orders exist
|
baseline: baselineData[idx]?.predicted ?? d.predicted,
|
||||||
|
orders: d.predictedOrders || 0,
|
||||||
}));
|
}));
|
||||||
}, [predictions]);
|
}, [predictions, baselinePredictions]);
|
||||||
|
|
||||||
|
// Keep simulatedData for export compatibility
|
||||||
|
const simulatedData = chartData;
|
||||||
|
|
||||||
const handleExportCSV = () => {
|
const handleExportCSV = () => {
|
||||||
if (!simulatedData.length) return;
|
if (!simulatedData.length) return;
|
||||||
|
|
||||||
// Create CSV headers
|
// Create CSV headers
|
||||||
const headers = ["Date", "Predicted Revenue", "Orders", "Confidence"];
|
const headers = ["Date", "Baseline Revenue", "Simulated Revenue", "Orders", "Confidence"];
|
||||||
|
|
||||||
// Create CSV rows
|
// Create CSV rows
|
||||||
const rows = simulatedData.map(d => [
|
const rows = simulatedData.map(d => [
|
||||||
format(new Date(d.date), "yyyy-MM-dd"),
|
format(new Date(d.date), "yyyy-MM-dd"),
|
||||||
d.predicted.toFixed(2),
|
d.baseline?.toFixed(2) || "",
|
||||||
d.orders || "", // Provide fallback if orders is undefined
|
d.simulated?.toFixed(2) || "",
|
||||||
|
d.orders || "",
|
||||||
predictions?.sales?.confidence || "unknown"
|
predictions?.sales?.confidence || "unknown"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -257,11 +305,11 @@ export default function PredictionsChart({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={fetchPredictions}
|
onClick={fetchBaseline}
|
||||||
disabled={loading}
|
disabled={loading || isSimulating}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 ${loading ? "animate-spin" : ""}`}
|
className={`h-4 w-4 ${loading || isSimulating ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -532,10 +580,15 @@ export default function PredictionsChart({
|
|||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="h-[300px] w-full mt-4">
|
<div className="h-[300px] 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%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<AreaChart
|
<AreaChart
|
||||||
data={simulatedData}
|
data={chartData}
|
||||||
margin={{
|
margin={{
|
||||||
top: 5,
|
top: 5,
|
||||||
right: 10,
|
right: 10,
|
||||||
@@ -544,15 +597,27 @@ export default function PredictionsChart({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="colorBaseline" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop
|
<stop
|
||||||
offset="5%"
|
offset="5%"
|
||||||
stopColor={simulationFactor !== 0 ? "#10b981" : "#8884d8"}
|
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}
|
stopOpacity={0.8}
|
||||||
/>
|
/>
|
||||||
<stop
|
<stop
|
||||||
offset="95%"
|
offset="95%"
|
||||||
stopColor={simulationFactor !== 0 ? "#10b981" : "#8884d8"}
|
stopColor="#10b981"
|
||||||
stopOpacity={0}
|
stopOpacity={0}
|
||||||
/>
|
/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
@@ -578,16 +643,32 @@ export default function PredictionsChart({
|
|||||||
borderColor: "hsl(var(--border))",
|
borderColor: "hsl(var(--border))",
|
||||||
borderRadius: "var(--radius)",
|
borderRadius: "var(--radius)",
|
||||||
}}
|
}}
|
||||||
formatter={(value: number) => [formatGBP(value), "Revenue"]}
|
formatter={(value: number, name: string) => [
|
||||||
|
formatGBP(value),
|
||||||
|
name === "baseline" ? "Baseline" : "Simulated"
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
|
{/* Always show baseline as solid line */}
|
||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="value"
|
dataKey="baseline"
|
||||||
stroke={simulationFactor !== 0 ? "#10b981" : "#8884d8"}
|
stroke="#8884d8"
|
||||||
fillOpacity={1}
|
fillOpacity={committedSimulationFactor !== 0 ? 0.3 : 1}
|
||||||
fill="url(#colorValue)"
|
fill="url(#colorBaseline)"
|
||||||
strokeDasharray={simulationFactor !== 0 ? "5 5" : "0"}
|
strokeWidth={committedSimulationFactor !== 0 ? 1 : 2}
|
||||||
/>
|
/>
|
||||||
|
{/* 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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -728,6 +809,6 @@ export default function PredictionsChart({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"commitHash": "02ba4b0",
|
"commitHash": "5d9f8fa",
|
||||||
"buildTime": "2026-01-12T03:57:23.436Z"
|
"buildTime": "2026-01-12T04:07:03.023Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user