276 lines
10 KiB
TypeScript
276 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.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>
|
|
);
|
|
};
|