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:
135
app/globals.css
135
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,23 +121,28 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Improved spacing for Chromebook screens */
|
/* Improved spacing for Chromebook screens */
|
||||||
.space-y-2 > * + * {
|
.space-y-2>*+* {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-y-4 > * + * {
|
.space-y-4>*+* {
|
||||||
margin-top: 1.25rem;
|
margin-top: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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;
|
||||||
@@ -138,7 +150,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Better spacing for touch interactions */
|
/* Better spacing for touch interactions */
|
||||||
.space-y-2 > * + * {
|
.space-y-2>*+* {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
@@ -313,18 +348,18 @@ body {
|
|||||||
/* Subtle Christmas gradient backgrounds */
|
/* Subtle Christmas gradient backgrounds */
|
||||||
.christmas-gradient {
|
.christmas-gradient {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
hsl(var(--christmas-red) / 0.1) 0%,
|
hsl(var(--christmas-red) / 0.1) 0%,
|
||||||
hsl(var(--christmas-green) / 0.1) 50%,
|
hsl(var(--christmas-green) / 0.1) 50%,
|
||||||
hsl(var(--christmas-gold) / 0.1) 100%);
|
hsl(var(--christmas-gold) / 0.1) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Christmas-themed borders */
|
/* Christmas-themed borders */
|
||||||
.christmas-border {
|
.christmas-border {
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-image: linear-gradient(45deg,
|
border-image: linear-gradient(45deg,
|
||||||
hsl(var(--christmas-red)),
|
hsl(var(--christmas-red)),
|
||||||
hsl(var(--christmas-green)),
|
hsl(var(--christmas-green)),
|
||||||
hsl(var(--christmas-gold))) 1;
|
hsl(var(--christmas-gold))) 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Christmas-themed styles - only active in December */
|
/* Christmas-themed styles - only active in December */
|
||||||
@@ -361,8 +396,8 @@ body {
|
|||||||
.christmas-theme button[class*="bg-primary"]:hover,
|
.christmas-theme button[class*="bg-primary"]:hover,
|
||||||
.christmas-theme [class*="bg-primary"]:hover {
|
.christmas-theme [class*="bg-primary"]:hover {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
hsl(var(--christmas-red)),
|
hsl(var(--christmas-red)),
|
||||||
hsl(var(--christmas-green)));
|
hsl(var(--christmas-green)));
|
||||||
transition: background 0.3s ease;
|
transition: background 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ export default function AdminAnalytics() {
|
|||||||
|
|
||||||
// Helper to transform data for recharts
|
// Helper to transform data for recharts
|
||||||
const transformChartData = (
|
const transformChartData = (
|
||||||
data: Array<{ date: string; [key: string]: any }>,
|
data: Array<{ date: string;[key: string]: any }>,
|
||||||
valueKey: string = "count",
|
valueKey: string = "count",
|
||||||
) => {
|
) => {
|
||||||
if (!data || data.length === 0) return [];
|
if (!data || data.length === 0) return [];
|
||||||
@@ -275,10 +275,10 @@ export default function AdminAnalytics() {
|
|||||||
const date =
|
const date =
|
||||||
parts.length === 3
|
parts.length === 3
|
||||||
? new Date(
|
? new Date(
|
||||||
parseInt(parts[0]),
|
parseInt(parts[0]),
|
||||||
parseInt(parts[1]) - 1,
|
parseInt(parts[1]) - 1,
|
||||||
parseInt(parts[2]),
|
parseInt(parts[2]),
|
||||||
)
|
)
|
||||||
: new Date(dateStr);
|
: new Date(dateStr);
|
||||||
|
|
||||||
// Format with day of week: "Mon, Nov 21"
|
// Format with day of week: "Mon, Nov 21"
|
||||||
@@ -329,10 +329,10 @@ export default function AdminAnalytics() {
|
|||||||
const date =
|
const date =
|
||||||
parts.length === 3
|
parts.length === 3
|
||||||
? new Date(
|
? new Date(
|
||||||
parseInt(parts[0]),
|
parseInt(parts[0]),
|
||||||
parseInt(parts[1]) - 1,
|
parseInt(parts[1]) - 1,
|
||||||
parseInt(parts[2]),
|
parseInt(parts[2]),
|
||||||
)
|
)
|
||||||
: new Date(dateStr);
|
: new Date(dateStr);
|
||||||
|
|
||||||
// Format with day of week: "Mon, Nov 21"
|
// Format with day of week: "Mon, Nov 21"
|
||||||
@@ -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={[
|
||||||
@@ -1417,7 +1417,7 @@ export default function AdminAnalytics() {
|
|||||||
const data = payload[0].payload;
|
const data = payload[0].payload;
|
||||||
const details =
|
const details =
|
||||||
growthData.customers.segmentDetails[
|
growthData.customers.segmentDetails[
|
||||||
data.name.split(" ")[0].toLowerCase()
|
data.name.split(" ")[0].toLowerCase()
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
||||||
|
|||||||
@@ -195,211 +195,204 @@ 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>
|
||||||
|
|
||||||
{/* Key Metrics Cards */}
|
<MotionWrapper className="space-y-12">
|
||||||
<MotionWrapper className="space-y-10">
|
{/* Analytics Tabs Setup */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 lg:gap-8">
|
<Tabs defaultValue="overview" className="space-y-10">
|
||||||
{isLoading
|
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-6 pb-2">
|
||||||
? [...Array(4)].map((_, i) => <MetricsCardSkeleton key={i} />)
|
<TabsList className="bg-transparent h-auto p-0 flex flex-wrap gap-2 lg:gap-4">
|
||||||
: metrics.map((metric) => (
|
<TabsTrigger
|
||||||
<MetricsCard key={metric.title} {...metric} />
|
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"
|
||||||
</div>
|
>
|
||||||
|
|
||||||
{/* Completion Rate Card */}
|
|
||||||
<motion.div>
|
|
||||||
<Card className="hover:shadow-xl hover:border-indigo-500/30 transition-all duration-300">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Activity className="h-5 w-5" />
|
|
||||||
Order Completion Rate
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Percentage of orders that have been successfully completed
|
|
||||||
</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>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-3xl font-bold">
|
|
||||||
{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">
|
|
||||||
{hideNumbers
|
|
||||||
? "** / **"
|
|
||||||
: `${data.orders.completed} / ${data.orders.total}`}
|
|
||||||
</Badge>
|
|
||||||
</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" />
|
<Activity className="h-4 w-4" />
|
||||||
Growth
|
Overview
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="revenue" className="flex items-center gap-2">
|
<TabsTrigger
|
||||||
<TrendingUp className="h-4 w-4" />
|
value="financials"
|
||||||
Revenue
|
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>
|
||||||
<TabsTrigger value="profit" className="flex items-center gap-2">
|
<TabsTrigger
|
||||||
<Calculator className="h-4 w-4" />
|
value="performance"
|
||||||
Profit
|
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"
|
||||||
</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" />
|
<BarChart3 className="h-4 w-4" />
|
||||||
Orders
|
Performance
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="predictions" className="flex items-center gap-2">
|
<TabsTrigger
|
||||||
<TrendingUp className="h-4 w-4" />
|
value="ai"
|
||||||
Predictions
|
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>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="growth" className="space-y-6">
|
{/* Contextual Time Range Selector */}
|
||||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
<div className="flex items-center gap-3 bg-muted/30 p-1 rounded-xl border border-border/20">
|
||||||
<Suspense fallback={<ChartSkeleton />}>
|
<span className="text-xs font-semibold text-muted-foreground px-2">Range</span>
|
||||||
<GrowthAnalyticsChart hideNumbers={hideNumbers} />
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
</Suspense>
|
<SelectTrigger className="w-[130px] h-8 border-none bg-transparent shadow-none focus:ring-0">
|
||||||
</motion.div>
|
<SelectValue />
|
||||||
</TabsContent>
|
</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="revenue" className="space-y-6">
|
<TabsContent value="overview" className="space-y-10 outline-none">
|
||||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
<motion.div
|
||||||
<Suspense fallback={<ChartSkeleton />}>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<RevenueChart timeRange={timeRange} hideNumbers={hideNumbers} />
|
animate={{ opacity: 1, y: 0 }}
|
||||||
</Suspense>
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||||
</motion.div>
|
className="space-y-10"
|
||||||
</TabsContent>
|
>
|
||||||
|
{/* Key Metrics Cards */}
|
||||||
|
<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) => (
|
||||||
|
<MetricsCard key={metric.title} {...metric} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<TabsContent value="profit" className="space-y-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
{/* Completion Rate Card */}
|
||||||
{/* Date Range Selector for Profit Calculator */}
|
<Card className="lg:col-span-1 glass-morphism premium-card">
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">Date Range</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Activity className="h-5 w-5 text-emerald-500" />
|
||||||
|
Order Completion
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Select a custom date range for profit calculations
|
Successfully processed orders
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:items-center">
|
{isLoading ? (
|
||||||
<DateRangePicker
|
<Skeleton className="h-24 w-full rounded-2xl" />
|
||||||
dateRange={profitDateRange}
|
) : (
|
||||||
onDateRangeChange={setProfitDateRange}
|
<div className="space-y-6">
|
||||||
placeholder="Select date range"
|
<div className="flex items-center justify-between">
|
||||||
showPresets={true}
|
<div className="text-4xl font-extrabold tracking-tight">
|
||||||
className="w-full sm:w-auto"
|
{hideNumbers ? "**%" : `${data.orders.completionRate}%`}
|
||||||
/>
|
</div>
|
||||||
{profitDateRange?.from && profitDateRange?.to && (
|
<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="text-sm text-muted-foreground flex items-center">
|
{hideNumbers
|
||||||
<span>
|
? "** / **"
|
||||||
{profitDateRange.from.toLocaleDateString()} -{" "}
|
: `${data.orders.completed} / ${data.orders.total}`}
|
||||||
{profitDateRange.to.toLocaleDateString()}
|
</Badge>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="w-full bg-secondary/50 rounded-full h-3 overflow-hidden border border-border/20">
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
|
{/* 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="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>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
<Card className="glass-morphism">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Profit Range</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Custom date selection for analysis
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DateRangePicker
|
||||||
|
dateRange={profitDateRange}
|
||||||
|
onDateRangeChange={setProfitDateRange}
|
||||||
|
placeholder="Select date range"
|
||||||
|
showPresets={true}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</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>
|
||||||
</motion.div>
|
</div>
|
||||||
</TabsContent>
|
</motion.div>
|
||||||
|
</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
|
||||||
<Suspense fallback={<ChartSkeleton />}>
|
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 />
|
<ProductPerformanceChart />
|
||||||
</Suspense>
|
</div>
|
||||||
</motion.div>
|
</Suspense>
|
||||||
</TabsContent>
|
<div className="space-y-8 min-w-0">
|
||||||
|
|
||||||
<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>
|
||||||
</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 }}>
|
|
||||||
<Suspense fallback={<ChartSkeleton />}>
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
<PredictionsChart timeRange={parseInt(timeRange)} />
|
<CustomerInsightsChart />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</motion.div>
|
</div>
|
||||||
</TabsContent>
|
</motion.div>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
</div>
|
|
||||||
|
<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>
|
||||||
</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,42 +25,88 @@ 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">
|
||||||
{getTrendIcon()}
|
{value}
|
||||||
<span className={`text-xs ${getTrendColor()}`}>
|
</div>
|
||||||
{trendValue}
|
<p className="text-[11px] text-white/40">
|
||||||
</span>
|
{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-[10px] font-bold uppercase tracking-wide">
|
||||||
|
{trend === "up" ? "+" : ""}{trendValue}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -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,58 +576,104 @@ 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>
|
<CardTitle className="text-xl font-bold flex items-center gap-2 tracking-tight">
|
||||||
<div className="flex items-center gap-4">
|
<Zap className="h-5 w-5 text-amber-500 fill-amber-500/20" />
|
||||||
<div className="flex flex-col items-end">
|
Scenario Lab
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
</CardTitle>
|
||||||
Simulate Traffic:{" "}
|
<CardDescription className="text-muted-foreground/80 font-medium">
|
||||||
<span className={simulationFactor > 0 ? "text-green-600" : simulationFactor < 0 ? "text-red-600" : ""}>
|
Adjust variables to see how traffic shifts impact your bottom line.
|
||||||
{simulationFactor > 0 ? "+" : ""}
|
</CardDescription>
|
||||||
{simulationFactor}%
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<Slider
|
|
||||||
value={[simulationFactor]}
|
|
||||||
min={-50}
|
|
||||||
max={50}
|
|
||||||
step={10}
|
|
||||||
onValueChange={(val) => setSimulationFactor(val[0])}
|
|
||||||
onValueCommit={(val) => setCommittedSimulationFactor(val[0])}
|
|
||||||
className="w-[150px] mt-1.5"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{simulationFactor !== 0 && (
|
|
||||||
<Button
|
<div className="flex items-center gap-4 bg-black/40 p-2.5 rounded-2xl border border-white/5 shadow-2xl backdrop-blur-md">
|
||||||
variant="ghost"
|
<div className="flex flex-col items-start min-w-[150px]">
|
||||||
size="icon"
|
<div className="flex items-center gap-1.5 mb-1 ml-1">
|
||||||
className="h-6 w-6"
|
<span className="text-[10px] font-bold uppercase tracking-wider text-primary/40">
|
||||||
onClick={() => {
|
Traffic Simulation
|
||||||
setSimulationFactor(0);
|
</span>
|
||||||
setCommittedSimulationFactor(0);
|
<TooltipProvider delayDuration={0}>
|
||||||
}}
|
<Tooltip>
|
||||||
title="Reset simulation"
|
<TooltipTrigger asChild>
|
||||||
>
|
<Info className="h-3 w-3 text-primary/30 cursor-help" />
|
||||||
<RefreshCw className="h-3 w-3" />
|
</TooltipTrigger>
|
||||||
</Button>
|
<TooltipContent side="top" className="max-w-[200px] z-[110] bg-black border-white/10 text-white p-2">
|
||||||
)}
|
<p className="text-[11px] leading-relaxed">
|
||||||
|
Simulate traffic growth or decline to see how it might impact your future revenue and order volume.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 w-full">
|
||||||
|
<Slider
|
||||||
|
value={[simulationFactor]}
|
||||||
|
min={-50}
|
||||||
|
max={50}
|
||||||
|
step={10}
|
||||||
|
onValueChange={(val) => setSimulationFactor(val[0])}
|
||||||
|
onValueCommit={(val) => setCommittedSimulationFactor(val[0])}
|
||||||
|
className="w-full flex-1"
|
||||||
|
/>
|
||||||
|
<Badge variant="outline" className={`ml-2 min-w-[50px] text-center font-bold border-2 ${simulationFactor > 0 ? "text-emerald-400 border-emerald-500/30 bg-emerald-500/10" : simulationFactor < 0 ? "text-rose-400 border-rose-500/30 bg-rose-500/10" : "text-primary/60"}`}>
|
||||||
|
{simulationFactor > 0 ? "+" : ""}{simulationFactor}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(simulationFactor !== 0 || committedSimulationFactor !== 0) && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 hover:bg-white/10 rounded-xl transition-all"
|
||||||
|
onClick={() => {
|
||||||
|
setSimulationFactor(0);
|
||||||
|
setCommittedSimulationFactor(0);
|
||||||
|
}}
|
||||||
|
title="Reset Scenario"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4 text-primary/70" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" size="sm" onClick={handleExportCSV} className="rounded-xl border-white/10 hover:bg-white/5 font-bold px-4">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Export Forecast
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={handleExportCSV} title="Export to CSV">
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
</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>
|
||||||
{committedSimulationFactor !== 0 && (
|
<span className="text-sm font-bold text-[#8884d8] tabular-nums">{formatGBP(data.baseline)}</span>
|
||||||
<p className="text-sm text-green-600">
|
</div>
|
||||||
Simulated: <span className="font-semibold">{formatGBP(data.simulated)}</span>
|
{committedSimulationFactor !== 0 && (
|
||||||
</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>
|
</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,9 +239,8 @@ 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">
|
||||||
{index + 1}
|
<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>
|
||||||
<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