Files
ember-market-frontend/components/modals/profit-analysis-modal.tsx
g 0176f89cb7 Add CSV export for orders and update UI symbols
Introduces an exportOrdersToCSV function in lib/api-client.ts to allow exporting orders by status as a CSV file. Updates various UI components to use the '•' (bullet) symbol instead of '·' (middle dot) and replaces some emoji/unicode characters for improved consistency and compatibility. Also normalizes the 'use client' directive to include a BOM in many files.
2025-12-15 17:57:18 +00: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>
);
};