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:
113
app/globals.css
113
app/globals.css
@@ -16,6 +16,7 @@ body {
|
|||||||
0% {
|
0% {
|
||||||
background-position: -200% 0;
|
background-position: -200% 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: 200% 0;
|
background-position: 200% 0;
|
||||||
}
|
}
|
||||||
@@ -61,7 +62,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Larger touch targets for interactive elements */
|
/* Larger touch targets for interactive elements */
|
||||||
button, input, textarea, [role="button"] {
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
[role="button"] {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +94,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Improved focus visibility */
|
/* Improved focus visibility */
|
||||||
input:focus, textarea:focus, button:focus {
|
input:focus,
|
||||||
|
textarea:focus,
|
||||||
|
button:focus {
|
||||||
outline: 2px solid hsl(var(--ring));
|
outline: 2px solid hsl(var(--ring));
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
@@ -102,6 +108,7 @@ body {
|
|||||||
|
|
||||||
/* Chromebook-specific optimizations */
|
/* Chromebook-specific optimizations */
|
||||||
@media screen and (max-width: 1366px) and (min-resolution: 1.5dppx) {
|
@media screen and (max-width: 1366px) and (min-resolution: 1.5dppx) {
|
||||||
|
|
||||||
/* Chromebook display optimizations */
|
/* Chromebook display optimizations */
|
||||||
.text-sm {
|
.text-sm {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
@@ -114,7 +121,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Better touch targets for Chromebooks */
|
/* Better touch targets for Chromebooks */
|
||||||
button, input, textarea, [role="button"], [role="tab"] {
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
[role="button"],
|
||||||
|
[role="tab"] {
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
}
|
}
|
||||||
@@ -131,6 +142,7 @@ body {
|
|||||||
|
|
||||||
/* Chromebook touch screen optimizations */
|
/* Chromebook touch screen optimizations */
|
||||||
@media (pointer: coarse) and (hover: none) {
|
@media (pointer: coarse) and (hover: none) {
|
||||||
|
|
||||||
/* Larger touch targets */
|
/* Larger touch targets */
|
||||||
.touch-target {
|
.touch-target {
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
@@ -148,13 +160,16 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Better input field sizing */
|
/* Better input field sizing */
|
||||||
input, textarea {
|
input,
|
||||||
|
textarea {
|
||||||
padding: 0.875rem;
|
padding: 0.875rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced focus states for touch */
|
/* 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: 3px solid hsl(var(--ring));
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
@@ -162,6 +177,7 @@ body {
|
|||||||
|
|
||||||
/* Chromebook keyboard navigation improvements */
|
/* Chromebook keyboard navigation improvements */
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
|
|
||||||
/* Better hover states for mouse/trackpad */
|
/* Better hover states for mouse/trackpad */
|
||||||
button:hover:not(:disabled) {
|
button:hover:not(:disabled) {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
@@ -169,7 +185,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Improved focus indicators */
|
/* 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: 2px solid hsl(var(--ring));
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
box-shadow: 0 0 0 4px hsl(var(--ring) / 0.2);
|
box-shadow: 0 0 0 4px hsl(var(--ring) / 0.2);
|
||||||
@@ -178,6 +196,7 @@ body {
|
|||||||
|
|
||||||
/* Chromebook display scaling fixes */
|
/* Chromebook display scaling fixes */
|
||||||
@media screen and (min-resolution: 1.5dppx) {
|
@media screen and (min-resolution: 1.5dppx) {
|
||||||
|
|
||||||
/* Prevent text from being too small on high-DPI displays */
|
/* Prevent text from being too small on high-DPI displays */
|
||||||
html {
|
html {
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
@@ -254,8 +273,17 @@ body {
|
|||||||
|
|
||||||
/* Christmas-themed animations */
|
/* Christmas-themed animations */
|
||||||
@keyframes twinkle {
|
@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 {
|
@keyframes snowflake {
|
||||||
@@ -263,6 +291,7 @@ body {
|
|||||||
transform: translateY(-100vh) rotate(0deg);
|
transform: translateY(-100vh) rotate(0deg);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(100vh) rotate(360deg);
|
transform: translateY(100vh) rotate(360deg);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -270,10 +299,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes sparkle {
|
@keyframes sparkle {
|
||||||
0%, 100% {
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0) rotate(0deg);
|
transform: scale(0) rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1) rotate(180deg);
|
transform: scale(1) rotate(180deg);
|
||||||
@@ -281,9 +313,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glow {
|
@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));
|
box-shadow: 0 0 5px hsl(var(--christmas-red)), 0 0 10px hsl(var(--christmas-red)), 0 0 15px hsl(var(--christmas-red));
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 10px hsl(var(--christmas-green)), 0 0 20px hsl(var(--christmas-green)), 0 0 30px hsl(var(--christmas-green));
|
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 {
|
.christmas-theme *:focus-visible {
|
||||||
outline-color: hsl(var(--christmas-red));
|
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 {
|
@layer base {
|
||||||
@@ -410,26 +481,27 @@ body {
|
|||||||
--christmas-green: 142 76% 36%;
|
--christmas-green: 142 76% 36%;
|
||||||
--christmas-gold: 43 96% 56%;
|
--christmas-gold: 43 96% 56%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 0 0% 3.9%;
|
--background: 240 10% 2%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 0 0% 3.9%;
|
--card: 240 10% 3%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
--popover: 0 0% 3.9%;
|
--popover: 240 10% 2%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--primary: 0 0% 98%;
|
--primary: 0 0% 98%;
|
||||||
--primary-foreground: 0 0% 9%;
|
--primary-foreground: 0 0% 9%;
|
||||||
--secondary: 0 0% 14.9%;
|
--secondary: 240 4% 10%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--muted: 0 0% 14.9%;
|
--muted: 240 4% 10%;
|
||||||
--muted-foreground: 0 0% 63.9%;
|
--muted-foreground: 240 5% 64.9%;
|
||||||
--accent: 0 0% 14.9%;
|
--accent: 240 4% 10%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 0 0% 14.9%;
|
--border: 240 4% 12%;
|
||||||
--input: 0 0% 14.9%;
|
--input: 240 4% 12%;
|
||||||
--ring: 0 0% 83.1%;
|
--ring: 240 5% 83.1%;
|
||||||
--chart-1: 220 70% 50%;
|
--chart-1: 220 70% 50%;
|
||||||
--chart-2: 160 60% 45%;
|
--chart-2: 160 60% 45%;
|
||||||
--chart-3: 30 80% 55%;
|
--chart-3: 30 80% 55%;
|
||||||
@@ -464,6 +536,7 @@ body {
|
|||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
) : growthData?.customers ? (
|
) : growthData?.customers ? (
|
||||||
<div className="h-64">
|
<div className="h-64 min-w-0">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer key={growthData?.customers ? 'ready' : 'loading'} width="100%" height="100%">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
data={[
|
data={[
|
||||||
|
|||||||
@@ -195,54 +195,115 @@ export default function AnalyticsDashboard({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10 pb-20">
|
||||||
{/* Header with Privacy Toggle */}
|
{/* Header with Integrated Toolbar */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-bold tracking-tight">
|
<h2 className="text-3xl font-bold tracking-tight">
|
||||||
Analytics Dashboard
|
Analytics Dashboard
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground mt-1">
|
||||||
Overview of your store's performance and metrics.
|
Real-time performance metrics and AI-driven insights.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setHideNumbers(!hideNumbers)}
|
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 ? (
|
{hideNumbers ? (
|
||||||
<>
|
<>
|
||||||
<EyeOff className="h-4 w-4" />
|
<EyeOff className="h-4 w-4" />
|
||||||
Show Numbers
|
<span className="hidden sm:inline">Numbers Hidden</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4 text-primary/70" />
|
||||||
Hide Numbers
|
<span className="hidden sm:inline">Hide Numbers</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<div className="w-px h-5 bg-white/10 mx-1" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={refreshData}
|
onClick={refreshData}
|
||||||
disabled={isLoading}
|
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
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Key Metrics Cards */}
|
||||||
<MotionWrapper className="space-y-10">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 lg:gap-8">
|
|
||||||
{isLoading
|
{isLoading
|
||||||
? [...Array(4)].map((_, i) => <MetricsCardSkeleton key={i} />)
|
? [...Array(4)].map((_, i) => <MetricsCardSkeleton key={i} />)
|
||||||
: metrics.map((metric) => (
|
: metrics.map((metric) => (
|
||||||
@@ -250,156 +311,88 @@ export default function AnalyticsDashboard({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
{/* Completion Rate Card */}
|
{/* Completion Rate Card */}
|
||||||
<motion.div>
|
<Card className="lg:col-span-1 glass-morphism premium-card">
|
||||||
<Card className="hover:shadow-xl hover:border-indigo-500/30 transition-all duration-300">
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<Activity className="h-5 w-5" />
|
<Activity className="h-5 w-5 text-emerald-500" />
|
||||||
Order Completion Rate
|
Order Completion
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Percentage of orders that have been successfully completed
|
Successfully processed orders
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center gap-4">
|
<Skeleton className="h-24 w-full rounded-2xl" />
|
||||||
<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>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-4">
|
<div className="space-y-6">
|
||||||
<div className="text-3xl font-bold">
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-4xl font-extrabold tracking-tight">
|
||||||
{hideNumbers ? "**%" : `${data.orders.completionRate}%`}
|
{hideNumbers ? "**%" : `${data.orders.completionRate}%`}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<Badge variant="outline" className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20 px-3 py-1 text-xs font-bold">
|
||||||
<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">
|
|
||||||
{hideNumbers
|
{hideNumbers
|
||||||
? "** / **"
|
? "** / **"
|
||||||
: `${data.orders.completed} / ${data.orders.total}`}
|
: `${data.orders.completed} / ${data.orders.total}`}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Time Period Selector */}
|
{/* Growth Chart Snippet (Simplified) */}
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:items-center sm:justify-between">
|
<div className="lg:col-span-2 min-w-0">
|
||||||
<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 }}>
|
|
||||||
<Suspense fallback={<ChartSkeleton />}>
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
<GrowthAnalyticsChart hideNumbers={hideNumbers} />
|
<GrowthAnalyticsChart hideNumbers={hideNumbers} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="revenue" className="space-y-6">
|
<TabsContent value="financials" className="space-y-8 outline-none">
|
||||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
<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 />}>
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
|
<div className="min-w-0">
|
||||||
<RevenueChart timeRange={timeRange} hideNumbers={hideNumbers} />
|
<RevenueChart timeRange={timeRange} hideNumbers={hideNumbers} />
|
||||||
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</motion.div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="profit" className="space-y-6">
|
<div className="space-y-8">
|
||||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
<Card className="glass-morphism">
|
||||||
{/* Date Range Selector for Profit Calculator */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">Date Range</CardTitle>
|
<CardTitle className="text-lg">Profit Range</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Select a custom date range for profit calculations
|
Custom date selection for analysis
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:items-center">
|
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
dateRange={profitDateRange}
|
dateRange={profitDateRange}
|
||||||
onDateRangeChange={setProfitDateRange}
|
onDateRangeChange={setProfitDateRange}
|
||||||
placeholder="Select date range"
|
placeholder="Select date range"
|
||||||
showPresets={true}
|
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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Suspense fallback={<ChartSkeleton />}>
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
<ProfitAnalyticsChart
|
<ProfitAnalyticsChart
|
||||||
dateRange={
|
dateRange={
|
||||||
@@ -413,42 +406,46 @@ export default function AnalyticsDashboard({
|
|||||||
hideNumbers={hideNumbers}
|
hideNumbers={hideNumbers}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="products" className="space-y-6">
|
<TabsContent value="performance" className="space-y-8 outline-none">
|
||||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
<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 />}>
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
|
<div className="min-w-0">
|
||||||
<ProductPerformanceChart />
|
<ProductPerformanceChart />
|
||||||
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</motion.div>
|
<div className="space-y-8 min-w-0">
|
||||||
</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 }}>
|
|
||||||
<Suspense fallback={<ChartSkeleton />}>
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
<OrderAnalyticsChart timeRange={timeRange} />
|
<OrderAnalyticsChart timeRange={timeRange} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
|
<CustomerInsightsChart />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="predictions" className="space-y-6">
|
<TabsContent value="ai" className="space-y-8 outline-none">
|
||||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
className="min-w-0"
|
||||||
|
>
|
||||||
<Suspense fallback={<ChartSkeleton />}>
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
<PredictionsChart timeRange={parseInt(timeRange)} />
|
<PredictionsChart timeRange={parseInt(timeRange)} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
|
||||||
</MotionWrapper>
|
</MotionWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export default function GrowthAnalyticsChart({
|
|||||||
</div>
|
</div>
|
||||||
) : growthData?.monthly && growthData.monthly.length > 0 ? (
|
) : growthData?.monthly && growthData.monthly.length > 0 ? (
|
||||||
<div className="h-80">
|
<div className="h-80">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer key={growthData?.monthly?.length || 0} width="100%" height="100%">
|
||||||
<ComposedChart
|
<ComposedChart
|
||||||
data={growthData.monthly.map((m) => ({
|
data={growthData.monthly.map((m) => ({
|
||||||
...m,
|
...m,
|
||||||
|
|||||||
@@ -25,43 +25,89 @@ export default function MetricsCard({
|
|||||||
const getTrendIcon = () => {
|
const getTrendIcon = () => {
|
||||||
switch (trend) {
|
switch (trend) {
|
||||||
case "up":
|
case "up":
|
||||||
return <TrendingUp className="h-4 w-4 text-green-500" />;
|
return <TrendingUp className="h-4 w-4 text-emerald-500" />;
|
||||||
case "down":
|
case "down":
|
||||||
return <TrendingDown className="h-4 w-4 text-red-500" />;
|
return <TrendingDown className="h-4 w-4 text-rose-500" />;
|
||||||
default:
|
default:
|
||||||
return <Minus className="h-4 w-4 text-gray-500" />;
|
return <Minus className="h-4 w-4 text-muted-foreground" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTrendColor = () => {
|
const getTrendColor = () => {
|
||||||
switch (trend) {
|
switch (trend) {
|
||||||
case "up":
|
case "up":
|
||||||
return "text-green-600";
|
return "text-emerald-400 bg-emerald-500/10 border-emerald-500/10";
|
||||||
case "down":
|
case "down":
|
||||||
return "text-red-600";
|
return "text-rose-400 bg-rose-500/10 border-rose-500/10";
|
||||||
default:
|
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 (
|
return (
|
||||||
<motion.div>
|
<motion.div
|
||||||
<Card className="hover:shadow-xl hover:border-indigo-500/30 transition-all duration-300">
|
whileHover={{ y: -4 }}
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
>
|
||||||
|
<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}
|
{title}
|
||||||
</CardTitle>
|
</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>
|
</CardHeader>
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{value}</div>
|
<CardContent className="relative z-10">
|
||||||
<p className="text-xs text-muted-foreground mt-1">{description}</p>
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center gap-1 mt-2">
|
<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()}
|
{getTrendIcon()}
|
||||||
<span className={`text-xs ${getTrendColor()}`}>
|
<span className="text-[10px] font-bold uppercase tracking-wide">
|
||||||
{trendValue}
|
{trend === "up" ? "+" : ""}{trendValue}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -164,6 +164,13 @@ export default function PredictionsChart({
|
|||||||
setSimulationFactor(0);
|
setSimulationFactor(0);
|
||||||
}, [timeRange]);
|
}, [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)
|
// Switch predictions when daysAhead changes (instant, from batch)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (batchData) {
|
if (batchData) {
|
||||||
@@ -322,10 +329,18 @@ export default function PredictionsChart({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="7">7 days</SelectItem>
|
<SelectItem value="7">7 days</SelectItem>
|
||||||
<SelectItem value="14">14 days</SelectItem>
|
<SelectItem value="14" disabled={timeRange < 14}>
|
||||||
<SelectItem value="30">30 days</SelectItem>
|
14 days {timeRange < 14 && "(Needs 14d history)"}
|
||||||
<SelectItem value="60">60 days</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="90">90 days</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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
@@ -386,7 +401,7 @@ export default function PredictionsChart({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent side="bottom" className="z-[100]">
|
||||||
<p>Predicted daily average revenue for the next {daysAhead} days</p>
|
<p>Predicted daily average revenue for the next {daysAhead} days</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -409,7 +424,7 @@ export default function PredictionsChart({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent side="bottom" className="z-[100]">
|
||||||
<p>Based on data consistency, historical accuracy, and model agreement</p>
|
<p>Based on data consistency, historical accuracy, and model agreement</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -428,7 +443,7 @@ export default function PredictionsChart({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent side="bottom" className="z-[100]">
|
||||||
<p>Predictions generated using a Deep Learning Ensemble Model</p>
|
<p>Predictions generated using a Deep Learning Ensemble Model</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -461,7 +476,7 @@ export default function PredictionsChart({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent side="bottom" className="z-[100]">
|
||||||
<p>Direction of the recent sales trend (slope analysis)</p>
|
<p>Direction of the recent sales trend (slope analysis)</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -504,7 +519,7 @@ export default function PredictionsChart({
|
|||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Info className="h-3 w-3 text-muted-foreground cursor-help" />
|
<Info className="h-3 w-3 text-muted-foreground cursor-help" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent side="bottom" className="z-[100]">
|
||||||
<p>Technical details about the active prediction model</p>
|
<p>Technical details about the active prediction model</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -521,7 +536,7 @@ export default function PredictionsChart({
|
|||||||
Hybrid Ensemble (Deep Learning)
|
Hybrid Ensemble (Deep Learning)
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent side="bottom" className="z-[100]">
|
||||||
<p>Combines LSTM Neural Networks with Statistical Methods (Holt-Winters, ARIMA)</p>
|
<p>Combines LSTM Neural Networks with Statistical Methods (Holt-Winters, ARIMA)</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -561,20 +576,39 @@ export default function PredictionsChart({
|
|||||||
{/* Daily Predictions Chart */}
|
{/* Daily Predictions Chart */}
|
||||||
{predictions?.sales?.dailyPredictions &&
|
{predictions?.sales?.dailyPredictions &&
|
||||||
predictions?.sales?.dailyPredictions.length > 0 && (
|
predictions?.sales?.dailyPredictions.length > 0 && (
|
||||||
<Card>
|
<Card className="glass-morphism border-primary/10 overflow-hidden">
|
||||||
<CardHeader className="pb-3 flex flex-row items-center justify-between space-y-0">
|
<CardHeader className="pb-6 bg-muted/5">
|
||||||
<CardTitle className="text-sm font-medium">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
Daily Revenue Forecast
|
<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>
|
</CardTitle>
|
||||||
<div className="flex items-center gap-4">
|
<CardDescription className="text-muted-foreground/80 font-medium">
|
||||||
<div className="flex flex-col items-end">
|
Adjust variables to see how traffic shifts impact your bottom line.
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
</CardDescription>
|
||||||
Simulate Traffic:{" "}
|
</div>
|
||||||
<span className={simulationFactor > 0 ? "text-green-600" : simulationFactor < 0 ? "text-red-600" : ""}>
|
|
||||||
{simulationFactor > 0 ? "+" : ""}
|
<div className="flex items-center gap-4 bg-black/40 p-2.5 rounded-2xl border border-white/5 shadow-2xl backdrop-blur-md">
|
||||||
{simulationFactor}%
|
<div className="flex flex-col items-start min-w-[150px]">
|
||||||
</span>
|
<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>
|
</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
|
<Slider
|
||||||
value={[simulationFactor]}
|
value={[simulationFactor]}
|
||||||
min={-50}
|
min={-50}
|
||||||
@@ -582,37 +616,64 @@ export default function PredictionsChart({
|
|||||||
step={10}
|
step={10}
|
||||||
onValueChange={(val) => setSimulationFactor(val[0])}
|
onValueChange={(val) => setSimulationFactor(val[0])}
|
||||||
onValueCommit={(val) => setCommittedSimulationFactor(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>
|
</div>
|
||||||
{simulationFactor !== 0 && (
|
</div>
|
||||||
|
|
||||||
|
{(simulationFactor !== 0 || committedSimulationFactor !== 0) && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6"
|
className="h-9 w-9 hover:bg-white/10 rounded-xl transition-all"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSimulationFactor(0);
|
setSimulationFactor(0);
|
||||||
setCommittedSimulationFactor(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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
Export
|
Export Forecast
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="pt-8">
|
||||||
<div className="h-80 w-full mt-4 relative">
|
{/* Legend / Key */}
|
||||||
{isSimulating && (
|
<div className="flex items-center gap-8 mb-8 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60">
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-background/50 z-10">
|
<div className="flex items-center gap-3">
|
||||||
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
|
<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>
|
</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
|
<AreaChart
|
||||||
data={chartData}
|
data={chartData}
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
@@ -622,7 +683,7 @@ export default function PredictionsChart({
|
|||||||
<stop
|
<stop
|
||||||
offset="5%"
|
offset="5%"
|
||||||
stopColor="#8884d8"
|
stopColor="#8884d8"
|
||||||
stopOpacity={0.6}
|
stopOpacity={0.3}
|
||||||
/>
|
/>
|
||||||
<stop
|
<stop
|
||||||
offset="95%"
|
offset="95%"
|
||||||
@@ -634,7 +695,7 @@ export default function PredictionsChart({
|
|||||||
<stop
|
<stop
|
||||||
offset="5%"
|
offset="5%"
|
||||||
stopColor="#10b981"
|
stopColor="#10b981"
|
||||||
stopOpacity={0.8}
|
stopOpacity={0.5}
|
||||||
/>
|
/>
|
||||||
<stop
|
<stop
|
||||||
offset="95%"
|
offset="95%"
|
||||||
@@ -643,50 +704,66 @@ export default function PredictionsChart({
|
|||||||
/>
|
/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border) / 0.4)" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="formattedDate"
|
dataKey="formattedDate"
|
||||||
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
|
dy={15}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(value) => `£${value}`}
|
tickFormatter={(value) => `£${value}`}
|
||||||
/>
|
/>
|
||||||
<RechartsTooltip
|
<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 }) => {
|
content={({ active, payload }) => {
|
||||||
if (active && payload?.length) {
|
if (active && payload?.length) {
|
||||||
const data = payload[0].payload;
|
const data = payload[0].payload;
|
||||||
return (
|
return (
|
||||||
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
<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-medium mb-2">{data.formattedDate}</p>
|
<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>
|
||||||
<p className="text-sm text-purple-600">
|
<div className="space-y-3">
|
||||||
Baseline: <span className="font-semibold">{formatGBP(data.baseline)}</span>
|
<div className="flex items-center justify-between gap-10">
|
||||||
</p>
|
<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 && (
|
{committedSimulationFactor !== 0 && (
|
||||||
<p className="text-sm text-green-600">
|
<div className="flex items-center justify-between gap-10">
|
||||||
Simulated: <span className="font-semibold">{formatGBP(data.simulated)}</span>
|
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-wide">Simulated:</span>
|
||||||
</p>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Always show baseline as solid line */}
|
{/* Always show baseline */}
|
||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="baseline"
|
dataKey="baseline"
|
||||||
stroke="#8884d8"
|
stroke="#8884d8"
|
||||||
fillOpacity={committedSimulationFactor !== 0 ? 0.3 : 1}
|
fillOpacity={1}
|
||||||
fill="url(#colorBaseline)"
|
fill="url(#colorBaseline)"
|
||||||
strokeWidth={committedSimulationFactor !== 0 ? 1 : 2}
|
strokeWidth={3}
|
||||||
activeDot={{ r: 4, strokeWidth: 0 }}
|
dot={false}
|
||||||
|
activeDot={{ r: 4, strokeWidth: 0, fill: "#8884d8" }}
|
||||||
/>
|
/>
|
||||||
{/* Show simulated line when simulation is active */}
|
{/* Show simulated line when simulation is active */}
|
||||||
{committedSimulationFactor !== 0 && (
|
{committedSimulationFactor !== 0 && (
|
||||||
@@ -694,11 +771,12 @@ export default function PredictionsChart({
|
|||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="simulated"
|
dataKey="simulated"
|
||||||
stroke="#10b981"
|
stroke="#10b981"
|
||||||
fillOpacity={0.6}
|
fillOpacity={1}
|
||||||
fill="url(#colorSimulated)"
|
fill="url(#colorSimulated)"
|
||||||
|
strokeWidth={3}
|
||||||
strokeDasharray="5 5"
|
strokeDasharray="5 5"
|
||||||
strokeWidth={2}
|
dot={false}
|
||||||
activeDot={{ r: 5, strokeWidth: 0 }}
|
activeDot={{ r: 6, strokeWidth: 3, stroke: "#fff", fill: "#10b981" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
PieChart,
|
PieChart,
|
||||||
Calculator,
|
Calculator,
|
||||||
Info,
|
Info,
|
||||||
AlertTriangle
|
AlertTriangle,
|
||||||
|
Package
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { formatGBP } from "@/utils/format";
|
import { formatGBP } from "@/utils/format";
|
||||||
@@ -28,6 +29,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
|
|||||||
const [data, setData] = useState<ProfitOverview | null>(null);
|
const [data, setData] = useState<ProfitOverview | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const maskValue = (value: string): string => {
|
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>
|
<CardTitle className="text-sm font-medium">Total Profit</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className={`text-2xl font-bold flex items-center gap-2 ${
|
<div className={`text-2xl font-bold flex items-center gap-2 ${profitDirection ? 'text-green-600' : 'text-red-600'
|
||||||
profitDirection ? 'text-green-600' : 'text-red-600'
|
|
||||||
}`}>
|
}`}>
|
||||||
{profitDirection ? (
|
{profitDirection ? (
|
||||||
<TrendingUp className="h-5 w-5" />
|
<TrendingUp className="h-5 w-5" />
|
||||||
@@ -327,15 +328,34 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={product.productId}
|
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 gap-4">
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
|
<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}
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{product.productName}</p>
|
<p className="font-semibold">{product.productName}</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
|
<Package className="h-3 w-3" />
|
||||||
{product.totalQuantitySold} units sold
|
{product.totalQuantitySold} units sold
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default function RevenueChart({ timeRange, hideNumbers = false }: Revenue
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Chart */}
|
{/* Chart */}
|
||||||
<div className="h-64">
|
<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 }}>
|
<AreaChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface ProfitOverview {
|
|||||||
topProfitableProducts: Array<{
|
topProfitableProducts: Array<{
|
||||||
productId: string;
|
productId: string;
|
||||||
productName: string;
|
productName: string;
|
||||||
|
image?: string;
|
||||||
totalQuantitySold: number;
|
totalQuantitySold: number;
|
||||||
totalRevenue: number;
|
totalRevenue: number;
|
||||||
totalCost: number;
|
totalCost: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user