Files
ember-market-frontend/components/modals/profit-analysis-modal.tsx
NotII be746664c5 Add profit analysis modal and cost tracking for products
Introduces a Profit Analysis modal for products, allowing users to view profit, margin, and markup calculations based on cost per unit and pricing tiers. Adds cost per unit input to the product modal, updates product types, and integrates the analysis modal into the products page and product table. This enhances product management with profit tracking and analysis features.
2025-08-26 20:52:38 +01:00

266 lines
9.3 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);
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">
Minimum quantity: {tier.minQuantity}
</p>
</div>
</div>
<div className="text-right space-y-1">
<div className={`font-medium ${getProfitColor(tier.profit)}`}>
Profit: {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>
);
};