Enhance customer and profit analysis dialogs UI/UX
All checks were successful
Build Frontend / build (push) Successful in 1m14s

Revamps the customer details dialog with improved layout, animations, and clearer stats breakdown. Upgrades the profit analysis modal with animated cards, clearer tier breakdown, and improved cost/margin/profit explanations. Also increases recent activity fetch limit, fixes quote hydration in dashboard content, and minor animation tweak in order table.
This commit is contained in:
g
2026-01-12 08:12:36 +00:00
parent 7ddcd7afb6
commit e369741b2d
5 changed files with 349 additions and 231 deletions

View File

@@ -5,9 +5,10 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
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 { TrendingUp, TrendingDown, Calculator, DollarSign, Loader2, Info } from "lucide-react";
import { toast } from "sonner";
import { apiRequest } from "@/lib/api";
import { motion, AnimatePresence } from "framer-motion";
interface ProfitAnalysisModalProps {
open: boolean;
@@ -69,7 +70,11 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
const formatCurrency = (amount: number | null) => {
if (amount === null) return "N/A";
return `£${amount.toFixed(2)}`;
return new Intl.NumberFormat('en-GB', {
style: 'currency',
currency: 'GBP',
minimumFractionDigits: 2
}).format(amount);
};
const formatPercentage = (percentage: number | null) => {
@@ -79,7 +84,7 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
const getProfitColor = (profit: number | null) => {
if (profit === null) return "text-muted-foreground";
return profit >= 0 ? "text-green-600" : "text-red-600";
return profit >= 0 ? "text-emerald-500" : "text-rose-500";
};
const getProfitIcon = (profit: number | null) => {
@@ -87,17 +92,33 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
return profit >= 0 ? TrendingUp : TrendingDown;
};
// Variants for staggered animations
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
if (loading) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<DialogContent className="max-w-4xl bg-background/80 backdrop-blur-md border-border/50">
<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>
<Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-2" />
<p className="text-muted-foreground">Calculating metrics...</p>
</div>
</div>
</DialogContent>
@@ -108,7 +129,7 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
if (!profitData) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<DialogContent className="max-w-4xl bg-background/80 backdrop-blur-md border-border/50">
<DialogHeader>
<DialogTitle>Profit Analysis - {productName}</DialogTitle>
</DialogHeader>
@@ -122,154 +143,194 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto bg-black/80 backdrop-blur-xl border-white/10 shadow-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5" />
Profit Analysis - {productName}
<DialogTitle className="flex items-center gap-2 text-xl">
<div className="p-2 rounded-lg bg-indigo-500/10 border border-indigo-500/20">
<DollarSign className="h-5 w-5 text-indigo-400" />
</div>
<span>Profit Analysis: <span className="text-muted-foreground font-normal">{productName}</span></span>
</DialogTitle>
</DialogHeader>
<div className="space-y-6">
<motion.div
className="space-y-6 py-4"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* 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>
<motion.div variants={itemVariants}>
<Card className="bg-emerald-500/5 border-emerald-500/20 backdrop-blur-sm hover:bg-emerald-500/10 transition-colors">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-emerald-400">Average Profit</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-emerald-500">
{formatCurrency(profitData.summary.averageProfit)}
</div>
<p className="text-xs text-emerald-400/60 mt-1">Per unit sold</p>
</CardContent>
</Card>
</motion.div>
<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>
<motion.div variants={itemVariants}>
<Card className="bg-blue-500/5 border-blue-500/20 backdrop-blur-sm hover:bg-blue-500/10 transition-colors">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-blue-400">Avg. Margin</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-500">
{formatPercentage(profitData.summary.averageProfitMargin)}
</div>
<p className="text-xs text-blue-400/60 mt-1">Of selling price</p>
</CardContent>
</Card>
</motion.div>
<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>
<motion.div variants={itemVariants}>
<Card className="bg-indigo-500/5 border-indigo-500/20 backdrop-blur-sm hover:bg-indigo-500/10 transition-colors">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-indigo-400">Avg. Markup</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-indigo-500">
{formatPercentage(profitData.summary.averageMarkup)}
</div>
<p className="text-xs text-indigo-400/60 mt-1">On cost price</p>
</CardContent>
</Card>
</motion.div>
</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>
<motion.div variants={itemVariants}>
<Card className="border-dashed border-2 border-muted bg-muted/20">
<CardContent className="pt-6">
<div className="text-center py-6">
<Calculator className="h-12 w-12 text-muted-foreground mx-auto mb-4 opacity-50" />
<h3 className="text-lg font-medium mb-2">Missing Cost Data</h3>
<p className="text-muted-foreground mb-4 max-w-sm mx-auto">
Add a generic "Cost Per Unit" to this product to see detailed profit calculations.
</p>
<Badge variant="outline" className="text-sm py-1 px-3">
Current Cost: {formatCurrency(profitData.costPerUnit)}
</Badge>
</div>
</CardContent>
</Card>
</motion.div>
)}
{/* 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>
<motion.div variants={itemVariants}>
<Card className="bg-white/5 border-white/10 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-md bg-muted/50">
<Info className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-sm font-medium text-muted-foreground">Base Cost Per Unit</span>
</div>
<span className="text-xl font-bold text-white">{formatCurrency(profitData.costPerUnit)}</span>
</div>
</CardContent>
</Card>
</motion.div>
{/* 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) => {
<motion.div variants={itemVariants} className="space-y-3">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider pl-1">Tier Breakdown</h3>
<div className="space-y-3">
{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
<motion.div
key={index}
className="flex items-center justify-between p-4 border rounded-lg"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 + (index * 0.1) }}
className="relative overflow-hidden group rounded-xl border border-white/5 bg-white/5 hover:bg-white/10 transition-all duration-300"
>
<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 className={`absolute left-0 top-0 bottom-0 w-1 ${tier.profit && tier.profit >= 0 ? 'bg-emerald-500' : 'bg-rose-500'}`} />
<div className="flex flex-col sm:flex-row sm:items-center justify-between p-4 pl-6 gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant="secondary" className="font-mono text-xs">
{tier.minQuantity}+ UNITS
</Badge>
<span className="text-muted-foreground text-sm">at</span>
<span className="font-bold text-white text-lg">{formatCurrency(tier.pricePerUnit)}</span>
</div>
<div className="flex gap-4 text-xs text-muted-foreground mt-2">
<span>Rev: <span className="text-white">{formatCurrency(totalRevenueForMinQty)}</span></span>
<span className="w-px h-4 bg-white/10" />
<span>Cost: <span className="text-white">{formatCurrency(totalCostForMinQty)}</span></span>
</div>
</div>
<div className="flex items-center justify-between sm:justify-end gap-6 sm:w-auto w-full pt-2 sm:pt-0 border-t sm:border-0 border-white/5">
<div className="text-right">
<div className="text-[10px] uppercase text-muted-foreground font-medium mb-0.5">Margin</div>
<div className={`font-mono font-bold ${tier.profit && tier.profit >= 50 ? 'text-emerald-400' : 'text-blue-400'}`}>
{formatPercentage(tier.profitMargin)}
</div>
</div>
<div className="text-right pl-4 border-l border-white/10">
<div className="text-[10px] uppercase text-muted-foreground font-medium mb-0.5">Net Profit</div>
<div className={`text-xl font-bold flex items-center justify-end gap-1 ${getProfitColor(tier.profit)}`}>
{tier.profit && tier.profit > 0 ? '+' : ''}{formatCurrency(tier.profit)}
</div>
<div className={`text-[10px] ${getProfitColor(totalProfitForMinQty)} opacity-80`}>
Total: {formatCurrency(totalProfitForMinQty)}
</div>
</div>
</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>
</motion.div>
);
})}
</div>
</CardContent>
</Card>
</div>
</motion.div>
{/* 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>
<motion.div variants={itemVariants}>
<div className="bg-indigo-500/5 rounded-lg border border-indigo-500/10 p-4">
<h4 className="flex items-center gap-2 text-sm font-medium text-indigo-300 mb-2">
<Info className="h-4 w-4" />
Quick Guide
</h4>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs text-muted-foreground">
<div>
<span className="text-indigo-200 font-semibold block mb-0.5">Profit</span>
Selling Price - Cost Price
</div>
<div>
<span className="text-indigo-200 font-semibold block mb-0.5">Margin</span>
(Profit / Selling Price) × 100
</div>
<div>
<span className="text-indigo-200 font-semibold block mb-0.5">Markup</span>
(Profit / Cost Price) × 100
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</motion.div>
</motion.div>
<div className="flex justify-end pt-4">
<Button onClick={onClose}>Close</Button>
<div className="flex justify-end pt-2">
<Button onClick={onClose} variant="secondary" className="hover:bg-white/20">Close Analysis</Button>
</div>
</DialogContent>
</Dialog>