Improve profit analytics chart and modal data handling

Enhanced ProfitAnalyticsChart to display more robust skeleton loaders and improved summary card logic with fallbacks for missing data. Updated the chart to distinguish between tracked and total revenue, and improved cost data coverage display. In the profit analysis modal, profit margin tiers are now sorted by minimum quantity. Updated the ProfitOverview interface to include revenueFromTrackedProducts.
This commit is contained in:
NotII
2025-08-26 21:36:08 +01:00
parent 6a2cd9aa79
commit 50d0100056
4 changed files with 104 additions and 16 deletions

View File

@@ -16,7 +16,7 @@ import {
import { useToast } from "@/hooks/use-toast";
import { formatGBP } from "@/utils/format";
import { getProfitOverview, type ProfitOverview } from "@/lib/services/profit-analytics-service";
import { TableSkeleton } from './SkeletonLoaders';
import { Skeleton } from "@/components/ui/skeleton";
interface ProfitAnalyticsChartProps {
timeRange: string;
@@ -63,7 +63,81 @@ export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }:
}, [timeRange, toast]);
if (isLoading) {
return <TableSkeleton />;
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calculator className="h-5 w-5" />
Profit Analytics
</CardTitle>
<CardDescription>
Track your actual profits based on sales and cost data
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Summary Cards Skeleton */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-20 mb-2" />
<Skeleton className="h-3 w-16" />
</CardContent>
</Card>
))}
</div>
{/* Coverage Card Skeleton */}
<Card>
<CardHeader>
<Skeleton className="h-6 w-40" />
<Skeleton className="h-4 w-60" />
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<Skeleton className="h-8 w-16" />
<div className="flex-1">
<Skeleton className="h-2 w-full rounded-full" />
</div>
<Skeleton className="h-6 w-16" />
</div>
</CardContent>
</Card>
{/* Products List Skeleton */}
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-64" />
</CardHeader>
<CardContent>
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded-full" />
<div>
<Skeleton className="h-4 w-32 mb-2" />
<Skeleton className="h-3 w-24" />
</div>
</div>
<div className="text-right">
<Skeleton className="h-4 w-20 mb-2" />
<Skeleton className="h-3 w-16" />
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
);
}
if (error || !data) {
@@ -111,6 +185,14 @@ export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }:
}
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;
const totalCost = data.summary.totalCost || 0;
const totalProfit = data.summary.totalProfit || 0;
const productsWithCostData = data.summary.productsWithCostData || 0;
const totalProductsSold = data.summary.totalProductsSold || 0;
return (
<div className="space-y-6">
@@ -118,14 +200,17 @@ export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<CardTitle className="text-sm font-medium">Revenue (Tracked)</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{maskValue(formatGBP(data.summary.totalRevenue))}
{maskValue(formatGBP(revenueFromTracked))}
</div>
<p className="text-xs text-muted-foreground">
From {data.summary.totalProductsSold} items sold
From {productsWithCostData} tracked items
</p>
<p className="text-xs text-muted-foreground mt-1">
Total revenue: {maskValue(formatGBP(totalRevenue))}
</p>
</CardContent>
</Card>
@@ -136,10 +221,10 @@ export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }:
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{maskValue(formatGBP(data.summary.totalCost))}
{maskValue(formatGBP(totalCost))}
</div>
<p className="text-xs text-muted-foreground">
From {data.summary.productsWithCostData} tracked items
From {productsWithCostData} tracked items
</p>
</CardContent>
</Card>
@@ -157,10 +242,10 @@ export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }:
) : (
<TrendingDown className="h-5 w-5" />
)}
{maskValue(formatGBP(data.summary.totalProfit))}
{maskValue(formatGBP(totalProfit))}
</div>
<p className="text-xs text-muted-foreground">
{maskValue(`${data.summary.overallProfitMargin.toFixed(1)}%`)} margin
{maskValue(`${(data.summary.overallProfitMargin || 0).toFixed(1)}%`)} margin
</p>
</CardContent>
</Card>
@@ -171,7 +256,7 @@ export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }:
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">
{maskValue(formatGBP(data.summary.averageProfitPerUnit))}
{maskValue(formatGBP(data.summary.averageProfitPerUnit || 0))}
</div>
<p className="text-xs text-muted-foreground">
Per unit sold
@@ -194,18 +279,18 @@ export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }:
<CardContent>
<div className="flex items-center gap-4">
<div className="text-3xl font-bold">
{hideNumbers ? "**%" : `${data.summary.costDataCoverage.toFixed(1)}%`}
{hideNumbers ? "**%" : `${(data.summary.costDataCoverage || 0).toFixed(1)}%`}
</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.summary.costDataCoverage}%` }}
style={{ width: hideNumbers ? "0%" : `${data.summary.costDataCoverage || 0}%` }}
/>
</div>
</div>
<Badge variant="secondary">
{hideNumbers ? "** / **" : `${data.summary.productsWithCostData} / ${data.summary.totalProductsSold}`}
{hideNumbers ? "** / **" : `${productsWithCostData} / ${totalProductsSold}`}
</Badge>
</div>
</CardContent>

View File

@@ -205,7 +205,9 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
</CardHeader>
<CardContent>
<div className="space-y-4">
{profitData.profitMargins.map((tier, index) => {
{profitData.profitMargins
.sort((a, b) => a.minQuantity - b.minQuantity)
.map((tier, index) => {
const ProfitIcon = getProfitIcon(tier.profit);
const totalProfitForMinQty = tier.profit !== null ? tier.profit * tier.minQuantity : null;

View File

@@ -4,6 +4,7 @@ export interface ProfitOverview {
period: string;
summary: {
totalRevenue: number;
revenueFromTrackedProducts: number;
totalCost: number;
totalProfit: number;
overallProfitMargin: number;

View File

@@ -1,4 +1,4 @@
{
"commitHash": "f19ddc4",
"buildTime": "2025-08-07T22:45:52.166Z"
"commitHash": "6a2cd9a",
"buildTime": "2025-08-26T20:09:00.025Z"
}