Revamp analytics dashboard UI and charts
All checks were successful
Build Frontend / build (push) Successful in 1m11s
All checks were successful
Build Frontend / build (push) Successful in 1m11s
Enhanced the AnalyticsDashboard layout with a premium glassmorphism UI, improved toolbar, and reorganized tabs for better clarity. MetricsCard now features dynamic color coding and trend badges. PredictionsChart received scenario simulation UI upgrades, disabled future ranges based on available history, and improved chart tooltips and visuals. ProfitAnalyticsChart added error handling for product images and minor UI refinements. Updated globals.css with new premium utility classes and improved dark mode color variables.
This commit is contained in:
@@ -164,6 +164,13 @@ export default function PredictionsChart({
|
||||
setSimulationFactor(0);
|
||||
}, [timeRange]);
|
||||
|
||||
// Auto-adjust daysAhead if it exceeds historical timeRange
|
||||
useEffect(() => {
|
||||
if (daysAhead > timeRange) {
|
||||
setDaysAhead(timeRange);
|
||||
}
|
||||
}, [timeRange, daysAhead]);
|
||||
|
||||
// Switch predictions when daysAhead changes (instant, from batch)
|
||||
useEffect(() => {
|
||||
if (batchData) {
|
||||
@@ -322,10 +329,18 @@ export default function PredictionsChart({
|
||||
</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>
|
||||
<SelectItem value="14" disabled={timeRange < 14}>
|
||||
14 days {timeRange < 14 && "(Needs 14d history)"}
|
||||
</SelectItem>
|
||||
<SelectItem value="30" disabled={timeRange < 30}>
|
||||
30 days {timeRange < 30 && "(Needs 30d history)"}
|
||||
</SelectItem>
|
||||
<SelectItem value="60" disabled={timeRange < 60}>
|
||||
60 days {timeRange < 60 && "(Needs 60d history)"}
|
||||
</SelectItem>
|
||||
<SelectItem value="90" disabled={timeRange < 90}>
|
||||
90 days {timeRange < 90 && "(Needs 90d history)"}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
@@ -386,7 +401,7 @@ export default function PredictionsChart({
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent side="bottom" className="z-[100]">
|
||||
<p>Predicted daily average revenue for the next {daysAhead} days</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -409,7 +424,7 @@ export default function PredictionsChart({
|
||||
</Badge>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent side="bottom" className="z-[100]">
|
||||
<p>Based on data consistency, historical accuracy, and model agreement</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -428,7 +443,7 @@ export default function PredictionsChart({
|
||||
</Badge>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent side="bottom" className="z-[100]">
|
||||
<p>Predictions generated using a Deep Learning Ensemble Model</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -461,7 +476,7 @@ export default function PredictionsChart({
|
||||
</Badge>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent side="bottom" className="z-[100]">
|
||||
<p>Direction of the recent sales trend (slope analysis)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -504,7 +519,7 @@ export default function PredictionsChart({
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3 w-3 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent side="bottom" className="z-[100]">
|
||||
<p>Technical details about the active prediction model</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -521,7 +536,7 @@ export default function PredictionsChart({
|
||||
Hybrid Ensemble (Deep Learning)
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent side="bottom" className="z-[100]">
|
||||
<p>Combines LSTM Neural Networks with Statistical Methods (Holt-Winters, ARIMA)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -561,58 +576,104 @@ export default function PredictionsChart({
|
||||
{/* 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"
|
||||
/>
|
||||
<Card className="glass-morphism border-primary/10 overflow-hidden">
|
||||
<CardHeader className="pb-6 bg-muted/5">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-xl font-bold flex items-center gap-2 tracking-tight">
|
||||
<Zap className="h-5 w-5 text-amber-500 fill-amber-500/20" />
|
||||
Scenario Lab
|
||||
</CardTitle>
|
||||
<CardDescription className="text-muted-foreground/80 font-medium">
|
||||
Adjust variables to see how traffic shifts impact your bottom line.
|
||||
</CardDescription>
|
||||
</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 className="flex items-center gap-4 bg-black/40 p-2.5 rounded-2xl border border-white/5 shadow-2xl backdrop-blur-md">
|
||||
<div className="flex flex-col items-start min-w-[150px]">
|
||||
<div className="flex items-center gap-1.5 mb-1 ml-1">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-primary/40">
|
||||
Traffic Simulation
|
||||
</span>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3 w-3 text-primary/30 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-[200px] z-[110] bg-black border-white/10 text-white p-2">
|
||||
<p className="text-[11px] leading-relaxed">
|
||||
Simulate traffic growth or decline to see how it might impact your future revenue and order volume.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 w-full">
|
||||
<Slider
|
||||
value={[simulationFactor]}
|
||||
min={-50}
|
||||
max={50}
|
||||
step={10}
|
||||
onValueChange={(val) => setSimulationFactor(val[0])}
|
||||
onValueCommit={(val) => setCommittedSimulationFactor(val[0])}
|
||||
className="w-full flex-1"
|
||||
/>
|
||||
<Badge variant="outline" className={`ml-2 min-w-[50px] text-center font-bold border-2 ${simulationFactor > 0 ? "text-emerald-400 border-emerald-500/30 bg-emerald-500/10" : simulationFactor < 0 ? "text-rose-400 border-rose-500/30 bg-rose-500/10" : "text-primary/60"}`}>
|
||||
{simulationFactor > 0 ? "+" : ""}{simulationFactor}%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(simulationFactor !== 0 || committedSimulationFactor !== 0) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 hover:bg-white/10 rounded-xl transition-all"
|
||||
onClick={() => {
|
||||
setSimulationFactor(0);
|
||||
setCommittedSimulationFactor(0);
|
||||
}}
|
||||
title="Reset Scenario"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 text-primary/70" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={handleExportCSV} className="rounded-xl border-white/10 hover:bg-white/5 font-bold px-4">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Export Forecast
|
||||
</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" />
|
||||
<CardContent className="pt-8">
|
||||
{/* Legend / Key */}
|
||||
<div className="flex items-center gap-8 mb-8 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#8884d8]" />
|
||||
Baseline Forecast
|
||||
</div>
|
||||
{committedSimulationFactor !== 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#10b981]" />
|
||||
Simulated Scenario
|
||||
</div>
|
||||
)}
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
</div>
|
||||
|
||||
<div className="h-80 w-full relative">
|
||||
{isSimulating && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm z-20 transition-all rounded-xl">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="relative">
|
||||
<RefreshCw className="h-10 w-10 animate-spin text-primary" />
|
||||
<Zap className="h-4 w-4 text-amber-500 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</div>
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-primary animate-pulse">Running Neural Simulation...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ResponsiveContainer key={`${daysAhead}-${timeRange}`} width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
@@ -622,7 +683,7 @@ export default function PredictionsChart({
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="#8884d8"
|
||||
stopOpacity={0.6}
|
||||
stopOpacity={0.3}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
@@ -634,7 +695,7 @@ export default function PredictionsChart({
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="#10b981"
|
||||
stopOpacity={0.8}
|
||||
stopOpacity={0.5}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
@@ -643,50 +704,66 @@ export default function PredictionsChart({
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border) / 0.4)" />
|
||||
<XAxis
|
||||
dataKey="formattedDate"
|
||||
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
||||
tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
dy={15}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
||||
tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => `£${value}`}
|
||||
/>
|
||||
<RechartsTooltip
|
||||
cursor={{ fill: "transparent", stroke: "hsl(var(--muted-foreground))", strokeDasharray: "3 3" }}
|
||||
cursor={{ fill: "transparent", stroke: "hsl(var(--primary) / 0.05)", strokeWidth: 40 }}
|
||||
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 className="bg-[#050505] p-5 rounded-2xl shadow-2xl border border-white/10 backdrop-blur-2xl ring-1 ring-white/5">
|
||||
<p className="font-bold text-[10px] uppercase tracking-wide text-muted-foreground mb-4 border-b border-white/5 pb-3 px-1">{data.formattedDate}</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-10">
|
||||
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-wide">Baseline:</span>
|
||||
<span className="text-sm font-bold text-[#8884d8] tabular-nums">{formatGBP(data.baseline)}</span>
|
||||
</div>
|
||||
{committedSimulationFactor !== 0 && (
|
||||
<div className="flex items-center justify-between gap-10">
|
||||
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-wide">Simulated:</span>
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-sm font-bold text-emerald-400 tabular-nums">{formatGBP(data.simulated)}</span>
|
||||
<span className={`text-[10px] font-bold mt-0.5 ${data.simulated > data.baseline ? 'text-emerald-500' : 'text-rose-500'}`}>
|
||||
{data.simulated > data.baseline ? '▴' : '▾'} {Math.abs(((data.simulated / data.baseline - 1) * 100)).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-10 pt-3 border-t border-white/5">
|
||||
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-widest">Est. Orders:</span>
|
||||
<span className="text-sm font-bold tabular-nums">
|
||||
{Math.round(data.orders)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
{/* Always show baseline as solid line */}
|
||||
{/* Always show baseline */}
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="baseline"
|
||||
stroke="#8884d8"
|
||||
fillOpacity={committedSimulationFactor !== 0 ? 0.3 : 1}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorBaseline)"
|
||||
strokeWidth={committedSimulationFactor !== 0 ? 1 : 2}
|
||||
activeDot={{ r: 4, strokeWidth: 0 }}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
activeDot={{ r: 4, strokeWidth: 0, fill: "#8884d8" }}
|
||||
/>
|
||||
{/* Show simulated line when simulation is active */}
|
||||
{committedSimulationFactor !== 0 && (
|
||||
@@ -694,11 +771,12 @@ export default function PredictionsChart({
|
||||
type="monotone"
|
||||
dataKey="simulated"
|
||||
stroke="#10b981"
|
||||
fillOpacity={0.6}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorSimulated)"
|
||||
strokeWidth={3}
|
||||
strokeDasharray="5 5"
|
||||
strokeWidth={2}
|
||||
activeDot={{ r: 5, strokeWidth: 0 }}
|
||||
dot={false}
|
||||
activeDot={{ r: 6, strokeWidth: 3, stroke: "#fff", fill: "#10b981" }}
|
||||
/>
|
||||
)}
|
||||
</AreaChart>
|
||||
|
||||
Reference in New Issue
Block a user