Files
ember-market-frontend/components/modals/profit-analysis-modal.tsx
NotII 50d0100056 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.
2025-08-26 21:36:08 +01:00

278 lines
10 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { TrendingUp, TrendingDown, Calculator, DollarSign } from "lucide-react";
import { toast } from "sonner";
import { apiRequest } from "@/lib/api";
interface ProfitAnalysisModalProps {
open: boolean;
onClose: () => void;
productId: string;
productName: string;
}
interface ProfitData {
productId: string;
name: string;
costPerUnit: number;
pricing: Array<{
minQuantity: number;
pricePerUnit: number;
}>;
profitMargins: Array<{
minQuantity: number;
pricePerUnit: number;
profit: number | null;
profitMargin: number | null;
markup: number | null;
}>;
summary: {
hasCostData: boolean;
averageProfit: number | null;
averageProfitMargin: number | null;
averageMarkup: number | null;
};
}
export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
open,
onClose,
productId,
productName,
}) => {
const [profitData, setProfitData] = useState<ProfitData | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (open && productId) {
fetchProfitAnalysis();
}
}, [open, productId]);
const fetchProfitAnalysis = async () => {
try {
setLoading(true);
const response = await apiRequest(`/products/${productId}/profit-analysis`);
setProfitData(response);
} catch (error) {
console.error("Error fetching profit analysis:", error);
toast.error("Failed to load profit analysis");
} finally {
setLoading(false);
}
};
const formatCurrency = (amount: number | null) => {
if (amount === null) return "N/A";
return `£${amount.toFixed(2)}`;
};
const formatPercentage = (percentage: number | null) => {
if (percentage === null) return "N/A";
return `${percentage.toFixed(1)}%`;
};
const getProfitColor = (profit: number | null) => {
if (profit === null) return "text-muted-foreground";
return profit >= 0 ? "text-green-600" : "text-red-600";
};
const getProfitIcon = (profit: number | null) => {
if (profit === null) return Calculator;
return profit >= 0 ? TrendingUp : TrendingDown;
};
if (loading) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Profit Analysis - {productName}</DialogTitle>
</DialogHeader>
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
<p className="text-muted-foreground">Loading profit analysis...</p>
</div>
</div>
</DialogContent>
</Dialog>
);
}
if (!profitData) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Profit Analysis - {productName}</DialogTitle>
</DialogHeader>
<div className="text-center py-8">
<p className="text-muted-foreground">No profit data available</p>
</div>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5" />
Profit Analysis - {productName}
</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* Summary Cards */}
{profitData.summary.hasCostData ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Average Profit</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{formatCurrency(profitData.summary.averageProfit)}
</div>
<p className="text-xs text-muted-foreground">Per unit sold</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Average Profit Margin</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">
{formatPercentage(profitData.summary.averageProfitMargin)}
</div>
<p className="text-xs text-muted-foreground">Of selling price</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Average Markup</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">
{formatPercentage(profitData.summary.averageMarkup)}
</div>
<p className="text-xs text-muted-foreground">On cost price</p>
</CardContent>
</Card>
</div>
) : (
<Card>
<CardContent className="pt-6">
<div className="text-center">
<Calculator className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">No Cost Data Available</h3>
<p className="text-muted-foreground mb-4">
Add a cost per unit to this product to see profit calculations.
</p>
<Badge variant="outline">Cost Per Unit: {formatCurrency(profitData.costPerUnit)}</Badge>
</div>
</CardContent>
</Card>
)}
{/* Cost Information */}
<Card>
<CardHeader>
<CardTitle className="text-base">Cost Information</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Cost Per Unit:</span>
<span className="text-lg font-semibold">{formatCurrency(profitData.costPerUnit)}</span>
</div>
</CardContent>
</Card>
{/* Pricing Tier Analysis */}
<Card>
<CardHeader>
<CardTitle className="text-base">Pricing Tier Analysis</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{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;
const totalRevenueForMinQty = tier.pricePerUnit * tier.minQuantity;
const totalCostForMinQty = profitData.costPerUnit * tier.minQuantity;
return (
<div
key={index}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-3">
<ProfitIcon className={`h-5 w-5 ${getProfitColor(tier.profit)}`} />
<div>
<p className="font-medium">
{tier.minQuantity}+ units @ {formatCurrency(tier.pricePerUnit)}
</p>
<p className="text-sm text-muted-foreground">
Revenue for {tier.minQuantity} units: {formatCurrency(totalRevenueForMinQty)}
</p>
<p className="text-sm text-muted-foreground">
Cost for {tier.minQuantity} units: {formatCurrency(totalCostForMinQty)}
</p>
</div>
</div>
<div className="text-right space-y-1">
<div className={`font-medium ${getProfitColor(totalProfitForMinQty)}`}>
Total Profit: {formatCurrency(totalProfitForMinQty)}
</div>
<div className="text-sm text-muted-foreground">
Per unit: {formatCurrency(tier.profit)}
</div>
<div className="text-sm text-muted-foreground">
Margin: {formatPercentage(tier.profitMargin)} |
Markup: {formatPercentage(tier.markup)}
</div>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
{/* Help Text */}
<Card className="bg-muted/50">
<CardContent className="pt-6">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Understanding the Metrics:</h4>
<ul className="space-y-1 text-muted-foreground">
<li><strong>Profit:</strong> Selling price minus cost price</li>
<li><strong>Profit Margin:</strong> Profit as a percentage of selling price</li>
<li><strong>Markup:</strong> Profit as a percentage of cost price</li>
</ul>
</div>
</CardContent>
</Card>
</div>
<div className="flex justify-end pt-4">
<Button onClick={onClose}>Close</Button>
</div>
</DialogContent>
</Dialog>
);
};