From a05787a0913edf8ed37b88b8a2b3a8870592ebbf Mon Sep 17 00:00:00 2001 From: g Date: Mon, 12 Jan 2026 05:44:54 +0000 Subject: [PATCH] Revamp analytics dashboard UI and charts 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. --- app/globals.css | 221 +++++++---- components/admin/AdminAnalytics.tsx | 24 +- components/analytics/AnalyticsDashboard.tsx | 369 +++++++++--------- components/analytics/GrowthAnalyticsChart.tsx | 2 +- components/analytics/MetricsCard.tsx | 84 +++- components/analytics/PredictionsChart.tsx | 238 +++++++---- components/analytics/ProfitAnalyticsChart.tsx | 62 ++- components/analytics/RevenueChart.tsx | 2 +- lib/services/profit-analytics-service.ts | 9 +- 9 files changed, 613 insertions(+), 398 deletions(-) diff --git a/app/globals.css b/app/globals.css index 3ee6b34..dd54312 100644 --- a/app/globals.css +++ b/app/globals.css @@ -10,17 +10,18 @@ body { .text-balance { text-wrap: balance; } - + /* Shimmer animation for loading indicators */ @keyframes shimmer { 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } } - + /* Accessibility improvements */ .sr-only { position: absolute; @@ -33,164 +34,182 @@ body { white-space: nowrap; border: 0; } - + /* Better focus states for keyboard navigation */ .focus-visible:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; } - + /* Improved touch targets for mobile/Chromebook */ .touch-target { min-height: 44px; min-width: 44px; } - + /* Better contrast for Chromebook displays */ @media (prefers-contrast: high) { .border-input { border-color: hsl(var(--foreground)); } } - + /* Chromebook and touch device optimizations */ @media (pointer: coarse) { .touch-target { min-height: 48px; min-width: 48px; } - + /* Larger touch targets for interactive elements */ - button, input, textarea, [role="button"] { + button, + input, + textarea, + [role="button"] { min-height: 44px; } } - + /* Better focus indicators for keyboard navigation */ .focus-visible:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2); } - + /* Improved scrolling for touch devices */ .overflow-y-auto { -webkit-overflow-scrolling: touch; scroll-behavior: smooth; } - + /* Enhanced contrast for better visibility */ .text-muted-foreground { color: hsl(var(--muted-foreground) / 0.8); } - + /* Better button contrast */ button:not(:disabled):hover { filter: brightness(1.05); } - + /* Improved focus visibility */ - input:focus, textarea:focus, button:focus { + input:focus, + textarea:focus, + button:focus { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; } - + /* Better message bubble contrast */ .bg-primary { background-color: hsl(var(--primary) / 0.9); } - + /* Chromebook-specific optimizations */ @media screen and (max-width: 1366px) and (min-resolution: 1.5dppx) { + /* Chromebook display optimizations */ .text-sm { font-size: 0.875rem; line-height: 1.25rem; } - + .text-base { font-size: 1rem; line-height: 1.5rem; } - + /* Better touch targets for Chromebooks */ - button, input, textarea, [role="button"], [role="tab"] { + button, + input, + textarea, + [role="button"], + [role="tab"] { min-height: 48px; min-width: 48px; } - + /* Improved spacing for Chromebook screens */ - .space-y-2 > * + * { + .space-y-2>*+* { margin-top: 0.75rem; } - - .space-y-4 > * + * { + + .space-y-4>*+* { margin-top: 1.25rem; } } - + /* Chromebook touch screen optimizations */ @media (pointer: coarse) and (hover: none) { + /* Larger touch targets */ .touch-target { min-height: 52px; min-width: 52px; } - + /* Better spacing for touch interactions */ - .space-y-2 > * + * { + .space-y-2>*+* { margin-top: 1rem; } - + /* Improved button padding */ button { padding: 0.75rem 1rem; } - + /* Better input field sizing */ - input, textarea { + input, + textarea { padding: 0.875rem; font-size: 1rem; } - + /* Enhanced focus states for touch */ - button:focus-visible, input:focus-visible, textarea:focus-visible { + button:focus-visible, + input:focus-visible, + textarea:focus-visible { outline: 3px solid hsl(var(--ring)); outline-offset: 2px; } } - + /* Chromebook keyboard navigation improvements */ @media (hover: hover) and (pointer: fine) { + /* Better hover states for mouse/trackpad */ button:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } - + /* Improved focus indicators */ - button:focus-visible, input:focus-visible, textarea:focus-visible { + button:focus-visible, + input:focus-visible, + textarea:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; box-shadow: 0 0 0 4px hsl(var(--ring) / 0.2); } } - + /* Chromebook display scaling fixes */ @media screen and (min-resolution: 1.5dppx) { + /* Prevent text from being too small on high-DPI displays */ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } - + /* Better font rendering */ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } } - + /* Chromebook scrolling improvements */ .overflow-y-auto { -webkit-overflow-scrolling: touch; @@ -198,14 +217,14 @@ body { /* Better momentum scrolling for Chromebooks */ overscroll-behavior: contain; } - + /* Chromebook chat interface optimizations */ .chat-message { /* Better message bubble sizing for touch */ min-height: 44px; padding: 0.75rem; } - + /* Chromebook form optimizations */ .form-input { /* Better input field sizing for Chromebooks */ @@ -213,7 +232,7 @@ body { font-size: 1rem; padding: 0.75rem 1rem; } - + /* Chromebook button optimizations */ .btn-chromebook { min-height: 48px; @@ -222,17 +241,17 @@ body { font-size: 1rem; border-radius: 0.5rem; } - + /* Enhanced keyboard focus indicators for Chromebooks */ .keyboard-focus { outline: 3px solid hsl(var(--ring)); outline-offset: 2px; box-shadow: 0 0 0 4px hsl(var(--ring) / 0.3); } - + /* Better focus management for Chromebook keyboard navigation */ - button:focus-visible, - input:focus-visible, + button:focus-visible, + input:focus-visible, textarea:focus-visible, [role="button"]:focus-visible, [role="tab"]:focus-visible { @@ -240,22 +259,31 @@ body { outline-offset: 2px; box-shadow: 0 0 0 4px hsl(var(--ring) / 0.3); } - + /* Chromebook-specific focus ring */ @media (prefers-reduced-motion: no-preference) { .keyboard-focus { transition: outline 0.2s ease, box-shadow 0.2s ease; } } - + .bg-muted { background-color: hsl(var(--muted) / 0.8); } /* Christmas-themed animations */ @keyframes twinkle { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.3; transform: scale(0.8); } + + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.3; + transform: scale(0.8); + } } @keyframes snowflake { @@ -263,6 +291,7 @@ body { transform: translateY(-100vh) rotate(0deg); opacity: 1; } + 100% { transform: translateY(100vh) rotate(360deg); opacity: 0; @@ -270,20 +299,26 @@ body { } @keyframes sparkle { - 0%, 100% { + + 0%, + 100% { opacity: 0; transform: scale(0) rotate(0deg); } - 50% { + + 50% { opacity: 1; transform: scale(1) rotate(180deg); } } @keyframes glow { - 0%, 100% { + + 0%, + 100% { box-shadow: 0 0 5px hsl(var(--christmas-red)), 0 0 10px hsl(var(--christmas-red)), 0 0 15px hsl(var(--christmas-red)); } + 50% { box-shadow: 0 0 10px hsl(var(--christmas-green)), 0 0 20px hsl(var(--christmas-green)), 0 0 30px hsl(var(--christmas-green)); } @@ -312,19 +347,19 @@ body { /* Subtle Christmas gradient backgrounds */ .christmas-gradient { - background: linear-gradient(135deg, - hsl(var(--christmas-red) / 0.1) 0%, - hsl(var(--christmas-green) / 0.1) 50%, - hsl(var(--christmas-gold) / 0.1) 100%); + background: linear-gradient(135deg, + hsl(var(--christmas-red) / 0.1) 0%, + hsl(var(--christmas-green) / 0.1) 50%, + hsl(var(--christmas-gold) / 0.1) 100%); } /* Christmas-themed borders */ .christmas-border { border: 2px solid; - border-image: linear-gradient(45deg, - hsl(var(--christmas-red)), - hsl(var(--christmas-green)), - hsl(var(--christmas-gold))) 1; + border-image: linear-gradient(45deg, + hsl(var(--christmas-red)), + hsl(var(--christmas-green)), + hsl(var(--christmas-gold))) 1; } /* Christmas-themed styles - only active in December */ @@ -342,7 +377,7 @@ body { left: 0; width: 100%; height: 100%; - background-image: + background-image: radial-gradient(circle at 20% 50%, hsl(var(--christmas-red) / 0.03) 0%, transparent 50%), radial-gradient(circle at 80% 80%, hsl(var(--christmas-green) / 0.03) 0%, transparent 50%), radial-gradient(circle at 40% 20%, hsl(var(--christmas-gold) / 0.03) 0%, transparent 50%); @@ -360,9 +395,9 @@ body { /* Using more specific selector to avoid Turbopack CSS parsing issues */ .christmas-theme button[class*="bg-primary"]:hover, .christmas-theme [class*="bg-primary"]:hover { - background: linear-gradient(135deg, - hsl(var(--christmas-red)), - hsl(var(--christmas-green))); + background: linear-gradient(135deg, + hsl(var(--christmas-red)), + hsl(var(--christmas-green))); transition: background 0.3s ease; } @@ -376,6 +411,42 @@ body { .christmas-theme *:focus-visible { outline-color: hsl(var(--christmas-red)); } + + /* Premium UI Utilities */ + .glass-morphism { + @apply bg-background/60 backdrop-blur-md border border-border/50; + } + + .dark .glass-morphism { + @apply bg-black/40 backdrop-blur-xl border-white/5; + } + + .premium-card { + @apply transition-all duration-300; + } + + .premium-card:hover { + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); + border-color: hsl(var(--primary) / 0.2); + } + + .dark .premium-card { + @apply bg-card; + } + + .dark .premium-card:hover { + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6); + border-color: hsl(var(--primary) / 0.2); + } + + .text-gradient { + @apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/60; + } + + .bg-gradient-premium { + background: radial-gradient(circle at top left, hsl(var(--primary) / 0.05), transparent), + radial-gradient(circle at bottom right, hsl(var(--primary) / 0.02), transparent); + } } @layer base { @@ -410,26 +481,27 @@ body { --christmas-green: 142 76% 36%; --christmas-gold: 43 96% 56%; } + .dark { - --background: 0 0% 3.9%; + --background: 240 10% 2%; --foreground: 0 0% 98%; - --card: 0 0% 3.9%; + --card: 240 10% 3%; --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; + --popover: 240 10% 2%; --popover-foreground: 0 0% 98%; --primary: 0 0% 98%; --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; + --secondary: 240 4% 10%; --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; + --muted: 240 4% 10%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 4% 10%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; + --border: 240 4% 12%; + --input: 240 4% 12%; + --ring: 240 5% 83.1%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; @@ -464,7 +536,8 @@ body { * { @apply border-border; } + body { @apply bg-background text-foreground; } -} +} \ No newline at end of file diff --git a/components/admin/AdminAnalytics.tsx b/components/admin/AdminAnalytics.tsx index 2e2e0a0..a44991f 100644 --- a/components/admin/AdminAnalytics.tsx +++ b/components/admin/AdminAnalytics.tsx @@ -263,7 +263,7 @@ export default function AdminAnalytics() { // Helper to transform data for recharts const transformChartData = ( - data: Array<{ date: string; [key: string]: any }>, + data: Array<{ date: string;[key: string]: any }>, valueKey: string = "count", ) => { if (!data || data.length === 0) return []; @@ -275,10 +275,10 @@ export default function AdminAnalytics() { const date = parts.length === 3 ? new Date( - parseInt(parts[0]), - parseInt(parts[1]) - 1, - parseInt(parts[2]), - ) + parseInt(parts[0]), + parseInt(parts[1]) - 1, + parseInt(parts[2]), + ) : new Date(dateStr); // Format with day of week: "Mon, Nov 21" @@ -329,10 +329,10 @@ export default function AdminAnalytics() { const date = parts.length === 3 ? new Date( - parseInt(parts[0]), - parseInt(parts[1]) - 1, - parseInt(parts[2]), - ) + parseInt(parts[0]), + parseInt(parts[1]) - 1, + parseInt(parts[2]), + ) : new Date(dateStr); // Format with day of week: "Mon, Nov 21" @@ -1365,8 +1365,8 @@ export default function AdminAnalytics() {
) : growthData?.customers ? ( -
- +
+ diff --git a/components/analytics/AnalyticsDashboard.tsx b/components/analytics/AnalyticsDashboard.tsx index 4e37f52..df8f163 100644 --- a/components/analytics/AnalyticsDashboard.tsx +++ b/components/analytics/AnalyticsDashboard.tsx @@ -195,211 +195,204 @@ export default function AnalyticsDashboard({ ]; return ( -
- {/* Header with Privacy Toggle */} -
+
+ {/* Header with Integrated Toolbar */} +

Analytics Dashboard

-

- Overview of your store's performance and metrics. +

+ Real-time performance metrics and AI-driven insights.

-
+ +
+ +
+
- {/* Key Metrics Cards */} - -
- {isLoading - ? [...Array(4)].map((_, i) => ) - : metrics.map((metric) => ( - - ))} -
- - {/* Completion Rate Card */} - - - - - - Order Completion Rate - - - Percentage of orders that have been successfully completed - - - - {isLoading ? ( -
-
-
-
-
-
-
- ) : ( -
-
- {hideNumbers ? "**%" : `${data.orders.completionRate}%`} -
-
-
-
-
-
- - {hideNumbers - ? "** / **" - : `${data.orders.completed} / ${data.orders.total}`} - -
- )} - - - - - {/* Time Period Selector */} -
-
-

Time Period

-

- Revenue, Profit, and Orders tabs use time filtering. Products and - Customers show all-time data. -

-
- -
- - {/* Analytics Tabs */} -
- - - + + {/* Analytics Tabs Setup */} + +
+ + - Growth + Overview - - - Revenue + + + Financials - - - Profit - - - - Products - - - - Customers - - + - Orders + Performance - - - Predictions + + + AI Insights - - - }> - - - - + {/* Contextual Time Range Selector */} +
+ Range + +
+
- - - }> - - - - + + + {/* Key Metrics Cards */} +
+ {isLoading + ? [...Array(4)].map((_, i) => ) + : metrics.map((metric) => ( + + ))} +
- - - {/* Date Range Selector for Profit Calculator */} - +
+ {/* Completion Rate Card */} + - Date Range + + + Order Completion + - Select a custom date range for profit calculations + Successfully processed orders -
- - {profitDateRange?.from && profitDateRange?.to && ( -
- - {profitDateRange.from.toLocaleDateString()} -{" "} - {profitDateRange.to.toLocaleDateString()} - + {isLoading ? ( + + ) : ( +
+
+
+ {hideNumbers ? "**%" : `${data.orders.completionRate}%`} +
+ + {hideNumbers + ? "** / **" + : `${data.orders.completed} / ${data.orders.total}`} +
- )} -
+
+ +
+
+ )} + + {/* Growth Chart Snippet (Simplified) */} +
+ }> + + +
+
+ + + + + + }> +
+ +
+
+ +
+ + + Profit Range + + Custom date selection for analysis + + + + + + + }> - - +
+
+
- - - }> + + + }> +
- - - - - - - }> - - - - - - - +
+
+
}> - - - - - }> - + - - - -
+
+
+
+ + + + }> + + + + +
); diff --git a/components/analytics/GrowthAnalyticsChart.tsx b/components/analytics/GrowthAnalyticsChart.tsx index cde1ba3..52289de 100644 --- a/components/analytics/GrowthAnalyticsChart.tsx +++ b/components/analytics/GrowthAnalyticsChart.tsx @@ -183,7 +183,7 @@ export default function GrowthAnalyticsChart({
) : growthData?.monthly && growthData.monthly.length > 0 ? (
- + ({ ...m, diff --git a/components/analytics/MetricsCard.tsx b/components/analytics/MetricsCard.tsx index 933883a..450870c 100644 --- a/components/analytics/MetricsCard.tsx +++ b/components/analytics/MetricsCard.tsx @@ -25,42 +25,88 @@ export default function MetricsCard({ const getTrendIcon = () => { switch (trend) { case "up": - return ; + return ; case "down": - return ; + return ; default: - return ; + return ; } }; const getTrendColor = () => { switch (trend) { case "up": - return "text-green-600"; + return "text-emerald-400 bg-emerald-500/10 border-emerald-500/10"; case "down": - return "text-red-600"; + return "text-rose-400 bg-rose-500/10 border-rose-500/10"; default: - return "text-gray-600"; + return "text-blue-400 bg-blue-500/10 border-blue-500/10"; } }; + const getCategoryColor = () => { + const t = title.toLowerCase(); + if (t.includes("revenue") || t.includes("profit")) return "amber"; + if (t.includes("order")) return "blue"; + if (t.includes("customer")) return "indigo"; + if (t.includes("product") || t.includes("inventory")) return "purple"; + return "primary"; + } + + const categoryColor = getCategoryColor(); + + const getIconContainerColor = () => { + switch (categoryColor) { + case "amber": return "bg-amber-500/15 text-amber-500 border-amber-500/20"; + case "blue": return "bg-blue-500/15 text-blue-500 border-blue-500/20"; + case "indigo": return "bg-indigo-500/15 text-indigo-500 border-indigo-500/20"; + case "purple": return "bg-purple-500/15 text-purple-500 border-purple-500/20"; + default: return "bg-primary/15 text-primary border-primary/20"; + } + } + + const getBadgeColor = () => { + switch (categoryColor) { + case "amber": return "bg-amber-500/10 text-amber-400/80 border-amber-500/20"; + case "blue": return "bg-blue-500/10 text-blue-400/80 border-blue-500/20"; + case "indigo": return "bg-indigo-500/10 text-indigo-400/80 border-indigo-500/20"; + case "purple": return "bg-purple-500/10 text-purple-400/80 border-purple-500/20"; + default: return "bg-primary/10 text-primary/60 border-primary/20"; + } + } + return ( - - - - + + + + {title} - +
+ +
- -
{value}
-

{description}

-
- {getTrendIcon()} - - {trendValue} - + + +
+
+ {value} +
+

+ {description} +

+
+ +
+
+ {getTrendIcon()} + + {trend === "up" ? "+" : ""}{trendValue} + +
diff --git a/components/analytics/PredictionsChart.tsx b/components/analytics/PredictionsChart.tsx index af37d4a..ad12647 100644 --- a/components/analytics/PredictionsChart.tsx +++ b/components/analytics/PredictionsChart.tsx @@ -164,6 +164,13 @@ export default function PredictionsChart({ setSimulationFactor(0); }, [timeRange]); + // Auto-adjust daysAhead if it exceeds historical timeRange + useEffect(() => { + if (daysAhead > timeRange) { + setDaysAhead(timeRange); + } + }, [timeRange, daysAhead]); + // Switch predictions when daysAhead changes (instant, from batch) useEffect(() => { if (batchData) { @@ -322,10 +329,18 @@ export default function PredictionsChart({ 7 days - 14 days - 30 days - 60 days - 90 days + + 14 days {timeRange < 14 && "(Needs 14d history)"} + + + 30 days {timeRange < 30 && "(Needs 30d history)"} + + + 60 days {timeRange < 60 && "(Needs 60d history)"} + + + 90 days {timeRange < 90 && "(Needs 90d history)"} +
- +

Predicted daily average revenue for the next {daysAhead} days

@@ -409,7 +424,7 @@ export default function PredictionsChart({ - +

Based on data consistency, historical accuracy, and model agreement

@@ -428,7 +443,7 @@ export default function PredictionsChart({ - +

Predictions generated using a Deep Learning Ensemble Model

@@ -461,7 +476,7 @@ export default function PredictionsChart({ - +

Direction of the recent sales trend (slope analysis)

@@ -504,7 +519,7 @@ export default function PredictionsChart({ - +

Technical details about the active prediction model

@@ -521,7 +536,7 @@ export default function PredictionsChart({ Hybrid Ensemble (Deep Learning) - +

Combines LSTM Neural Networks with Statistical Methods (Holt-Winters, ARIMA)

@@ -561,58 +576,104 @@ export default function PredictionsChart({ {/* Daily Predictions Chart */} {predictions?.sales?.dailyPredictions && predictions?.sales?.dailyPredictions.length > 0 && ( - - - - Daily Revenue Forecast - -
-
- - Simulate Traffic:{" "} - 0 ? "text-green-600" : simulationFactor < 0 ? "text-red-600" : ""}> - {simulationFactor > 0 ? "+" : ""} - {simulationFactor}% - - - setSimulationFactor(val[0])} - onValueCommit={(val) => setCommittedSimulationFactor(val[0])} - className="w-[150px] mt-1.5" - /> + + +
+
+ + + Scenario Lab + + + Adjust variables to see how traffic shifts impact your bottom line. +
- {simulationFactor !== 0 && ( - - )} + +
+
+
+ + Traffic Simulation + + + + + + + +

+ Simulate traffic growth or decline to see how it might impact your future revenue and order volume. +

+
+
+
+
+
+ setSimulationFactor(val[0])} + onValueCommit={(val) => setCommittedSimulationFactor(val[0])} + className="w-full flex-1" + /> + 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}% + +
+
+ + {(simulationFactor !== 0 || committedSimulationFactor !== 0) && ( + + )} +
+ +
-
- -
- {isSimulating && ( -
- + + {/* Legend / Key */} +
+
+
+ Baseline Forecast +
+ {committedSimulationFactor !== 0 && ( +
+
+ Simulated Scenario
)} - +
+ +
+ {isSimulating && ( +
+
+
+ + +
+ Running Neural Simulation... +
+
+ )} + - + `£${value}`} /> { if (active && payload?.length) { const data = payload[0].payload; return ( -
-

{data.formattedDate}

-

- Baseline: {formatGBP(data.baseline)} -

- {committedSimulationFactor !== 0 && ( -

- Simulated: {formatGBP(data.simulated)} -

- )} +
+

{data.formattedDate}

+
+
+ Baseline: + {formatGBP(data.baseline)} +
+ {committedSimulationFactor !== 0 && ( +
+ Simulated: +
+ {formatGBP(data.simulated)} + data.baseline ? 'text-emerald-500' : 'text-rose-500'}`}> + {data.simulated > data.baseline ? '▴' : '▾'} {Math.abs(((data.simulated / data.baseline - 1) * 100)).toFixed(1)}% + +
+
+ )} +
+ Est. Orders: + + {Math.round(data.orders)} +
+
); } return null; }} /> - {/* Always show baseline as solid line */} + {/* Always show baseline */} {/* Show simulated line when simulation is active */} {committedSimulationFactor !== 0 && ( @@ -694,11 +771,12 @@ export default function PredictionsChart({ type="monotone" dataKey="simulated" stroke="#10b981" - fillOpacity={0.6} + fillOpacity={1} fill="url(#colorSimulated)" + strokeWidth={3} strokeDasharray="5 5" - strokeWidth={2} - activeDot={{ r: 5, strokeWidth: 0 }} + dot={false} + activeDot={{ r: 6, strokeWidth: 3, stroke: "#fff", fill: "#10b981" }} /> )} diff --git a/components/analytics/ProfitAnalyticsChart.tsx b/components/analytics/ProfitAnalyticsChart.tsx index 9331d57..e2f3978 100644 --- a/components/analytics/ProfitAnalyticsChart.tsx +++ b/components/analytics/ProfitAnalyticsChart.tsx @@ -4,14 +4,15 @@ import { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { - TrendingUp, - TrendingDown, - DollarSign, +import { + TrendingUp, + TrendingDown, + DollarSign, PieChart, Calculator, Info, - AlertTriangle + AlertTriangle, + Package } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { formatGBP } from "@/utils/format"; @@ -28,6 +29,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [imageErrors, setImageErrors] = useState>({}); const { toast } = useToast(); const maskValue = (value: string): string => { @@ -93,7 +95,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers ))}
- + {/* Coverage Card Skeleton */} @@ -110,7 +112,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
- + {/* Products List Skeleton */} @@ -188,7 +190,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers } const profitDirection = data.summary.totalProfit >= 0; - + // Fallback for backwards compatibility const revenueFromTracked = data.summary.revenueFromTrackedProducts || data.summary.totalRevenue || 0; const totalRevenue = data.summary.totalRevenue || 0; @@ -237,9 +239,8 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers Total Profit -
+
{profitDirection ? ( ) : ( @@ -286,7 +287,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
-
@@ -307,7 +308,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers Most Profitable Products - {dateRange + {dateRange ? `Products generating the highest total profit (${new Date(dateRange.from).toLocaleDateString()} - ${new Date(dateRange.to).toLocaleDateString()})` : `Products generating the highest total profit (last ${timeRange || '30'} days)` } @@ -323,24 +324,43 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
{data.topProfitableProducts.map((product, index) => { const profitPositive = product.totalProfit >= 0; - + return (
-
-
- {index + 1} +
+
+
+ {product.image && !imageErrors[product.productId] ? ( + {product.productName} { + setImageErrors(prev => ({ ...prev, [product.productId]: true })); + }} + /> + ) : ( +
+ {product.productName.charAt(0)} +
+ )} +
+
+ {index + 1} +
-

{product.productName}

-

+

{product.productName}

+

+ {product.totalQuantitySold} units sold

- +
{maskValue(formatGBP(product.totalProfit))} diff --git a/components/analytics/RevenueChart.tsx b/components/analytics/RevenueChart.tsx index c941b82..7c27504 100644 --- a/components/analytics/RevenueChart.tsx +++ b/components/analytics/RevenueChart.tsx @@ -175,7 +175,7 @@ export default function RevenueChart({ timeRange, hideNumbers = false }: Revenue
{/* Chart */}
- + diff --git a/lib/services/profit-analytics-service.ts b/lib/services/profit-analytics-service.ts index b9c16d3..1028055 100644 --- a/lib/services/profit-analytics-service.ts +++ b/lib/services/profit-analytics-service.ts @@ -18,6 +18,7 @@ export interface ProfitOverview { topProfitableProducts: Array<{ productId: string; productName: string; + image?: string; totalQuantitySold: number; totalRevenue: number; totalCost: number; @@ -51,7 +52,7 @@ export const getProfitOverview = async ( periodOrRange?: string | DateRange ): Promise => { let url = '/analytics/profit-overview'; - + if (periodOrRange && typeof periodOrRange !== 'string') { // Date range provided const startDate = periodOrRange.from.toISOString().split('T')[0]; @@ -62,7 +63,7 @@ export const getProfitOverview = async ( const period = periodOrRange || '30'; url += `?period=${period}`; } - + return apiRequest(url); }; @@ -70,7 +71,7 @@ export const getProfitTrends = async ( periodOrRange?: string | DateRange ): Promise => { let url = '/analytics/profit-trends'; - + if (periodOrRange && typeof periodOrRange !== 'string') { // Date range provided const startDate = periodOrRange.from.toISOString().split('T')[0]; @@ -81,6 +82,6 @@ export const getProfitTrends = async ( const period = periodOrRange || '30'; url += `?period=${period}`; } - + return apiRequest(url); };