Revamp analytics dashboard UI and charts
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:
g
2026-01-12 05:44:54 +00:00
parent a0605e47de
commit a05787a091
9 changed files with 613 additions and 398 deletions

View File

@@ -16,6 +16,7 @@ body {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
@@ -61,7 +62,10 @@ body {
}
/* Larger touch targets for interactive elements */
button, input, textarea, [role="button"] {
button,
input,
textarea,
[role="button"] {
min-height: 44px;
}
}
@@ -90,7 +94,9 @@ body {
}
/* Improved focus visibility */
input:focus, textarea:focus, button:focus {
input:focus,
textarea:focus,
button:focus {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
@@ -102,6 +108,7 @@ body {
/* Chromebook-specific optimizations */
@media screen and (max-width: 1366px) and (min-resolution: 1.5dppx) {
/* Chromebook display optimizations */
.text-sm {
font-size: 0.875rem;
@@ -114,7 +121,11 @@ body {
}
/* Better touch targets for Chromebooks */
button, input, textarea, [role="button"], [role="tab"] {
button,
input,
textarea,
[role="button"],
[role="tab"] {
min-height: 48px;
min-width: 48px;
}
@@ -131,6 +142,7 @@ body {
/* Chromebook touch screen optimizations */
@media (pointer: coarse) and (hover: none) {
/* Larger touch targets */
.touch-target {
min-height: 52px;
@@ -148,13 +160,16 @@ body {
}
/* Better input field sizing */
input, textarea {
input,
textarea {
padding: 0.875rem;
font-size: 1rem;
}
/* Enhanced focus states for touch */
button:focus-visible, input:focus-visible, textarea:focus-visible {
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 3px solid hsl(var(--ring));
outline-offset: 2px;
}
@@ -162,6 +177,7 @@ body {
/* Chromebook keyboard navigation improvements */
@media (hover: hover) and (pointer: fine) {
/* Better hover states for mouse/trackpad */
button:hover:not(:disabled) {
transform: translateY(-1px);
@@ -169,7 +185,9 @@ body {
}
/* Improved focus indicators */
button:focus-visible, input:focus-visible, textarea:focus-visible {
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
box-shadow: 0 0 0 4px hsl(var(--ring) / 0.2);
@@ -178,6 +196,7 @@ body {
/* Chromebook display scaling fixes */
@media screen and (min-resolution: 1.5dppx) {
/* Prevent text from being too small on high-DPI displays */
html {
-webkit-text-size-adjust: 100%;
@@ -254,8 +273,17 @@ body {
/* Christmas-themed animations */
@keyframes twinkle {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.3; transform: scale(0.8); }
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(0.8);
}
}
@keyframes snowflake {
@@ -263,6 +291,7 @@ body {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(360deg);
opacity: 0;
@@ -270,10 +299,13 @@ body {
}
@keyframes sparkle {
0%, 100% {
0%,
100% {
opacity: 0;
transform: scale(0) rotate(0deg);
}
50% {
opacity: 1;
transform: scale(1) rotate(180deg);
@@ -281,9 +313,12 @@ body {
}
@keyframes glow {
0%, 100% {
0%,
100% {
box-shadow: 0 0 5px hsl(var(--christmas-red)), 0 0 10px hsl(var(--christmas-red)), 0 0 15px hsl(var(--christmas-red));
}
50% {
box-shadow: 0 0 10px hsl(var(--christmas-green)), 0 0 20px hsl(var(--christmas-green)), 0 0 30px hsl(var(--christmas-green));
}
@@ -376,6 +411,42 @@ body {
.christmas-theme *:focus-visible {
outline-color: hsl(var(--christmas-red));
}
/* Premium UI Utilities */
.glass-morphism {
@apply bg-background/60 backdrop-blur-md border border-border/50;
}
.dark .glass-morphism {
@apply bg-black/40 backdrop-blur-xl border-white/5;
}
.premium-card {
@apply transition-all duration-300;
}
.premium-card:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
border-color: hsl(var(--primary) / 0.2);
}
.dark .premium-card {
@apply bg-card;
}
.dark .premium-card:hover {
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
border-color: hsl(var(--primary) / 0.2);
}
.text-gradient {
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/60;
}
.bg-gradient-premium {
background: radial-gradient(circle at top left, hsl(var(--primary) / 0.05), transparent),
radial-gradient(circle at bottom right, hsl(var(--primary) / 0.02), transparent);
}
}
@layer base {
@@ -410,26 +481,27 @@ body {
--christmas-green: 142 76% 36%;
--christmas-gold: 43 96% 56%;
}
.dark {
--background: 0 0% 3.9%;
--background: 240 10% 2%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card: 240 10% 3%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover: 240 10% 2%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary: 240 4% 10%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--muted: 240 4% 10%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 4% 10%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--border: 240 4% 12%;
--input: 240 4% 12%;
--ring: 240 5% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
@@ -464,6 +536,7 @@ body {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}

View File

@@ -1365,8 +1365,8 @@ export default function AdminAnalytics() {
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
</div>
) : growthData?.customers ? (
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<div className="h-64 min-w-0">
<ResponsiveContainer key={growthData?.customers ? 'ready' : 'loading'} width="100%" height="100%">
<PieChart>
<Pie
data={[

View File

@@ -195,54 +195,115 @@ export default function AnalyticsDashboard({
];
return (
<div className="space-y-10">
{/* Header with Privacy Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-10 pb-20">
{/* Header with Integrated Toolbar */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
<h2 className="text-3xl font-bold tracking-tight">
Analytics Dashboard
</h2>
<p className="text-muted-foreground">
Overview of your store's performance and metrics.
<p className="text-muted-foreground mt-1">
Real-time performance metrics and AI-driven insights.
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 p-1.5 glass-morphism rounded-2xl border border-white/5 shadow-2xl backdrop-blur-xl ring-1 ring-white/5">
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={() => setHideNumbers(!hideNumbers)}
className="flex items-center gap-2"
className={`flex items-center gap-2 rounded-xl transition-all font-medium px-4 ${hideNumbers ? 'bg-primary text-primary-foreground shadow-lg' : 'hover:bg-white/5'}`}
>
{hideNumbers ? (
<>
<EyeOff className="h-4 w-4" />
Show Numbers
<span className="hidden sm:inline">Numbers Hidden</span>
</>
) : (
<>
<Eye className="h-4 w-4" />
Hide Numbers
<Eye className="h-4 w-4 text-primary/70" />
<span className="hidden sm:inline">Hide Numbers</span>
</>
)}
</Button>
<div className="w-px h-5 bg-white/10 mx-1" />
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={refreshData}
disabled={isLoading}
className="flex items-center gap-2"
className="flex items-center gap-2 rounded-xl hover:bg-white/5 font-medium px-4"
>
<RefreshCw
className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`}
className={`h-4 w-4 ${isLoading ? "animate-spin text-primary" : "text-primary/70"}`}
/>
Refresh
<span className="hidden sm:inline">Refresh Data</span>
</Button>
</div>
</div>
<MotionWrapper className="space-y-12">
{/* Analytics Tabs Setup */}
<Tabs defaultValue="overview" className="space-y-10">
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-6 pb-2">
<TabsList className="bg-transparent h-auto p-0 flex flex-wrap gap-2 lg:gap-4">
<TabsTrigger
value="overview"
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
>
<Activity className="h-4 w-4" />
Overview
</TabsTrigger>
<TabsTrigger
value="financials"
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
>
<DollarSign className="h-4 w-4" />
Financials
</TabsTrigger>
<TabsTrigger
value="performance"
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
>
<BarChart3 className="h-4 w-4" />
Performance
</TabsTrigger>
<TabsTrigger
value="ai"
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
>
<Calculator className="h-4 w-4" />
AI Insights
</TabsTrigger>
</TabsList>
{/* Contextual Time Range Selector */}
<div className="flex items-center gap-3 bg-muted/30 p-1 rounded-xl border border-border/20">
<span className="text-xs font-semibold text-muted-foreground px-2">Range</span>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-[130px] h-8 border-none bg-transparent shadow-none focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="rounded-xl border-border/40">
<SelectItem value="7">Last 7 days</SelectItem>
<SelectItem value="30">Last 30 days</SelectItem>
<SelectItem value="90">Last 90 days</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<TabsContent value="overview" className="space-y-10 outline-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="space-y-10"
>
{/* Key Metrics Cards */}
<MotionWrapper className="space-y-10">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 lg:gap-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{isLoading
? [...Array(4)].map((_, i) => <MetricsCardSkeleton key={i} />)
: metrics.map((metric) => (
@@ -250,156 +311,88 @@ export default function AnalyticsDashboard({
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Completion Rate Card */}
<motion.div>
<Card className="hover:shadow-xl hover:border-indigo-500/30 transition-all duration-300">
<Card className="lg:col-span-1 glass-morphism premium-card">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Order Completion Rate
<CardTitle className="flex items-center gap-2 text-lg">
<Activity className="h-5 w-5 text-emerald-500" />
Order Completion
</CardTitle>
<CardDescription>
Percentage of orders that have been successfully completed
Successfully processed orders
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center gap-4">
<div className="h-12 w-16 bg-muted/20 rounded animate-pulse" />
<div className="flex-1">
<div className="w-full bg-muted/20 rounded-full h-2 animate-pulse" />
</div>
<div className="h-6 w-16 bg-muted/20 rounded animate-pulse" />
</div>
<Skeleton className="h-24 w-full rounded-2xl" />
) : (
<div className="flex items-center gap-4">
<div className="text-3xl font-bold">
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="text-4xl font-extrabold tracking-tight">
{hideNumbers ? "**%" : `${data.orders.completionRate}%`}
</div>
<div className="flex-1">
<div className="w-full bg-secondary rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{
width: hideNumbers
? "0%"
: `${data.orders.completionRate}%`,
}}
/>
</div>
</div>
<Badge variant="secondary">
<Badge variant="outline" className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20 px-3 py-1 text-xs font-bold">
{hideNumbers
? "** / **"
: `${data.orders.completed} / ${data.orders.total}`}
</Badge>
</div>
<div className="w-full bg-secondary/50 rounded-full h-3 overflow-hidden border border-border/20">
<motion.div
initial={{ width: 0 }}
animate={{ width: hideNumbers ? "0%" : `${data.orders.completionRate}%` }}
transition={{ duration: 1, ease: "circOut" }}
className="bg-gradient-to-r from-emerald-500 to-teal-400 h-full rounded-full"
/>
</div>
</div>
)}
</CardContent>
</Card>
</motion.div>
{/* Time Period Selector */}
<div className="flex flex-col sm:flex-row gap-4 sm:items-center sm:justify-between">
<div>
<h3 className="text-lg font-semibold">Time Period</h3>
<p className="text-sm text-muted-foreground">
Revenue, Profit, and Orders tabs use time filtering. Products and
Customers show all-time data.
</p>
</div>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Last 7 days</SelectItem>
<SelectItem value="30">Last 30 days</SelectItem>
<SelectItem value="90">Last 90 days</SelectItem>
</SelectContent>
</Select>
</div>
{/* Analytics Tabs */}
<div className="space-y-8">
<Tabs defaultValue="growth" className="space-y-8">
<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">
<Activity className="h-4 w-4" />
Growth
</TabsTrigger>
<TabsTrigger value="revenue" className="flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Revenue
</TabsTrigger>
<TabsTrigger value="profit" className="flex items-center gap-2">
<Calculator className="h-4 w-4" />
Profit
</TabsTrigger>
<TabsTrigger value="products" className="flex items-center gap-2">
<Package className="h-4 w-4" />
Products
</TabsTrigger>
<TabsTrigger value="customers" className="flex items-center gap-2">
<Users className="h-4 w-4" />
Customers
</TabsTrigger>
<TabsTrigger value="orders" className="flex items-center gap-2">
<BarChart3 className="h-4 w-4" />
Orders
</TabsTrigger>
<TabsTrigger value="predictions" className="flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Predictions
</TabsTrigger>
</TabsList>
<TabsContent value="growth" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
{/* Growth Chart Snippet (Simplified) */}
<div className="lg:col-span-2 min-w-0">
<Suspense fallback={<ChartSkeleton />}>
<GrowthAnalyticsChart hideNumbers={hideNumbers} />
</Suspense>
</div>
</div>
</motion.div>
</TabsContent>
<TabsContent value="revenue" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<TabsContent value="financials" className="space-y-8 outline-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="grid grid-cols-1 xl:grid-cols-2 gap-8"
>
<Suspense fallback={<ChartSkeleton />}>
<div className="min-w-0">
<RevenueChart timeRange={timeRange} hideNumbers={hideNumbers} />
</div>
</Suspense>
</motion.div>
</TabsContent>
<TabsContent value="profit" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
{/* Date Range Selector for Profit Calculator */}
<Card>
<div className="space-y-8">
<Card className="glass-morphism">
<CardHeader>
<CardTitle className="text-lg">Date Range</CardTitle>
<CardTitle className="text-lg">Profit Range</CardTitle>
<CardDescription>
Select a custom date range for profit calculations
Custom date selection for analysis
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col sm:flex-row gap-4 sm:items-center">
<DateRangePicker
dateRange={profitDateRange}
onDateRangeChange={setProfitDateRange}
placeholder="Select date range"
showPresets={true}
className="w-full sm:w-auto"
className="w-full"
/>
{profitDateRange?.from && profitDateRange?.to && (
<div className="text-sm text-muted-foreground flex items-center">
<span>
{profitDateRange.from.toLocaleDateString()} -{" "}
{profitDateRange.to.toLocaleDateString()}
</span>
</div>
)}
</div>
</CardContent>
</Card>
<Suspense fallback={<ChartSkeleton />}>
<ProfitAnalyticsChart
dateRange={
@@ -413,42 +406,46 @@ export default function AnalyticsDashboard({
hideNumbers={hideNumbers}
/>
</Suspense>
</div>
</motion.div>
</TabsContent>
<TabsContent value="products" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<TabsContent value="performance" className="space-y-8 outline-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="grid grid-cols-1 lg:grid-cols-2 gap-8"
>
<Suspense fallback={<ChartSkeleton />}>
<div className="min-w-0">
<ProductPerformanceChart />
</div>
</Suspense>
</motion.div>
</TabsContent>
<TabsContent value="customers" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<Suspense fallback={<ChartSkeleton />}>
<CustomerInsightsChart />
</Suspense>
</motion.div>
</TabsContent>
<TabsContent value="orders" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<div className="space-y-8 min-w-0">
<Suspense fallback={<ChartSkeleton />}>
<OrderAnalyticsChart timeRange={timeRange} />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<CustomerInsightsChart />
</Suspense>
</div>
</motion.div>
</TabsContent>
<TabsContent value="predictions" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<TabsContent value="ai" className="space-y-8 outline-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="min-w-0"
>
<Suspense fallback={<ChartSkeleton />}>
<PredictionsChart timeRange={parseInt(timeRange)} />
</Suspense>
</motion.div>
</TabsContent>
</Tabs>
</div>
</MotionWrapper>
</div>
);

View File

@@ -183,7 +183,7 @@ export default function GrowthAnalyticsChart({
</div>
) : growthData?.monthly && growthData.monthly.length > 0 ? (
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer key={growthData?.monthly?.length || 0} width="100%" height="100%">
<ComposedChart
data={growthData.monthly.map((m) => ({
...m,

View File

@@ -25,43 +25,89 @@ export default function MetricsCard({
const getTrendIcon = () => {
switch (trend) {
case "up":
return <TrendingUp className="h-4 w-4 text-green-500" />;
return <TrendingUp className="h-4 w-4 text-emerald-500" />;
case "down":
return <TrendingDown className="h-4 w-4 text-red-500" />;
return <TrendingDown className="h-4 w-4 text-rose-500" />;
default:
return <Minus className="h-4 w-4 text-gray-500" />;
return <Minus className="h-4 w-4 text-muted-foreground" />;
}
};
const getTrendColor = () => {
switch (trend) {
case "up":
return "text-green-600";
return "text-emerald-400 bg-emerald-500/10 border-emerald-500/10";
case "down":
return "text-red-600";
return "text-rose-400 bg-rose-500/10 border-rose-500/10";
default:
return "text-gray-600";
return "text-blue-400 bg-blue-500/10 border-blue-500/10";
}
};
const getCategoryColor = () => {
const t = title.toLowerCase();
if (t.includes("revenue") || t.includes("profit")) return "amber";
if (t.includes("order")) return "blue";
if (t.includes("customer")) return "indigo";
if (t.includes("product") || t.includes("inventory")) return "purple";
return "primary";
}
const categoryColor = getCategoryColor();
const getIconContainerColor = () => {
switch (categoryColor) {
case "amber": return "bg-amber-500/15 text-amber-500 border-amber-500/20";
case "blue": return "bg-blue-500/15 text-blue-500 border-blue-500/20";
case "indigo": return "bg-indigo-500/15 text-indigo-500 border-indigo-500/20";
case "purple": return "bg-purple-500/15 text-purple-500 border-purple-500/20";
default: return "bg-primary/15 text-primary border-primary/20";
}
}
const getBadgeColor = () => {
switch (categoryColor) {
case "amber": return "bg-amber-500/10 text-amber-400/80 border-amber-500/20";
case "blue": return "bg-blue-500/10 text-blue-400/80 border-blue-500/20";
case "indigo": return "bg-indigo-500/10 text-indigo-400/80 border-indigo-500/20";
case "purple": return "bg-purple-500/10 text-purple-400/80 border-purple-500/20";
default: return "bg-primary/10 text-primary/60 border-primary/20";
}
}
return (
<motion.div>
<Card className="hover:shadow-xl hover:border-indigo-500/30 transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
<motion.div
whileHover={{ y: -4 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<Card className="glass-morphism premium-card relative overflow-hidden group border-white/5">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4 relative z-10">
<CardTitle className="text-[10px] font-bold tracking-wider text-white/40 uppercase">
{title}
</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
<div className={`p-2.5 rounded-2xl border ${getIconContainerColor()} transition-all duration-300 group-hover:scale-105`}>
<Icon className="h-4 w-4" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground mt-1">{description}</p>
<div className="flex items-center gap-1 mt-2">
<CardContent className="relative z-10">
<div className="space-y-1.5">
<div className="text-3xl font-bold text-white">
{value}
</div>
<p className="text-[11px] text-white/40">
{description}
</p>
</div>
<div className="flex items-center gap-2 mt-6 pt-5">
<div className={`flex items-center gap-1.5 px-3 py-1 rounded-full border ${getTrendColor()} transition-all duration-300`}>
{getTrendIcon()}
<span className={`text-xs ${getTrendColor()}`}>
{trendValue}
<span className="text-[10px] font-bold uppercase tracking-wide">
{trend === "up" ? "+" : ""}{trendValue}
</span>
</div>
</div>
</CardContent>
</Card>
</motion.div>

View File

@@ -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,20 +576,39 @@ 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
<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>
<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>
<CardDescription className="text-muted-foreground/80 font-medium">
Adjust variables to see how traffic shifts impact your bottom line.
</CardDescription>
</div>
<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}
@@ -582,37 +616,64 @@ export default function PredictionsChart({
step={10}
onValueChange={(val) => setSimulationFactor(val[0])}
onValueCommit={(val) => setCommittedSimulationFactor(val[0])}
className="w-[150px] mt-1.5"
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>
{simulationFactor !== 0 && (
</div>
{(simulationFactor !== 0 || committedSimulationFactor !== 0) && (
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
className="h-9 w-9 hover:bg-white/10 rounded-xl transition-all"
onClick={() => {
setSimulationFactor(0);
setCommittedSimulationFactor(0);
}}
title="Reset simulation"
title="Reset Scenario"
>
<RefreshCw className="h-3 w-3" />
<RefreshCw className="h-4 w-4 text-primary/70" />
</Button>
)}
</div>
<Button variant="outline" size="sm" onClick={handleExportCSV} title="Export to CSV">
<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
Export Forecast
</Button>
</div>
</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>
<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 && (
<p className="text-sm text-green-600">
Simulated: <span className="font-semibold">{formatGBP(data.simulated)}</span>
</p>
<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>

View File

@@ -11,7 +11,8 @@ import {
PieChart,
Calculator,
Info,
AlertTriangle
AlertTriangle,
Package
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { formatGBP } from "@/utils/format";
@@ -28,6 +29,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
const [data, setData] = useState<ProfitOverview | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
const { toast } = useToast();
const maskValue = (value: string): string => {
@@ -237,8 +239,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
<CardTitle className="text-sm font-medium">Total Profit</CardTitle>
</CardHeader>
<CardContent>
<div className={`text-2xl font-bold flex items-center gap-2 ${
profitDirection ? 'text-green-600' : 'text-red-600'
<div className={`text-2xl font-bold flex items-center gap-2 ${profitDirection ? 'text-green-600' : 'text-red-600'
}`}>
{profitDirection ? (
<TrendingUp className="h-5 w-5" />
@@ -327,15 +328,34 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
return (
<div
key={product.productId}
className="flex items-center justify-between p-4 border rounded-lg"
className="flex items-center justify-between p-4 border rounded-lg transition-colors hover:bg-muted/30"
>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
<div className="flex items-center gap-4">
<div className="relative flex-shrink-0">
<div className="w-12 h-12 rounded-full overflow-hidden border-2 border-background shadow-sm bg-muted flex items-center justify-center">
{product.image && !imageErrors[product.productId] ? (
<img
src={`/api/products/${product.productId}/image`}
alt={product.productName}
className="w-full h-full object-cover"
onError={() => {
setImageErrors(prev => ({ ...prev, [product.productId]: true }));
}}
/>
) : (
<div className="flex items-center justify-center w-full h-full bg-primary/10 text-primary font-bold text-lg">
{product.productName.charAt(0)}
</div>
)}
</div>
<div className="absolute -top-1 -left-1 w-5 h-5 bg-primary text-[10px] text-primary-foreground flex items-center justify-center rounded-full font-bold border-2 border-background shadow-sm">
{index + 1}
</div>
</div>
<div>
<p className="font-medium">{product.productName}</p>
<p className="text-sm text-muted-foreground">
<p className="font-semibold">{product.productName}</p>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<Package className="h-3 w-3" />
{product.totalQuantitySold} units sold
</p>
</div>

View File

@@ -175,7 +175,7 @@ export default function RevenueChart({ timeRange, hideNumbers = false }: Revenue
<div className="space-y-6">
{/* Chart */}
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer key={timeRange} width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<defs>
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">

View File

@@ -18,6 +18,7 @@ export interface ProfitOverview {
topProfitableProducts: Array<{
productId: string;
productName: string;
image?: string;
totalQuantitySold: number;
totalRevenue: number;
totalCost: number;