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:
@@ -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) {
|
||||
@@ -112,20 +186,31 @@ 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">
|
||||
{/* Summary Cards */}
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface ProfitOverview {
|
||||
period: string;
|
||||
summary: {
|
||||
totalRevenue: number;
|
||||
revenueFromTrackedProducts: number;
|
||||
totalCost: number;
|
||||
totalProfit: number;
|
||||
overallProfitMargin: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"commitHash": "f19ddc4",
|
||||
"buildTime": "2025-08-07T22:45:52.166Z"
|
||||
"commitHash": "6a2cd9a",
|
||||
"buildTime": "2025-08-26T20:09:00.025Z"
|
||||
}
|
||||
Reference in New Issue
Block a user