Revamp analytics dashboard UI and charts
All checks were successful
Build Frontend / build (push) Successful in 1m11s

Enhanced the AnalyticsDashboard layout with a premium glassmorphism UI, improved toolbar, and reorganized tabs for better clarity. MetricsCard now features dynamic color coding and trend badges. PredictionsChart received scenario simulation UI upgrades, disabled future ranges based on available history, and improved chart tooltips and visuals. ProfitAnalyticsChart added error handling for product images and minor UI refinements. Updated globals.css with new premium utility classes and improved dark mode color variables.
This commit is contained in:
g
2026-01-12 05:44:54 +00:00
parent a0605e47de
commit a05787a091
9 changed files with 613 additions and 398 deletions

View File

@@ -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<ProfitOverview | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
const { toast } = useToast();
const maskValue = (value: string): string => {
@@ -93,7 +95,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
</Card>
))}
</div>
{/* Coverage Card Skeleton */}
<Card>
<CardHeader>
@@ -110,7 +112,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
</div>
</CardContent>
</Card>
{/* Products List Skeleton */}
<Card>
<CardHeader>
@@ -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
<CardTitle className="text-sm font-medium">Total Profit</CardTitle>
</CardHeader>
<CardContent>
<div className={`text-2xl font-bold flex items-center gap-2 ${
profitDirection ? 'text-green-600' : 'text-red-600'
}`}>
<div className={`text-2xl font-bold flex items-center gap-2 ${profitDirection ? 'text-green-600' : 'text-red-600'
}`}>
{profitDirection ? (
<TrendingUp className="h-5 w-5" />
) : (
@@ -286,7 +287,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
</div>
<div className="flex-1">
<div className="w-full bg-secondary rounded-full h-2">
<div
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: hideNumbers ? "0%" : `${data.summary.costDataCoverage || 0}%` }}
/>
@@ -307,7 +308,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
Most Profitable Products
</CardTitle>
<CardDescription>
{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
<div className="space-y-4">
{data.topProfitableProducts.map((product, index) => {
const profitPositive = product.totalProfit >= 0;
return (
<div
key={product.productId}
className="flex items-center justify-between p-4 border rounded-lg"
className="flex items-center justify-between p-4 border rounded-lg transition-colors hover:bg-muted/30"
>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
{index + 1}
<div className="flex items-center gap-4">
<div className="relative flex-shrink-0">
<div className="w-12 h-12 rounded-full overflow-hidden border-2 border-background shadow-sm bg-muted flex items-center justify-center">
{product.image && !imageErrors[product.productId] ? (
<img
src={`/api/products/${product.productId}/image`}
alt={product.productName}
className="w-full h-full object-cover"
onError={() => {
setImageErrors(prev => ({ ...prev, [product.productId]: true }));
}}
/>
) : (
<div className="flex items-center justify-center w-full h-full bg-primary/10 text-primary font-bold text-lg">
{product.productName.charAt(0)}
</div>
)}
</div>
<div className="absolute -top-1 -left-1 w-5 h-5 bg-primary text-[10px] text-primary-foreground flex items-center justify-center rounded-full font-bold border-2 border-background shadow-sm">
{index + 1}
</div>
</div>
<div>
<p className="font-medium">{product.productName}</p>
<p className="text-sm text-muted-foreground">
<p className="font-semibold">{product.productName}</p>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<Package className="h-3 w-3" />
{product.totalQuantitySold} units sold
</p>
</div>
</div>
<div className="text-right">
<div className={`font-medium ${profitPositive ? 'text-green-600' : 'text-red-600'}`}>
{maskValue(formatGBP(product.totalProfit))}