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

@@ -43,7 +43,9 @@ import {
X, X,
CreditCard, CreditCard,
Calendar, Calendar,
ShoppingBag ShoppingBag,
Truck,
CheckCircle,
} from "lucide-react"; } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -469,117 +471,166 @@ export default function CustomerManagementPage() {
</Card> </Card>
{/* Customer Details Dialog */} {/* Customer Details Dialog */}
{selectedCustomer && ( <AnimatePresence>
<Dialog open={!!selectedCustomer} onOpenChange={(open) => !open && setSelectedCustomer(null)}> {selectedCustomer && (
<DialogContent className="max-w-[95vw] sm:max-w-2xl w-full overflow-y-auto max-h-[90vh] z-[80]"> <Dialog open={!!selectedCustomer} onOpenChange={(open) => !open && setSelectedCustomer(null)}>
<DialogHeader> <DialogContent className="max-w-[95vw] sm:max-w-2xl w-full overflow-y-auto max-h-[90vh] z-[80] bg-black/80 backdrop-blur-xl border-white/10 shadow-2xl p-0 gap-0">
<DialogTitle className="text-base"> <DialogHeader className="p-6 pb-2 border-b border-white/5">
Customer Details <DialogTitle className="text-xl flex items-center gap-3">
</DialogTitle> <div className="h-10 w-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-lg shadow-indigo-500/20">
</DialogHeader> {selectedCustomer.telegramUsername ? selectedCustomer.telegramUsername.substring(0, 1).toUpperCase() : 'U'}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 py-4"> <div>
{/* Customer Information */} <div className="font-bold">Customer Details</div>
<div> <div className="text-sm font-normal text-muted-foreground flex items-center gap-2">
<div className="mb-4"> @{selectedCustomer.telegramUsername || "Unknown"}
<h3 className="text-sm font-medium mb-2">Customer Information</h3> <span className="w-1 h-1 rounded-full bg-indigo-500" />
<div className="space-y-3"> <span className="font-mono text-xs opacity-70">ID: {selectedCustomer.telegramUserId}</span>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Username:</div>
<div className="font-medium">@{selectedCustomer.telegramUsername || "Unknown"}</div>
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Telegram ID:</div>
<div className="font-medium">{selectedCustomer.telegramUserId}</div>
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Chat ID:</div>
<div className="font-medium">{selectedCustomer.chatId}</div>
</div> </div>
</div> </div>
</DialogTitle>
</DialogHeader>
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Customer Information */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="space-y-4"
>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">Contact Info</h3>
<div className="rounded-xl border border-white/5 bg-white/5 p-4 space-y-4 backdrop-blur-sm">
<div className="space-y-3">
<div className="flex justify-between items-center text-sm group">
<div className="text-muted-foreground flex items-center gap-2">
<Users className="h-4 w-4 opacity-50" />
Username
</div>
<div className="font-medium text-white group-hover:text-primary transition-colors">@{selectedCustomer.telegramUsername || "Unknown"}</div>
</div>
<div className="flex justify-between items-center text-sm group">
<div className="text-muted-foreground flex items-center gap-2">
<CreditCard className="h-4 w-4 opacity-50" />
User ID
</div>
<div className="font-medium font-mono text-white/80">{selectedCustomer.telegramUserId}</div>
</div>
<div className="flex justify-between items-center text-sm group">
<div className="text-muted-foreground flex items-center gap-2">
<MessageCircle className="h-4 w-4 opacity-50" />
Chat ID
</div>
<div className="font-medium font-mono text-white/80">{selectedCustomer.chatId}</div>
</div>
</div>
<Button
variant="outline"
size="sm"
className="w-full border-indigo-500/20 hover:bg-indigo-500/10 hover:text-indigo-400 text-indigo-300 transition-colors"
onClick={() => {
window.open(`https://t.me/${selectedCustomer.telegramUsername || selectedCustomer.telegramUserId}`, '_blank');
}}
>
<MessageCircle className="h-4 w-4 mr-2" />
Open Telegram Chat
</Button>
</div>
</motion.div>
{/* Order Statistics */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="space-y-4"
>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">Lifetime Stats</h3>
<div className="rounded-xl border border-white/5 bg-gradient-to-br from-white/5 to-white/[0.02] p-4 space-y-4 backdrop-blur-sm">
<div className="grid grid-cols-2 gap-4">
<div className="bg-emerald-500/10 rounded-lg p-3 border border-emerald-500/20">
<div className="text-xs text-emerald-400/70 uppercase font-medium mb-1">Total Spent</div>
<div className="text-xl font-bold text-emerald-400">{formatCurrency(selectedCustomer.totalSpent)}</div>
</div>
<div className="bg-blue-500/10 rounded-lg p-3 border border-blue-500/20">
<div className="text-xs text-blue-400/70 uppercase font-medium mb-1">Total Orders</div>
<div className="text-xl font-bold text-blue-400">{selectedCustomer.totalOrders}</div>
</div>
</div>
<div className="space-y-2 pt-2 border-t border-white/5">
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground text-xs">First Order</div>
<div className="font-medium text-white/70 text-xs bg-white/5 px-2 py-1 rounded">
{formatDate(selectedCustomer.firstOrderDate)}
</div>
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground text-xs">Last Activity</div>
<div className="font-medium text-white/70 text-xs bg-white/5 px-2 py-1 rounded">
{formatDate(selectedCustomer.lastOrderDate)}
</div>
</div>
</div>
</div>
</motion.div>
</div> </div>
{/* Order Status Breakdown */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="space-y-4"
>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">Order History Breakdown</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="bg-blue-500/5 hover:bg-blue-500/10 transition-colors rounded-xl border border-blue-500/20 p-4 text-center group">
<ShoppingBag className="h-5 w-5 text-blue-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.paid}</p>
<p className="text-xs font-medium text-blue-400/70 uppercase">Paid</p>
</div>
<div className="bg-purple-500/5 hover:bg-purple-500/10 transition-colors rounded-xl border border-purple-500/20 p-4 text-center group">
<Loader2 className="h-5 w-5 text-purple-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.acknowledged}</p>
<p className="text-xs font-medium text-purple-400/70 uppercase">Processing</p>
</div>
<div className="bg-amber-500/5 hover:bg-amber-500/10 transition-colors rounded-xl border border-amber-500/20 p-4 text-center group">
<Truck className="h-5 w-5 text-amber-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.shipped}</p>
<p className="text-xs font-medium text-amber-400/70 uppercase">Shipped</p>
</div>
<div className="bg-emerald-500/5 hover:bg-emerald-500/10 transition-colors rounded-xl border border-emerald-500/20 p-4 text-center group">
<CheckCircle className="h-5 w-5 text-emerald-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.completed}</p>
<p className="text-xs font-medium text-emerald-400/70 uppercase">Completed</p>
</div>
</div>
</motion.div>
</div>
<DialogFooter className="p-6 pt-2 border-t border-white/5 bg-white/[0.02]">
<Button <Button
variant="outline" variant="ghost"
size="sm" onClick={() => setSelectedCustomer(null)}
className="w-full" className="hover:bg-white/5 text-muted-foreground hover:text-white"
onClick={() => { >
window.open(`https://t.me/${selectedCustomer.telegramUsername || selectedCustomer.telegramUserId}`, '_blank'); Close Profile
}} </Button>
<Button
onClick={() => router.push(`/dashboard/storefront/chat/new?userId=${selectedCustomer.telegramUserId}`)}
className="bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white shadow-lg shadow-indigo-500/25 border-0"
> >
<MessageCircle className="h-4 w-4 mr-2" /> <MessageCircle className="h-4 w-4 mr-2" />
Open Telegram Chat Message Customer
</Button> </Button>
</div> </DialogFooter>
</DialogContent>
{/* Order Statistics */} </Dialog>
<div> )}
<h3 className="text-sm font-medium mb-2">Order Statistics</h3> </AnimatePresence>
<div className="bg-background rounded-lg border border-border p-4 space-y-3">
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Total Orders:</div>
<div className="font-medium">{selectedCustomer.totalOrders}</div>
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Total Spent:</div>
<div className="font-medium">{formatCurrency(selectedCustomer.totalSpent)}</div>
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">First Order:</div>
<div className="font-medium">
{formatDate(selectedCustomer.firstOrderDate)}
</div>
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Last Order:</div>
<div className="font-medium">
{formatDate(selectedCustomer.lastOrderDate)}
</div>
</div>
</div>
</div>
</div>
{/* Order Status Breakdown */}
<div className="mb-4">
<h3 className="text-sm font-medium mb-2">Order Status Breakdown</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="bg-blue-500/10 rounded-lg border border-blue-500/20 p-3">
<p className="text-sm text-muted-foreground">Paid</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.paid}</p>
</div>
<div className="bg-purple-500/10 rounded-lg border border-purple-500/20 p-3">
<p className="text-sm text-muted-foreground">Acknowledged</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.acknowledged}</p>
</div>
<div className="bg-amber-500/10 rounded-lg border border-amber-500/20 p-3">
<p className="text-sm text-muted-foreground">Shipped</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.shipped}</p>
</div>
<div className="bg-green-500/10 rounded-lg border border-green-500/20 p-3">
<p className="text-sm text-muted-foreground">Completed</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.completed}</p>
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setSelectedCustomer(null)}
>
Close
</Button>
<Button
onClick={() => router.push(`/dashboard/storefront/chat/new?userId=${selectedCustomer.telegramUserId}`)}
>
<MessageCircle className="h-4 w-4 mr-2" />
Start Chat
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div> </div>
</Layout> </Layout>
); );

View File

@@ -38,7 +38,13 @@ export default function Content({ username, orderStats }: ContentProps) {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { toast } = useToast(); const { toast } = useToast();
const [randomQuote, setRandomQuote] = useState(getRandomQuote()); // Initialize with a default quote to match server-side rendering, then randomize on client
const [randomQuote, setRandomQuote] = useState({ text: "Loading wisdom...", author: "..." });
useEffect(() => {
// Determine quote on client-side to avoid hydration mismatch
setRandomQuote(getRandomQuote());
}, []);
const fetchTopProducts = async () => { const fetchTopProducts = async () => {
try { try {

View File

@@ -25,7 +25,7 @@ export default function RecentActivity() {
useEffect(() => { useEffect(() => {
async function fetchRecentOrders() { async function fetchRecentOrders() {
try { try {
const data = await clientFetch("/orders?limit=5&sortBy=orderDate&sortOrder=desc"); const data = await clientFetch("/orders?limit=10&sortBy=orderDate&sortOrder=desc");
setActivities(data.orders || []); setActivities(data.orders || []);
} catch (error) { } catch (error) {
console.error("Failed to fetch recent activity:", error); console.error("Failed to fetch recent activity:", error);

View File

@@ -5,9 +5,10 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; 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 { toast } from "sonner";
import { apiRequest } from "@/lib/api"; import { apiRequest } from "@/lib/api";
import { motion, AnimatePresence } from "framer-motion";
interface ProfitAnalysisModalProps { interface ProfitAnalysisModalProps {
open: boolean; open: boolean;
@@ -69,7 +70,11 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
const formatCurrency = (amount: number | null) => { const formatCurrency = (amount: number | null) => {
if (amount === null) return "N/A"; 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) => { const formatPercentage = (percentage: number | null) => {
@@ -79,7 +84,7 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
const getProfitColor = (profit: number | null) => { const getProfitColor = (profit: number | null) => {
if (profit === null) return "text-muted-foreground"; 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) => { const getProfitIcon = (profit: number | null) => {
@@ -87,17 +92,33 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
return profit >= 0 ? TrendingUp : TrendingDown; 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) { if (loading) {
return ( return (
<Dialog open={open} onOpenChange={onClose}> <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> <DialogHeader>
<DialogTitle>Profit Analysis - {productName}</DialogTitle> <DialogTitle>Profit Analysis - {productName}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div> <Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-2" />
<p className="text-muted-foreground">Loading profit analysis...</p> <p className="text-muted-foreground">Calculating metrics...</p>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
@@ -108,7 +129,7 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
if (!profitData) { if (!profitData) {
return ( return (
<Dialog open={open} onOpenChange={onClose}> <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> <DialogHeader>
<DialogTitle>Profit Analysis - {productName}</DialogTitle> <DialogTitle>Profit Analysis - {productName}</DialogTitle>
</DialogHeader> </DialogHeader>
@@ -122,154 +143,194 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
return ( return (
<Dialog open={open} onOpenChange={onClose}> <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> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2 text-xl">
<DollarSign className="h-5 w-5" /> <div className="p-2 rounded-lg bg-indigo-500/10 border border-indigo-500/20">
Profit Analysis - {productName} <DollarSign className="h-5 w-5 text-indigo-400" />
</div>
<span>Profit Analysis: <span className="text-muted-foreground font-normal">{productName}</span></span>
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-6"> <motion.div
className="space-y-6 py-4"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* Summary Cards */} {/* Summary Cards */}
{profitData.summary.hasCostData ? ( {profitData.summary.hasCostData ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card> <motion.div variants={itemVariants}>
<CardHeader className="pb-2"> <Card className="bg-emerald-500/5 border-emerald-500/20 backdrop-blur-sm hover:bg-emerald-500/10 transition-colors">
<CardTitle className="text-sm font-medium">Average Profit</CardTitle> <CardHeader className="pb-2">
</CardHeader> <CardTitle className="text-sm font-medium text-emerald-400">Average Profit</CardTitle>
<CardContent> </CardHeader>
<div className="text-2xl font-bold text-green-600"> <CardContent>
{formatCurrency(profitData.summary.averageProfit)} <div className="text-3xl font-bold text-emerald-500">
</div> {formatCurrency(profitData.summary.averageProfit)}
<p className="text-xs text-muted-foreground">Per unit sold</p> </div>
</CardContent> <p className="text-xs text-emerald-400/60 mt-1">Per unit sold</p>
</Card> </CardContent>
</Card>
</motion.div>
<Card> <motion.div variants={itemVariants}>
<CardHeader className="pb-2"> <Card className="bg-blue-500/5 border-blue-500/20 backdrop-blur-sm hover:bg-blue-500/10 transition-colors">
<CardTitle className="text-sm font-medium">Average Profit Margin</CardTitle> <CardHeader className="pb-2">
</CardHeader> <CardTitle className="text-sm font-medium text-blue-400">Avg. Margin</CardTitle>
<CardContent> </CardHeader>
<div className="text-2xl font-bold text-blue-600"> <CardContent>
{formatPercentage(profitData.summary.averageProfitMargin)} <div className="text-3xl font-bold text-blue-500">
</div> {formatPercentage(profitData.summary.averageProfitMargin)}
<p className="text-xs text-muted-foreground">Of selling price</p> </div>
</CardContent> <p className="text-xs text-blue-400/60 mt-1">Of selling price</p>
</Card> </CardContent>
</Card>
</motion.div>
<Card> <motion.div variants={itemVariants}>
<CardHeader className="pb-2"> <Card className="bg-indigo-500/5 border-indigo-500/20 backdrop-blur-sm hover:bg-indigo-500/10 transition-colors">
<CardTitle className="text-sm font-medium">Average Markup</CardTitle> <CardHeader className="pb-2">
</CardHeader> <CardTitle className="text-sm font-medium text-indigo-400">Avg. Markup</CardTitle>
<CardContent> </CardHeader>
<div className="text-2xl font-bold text-purple-600"> <CardContent>
{formatPercentage(profitData.summary.averageMarkup)} <div className="text-3xl font-bold text-indigo-500">
</div> {formatPercentage(profitData.summary.averageMarkup)}
<p className="text-xs text-muted-foreground">On cost price</p> </div>
</CardContent> <p className="text-xs text-indigo-400/60 mt-1">On cost price</p>
</Card> </CardContent>
</Card>
</motion.div>
</div> </div>
) : ( ) : (
<Card> <motion.div variants={itemVariants}>
<CardContent className="pt-6"> <Card className="border-dashed border-2 border-muted bg-muted/20">
<div className="text-center"> <CardContent className="pt-6">
<Calculator className="h-12 w-12 text-muted-foreground mx-auto mb-4" /> <div className="text-center py-6">
<h3 className="text-lg font-medium mb-2">No Cost Data Available</h3> <Calculator className="h-12 w-12 text-muted-foreground mx-auto mb-4 opacity-50" />
<p className="text-muted-foreground mb-4"> <h3 className="text-lg font-medium mb-2">Missing Cost Data</h3>
Add a cost per unit to this product to see profit calculations. <p className="text-muted-foreground mb-4 max-w-sm mx-auto">
</p> Add a generic "Cost Per Unit" to this product to see detailed profit calculations.
<Badge variant="outline">Cost Per Unit: {formatCurrency(profitData.costPerUnit)}</Badge> </p>
</div> <Badge variant="outline" className="text-sm py-1 px-3">
</CardContent> Current Cost: {formatCurrency(profitData.costPerUnit)}
</Card> </Badge>
</div>
</CardContent>
</Card>
</motion.div>
)} )}
{/* Cost Information */} {/* Cost Information */}
<Card> <motion.div variants={itemVariants}>
<CardHeader> <Card className="bg-white/5 border-white/10 backdrop-blur-sm">
<CardTitle className="text-base">Cost Information</CardTitle> <CardContent className="p-4">
</CardHeader> <div className="flex items-center justify-between">
<CardContent> <div className="flex items-center gap-3">
<div className="flex items-center justify-between"> <div className="p-2 rounded-md bg-muted/50">
<span className="text-sm font-medium">Cost Per Unit:</span> <Info className="h-4 w-4 text-muted-foreground" />
<span className="text-lg font-semibold">{formatCurrency(profitData.costPerUnit)}</span> </div>
</div> <span className="text-sm font-medium text-muted-foreground">Base Cost Per Unit</span>
</CardContent> </div>
</Card> <span className="text-xl font-bold text-white">{formatCurrency(profitData.costPerUnit)}</span>
</div>
</CardContent>
</Card>
</motion.div>
{/* Pricing Tier Analysis */} {/* Pricing Tier Analysis */}
<Card> <motion.div variants={itemVariants} className="space-y-3">
<CardHeader> <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider pl-1">Tier Breakdown</h3>
<CardTitle className="text-base">Pricing Tier Analysis</CardTitle> <div className="space-y-3">
</CardHeader> {profitData.profitMargins
<CardContent> .sort((a, b) => a.minQuantity - b.minQuantity)
<div className="space-y-4"> .map((tier, index) => {
{profitData.profitMargins
.sort((a, b) => a.minQuantity - b.minQuantity)
.map((tier, index) => {
const ProfitIcon = getProfitIcon(tier.profit); const ProfitIcon = getProfitIcon(tier.profit);
const totalProfitForMinQty = tier.profit !== null ? tier.profit * tier.minQuantity : null; const totalProfitForMinQty = tier.profit !== null ? tier.profit * tier.minQuantity : null;
const totalRevenueForMinQty = tier.pricePerUnit * tier.minQuantity; const totalRevenueForMinQty = tier.pricePerUnit * tier.minQuantity;
const totalCostForMinQty = profitData.costPerUnit * tier.minQuantity; const totalCostForMinQty = profitData.costPerUnit * tier.minQuantity;
return ( return (
<div <motion.div
key={index} 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"> <div className={`absolute left-0 top-0 bottom-0 w-1 ${tier.profit && tier.profit >= 0 ? 'bg-emerald-500' : 'bg-rose-500'}`} />
<ProfitIcon className={`h-5 w-5 ${getProfitColor(tier.profit)}`} />
<div> <div className="flex flex-col sm:flex-row sm:items-center justify-between p-4 pl-6 gap-4">
<p className="font-medium"> <div className="space-y-1">
{tier.minQuantity}+ units @ {formatCurrency(tier.pricePerUnit)} <div className="flex items-center gap-2">
</p> <Badge variant="secondary" className="font-mono text-xs">
<p className="text-sm text-muted-foreground"> {tier.minQuantity}+ UNITS
Revenue for {tier.minQuantity} units: {formatCurrency(totalRevenueForMinQty)} </Badge>
</p> <span className="text-muted-foreground text-sm">at</span>
<p className="text-sm text-muted-foreground"> <span className="font-bold text-white text-lg">{formatCurrency(tier.pricePerUnit)}</span>
Cost for {tier.minQuantity} units: {formatCurrency(totalCostForMinQty)} </div>
</p> <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> </div>
</motion.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> </div>
</CardContent> </motion.div>
</Card>
{/* Help Text */} {/* Help Text */}
<Card className="bg-muted/50"> <motion.div variants={itemVariants}>
<CardContent className="pt-6"> <div className="bg-indigo-500/5 rounded-lg border border-indigo-500/10 p-4">
<div className="space-y-2 text-sm"> <h4 className="flex items-center gap-2 text-sm font-medium text-indigo-300 mb-2">
<h4 className="font-medium">Understanding the Metrics:</h4> <Info className="h-4 w-4" />
<ul className="space-y-1 text-muted-foreground"> Quick Guide
<li><strong>Profit:</strong> Selling price minus cost price</li> </h4>
<li><strong>Profit Margin:</strong> Profit as a percentage of selling price</li> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs text-muted-foreground">
<li><strong>Markup:</strong> Profit as a percentage of cost price</li> <div>
</ul> <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> </div>
</CardContent> </div>
</Card> </motion.div>
</div> </motion.div>
<div className="flex justify-end pt-4"> <div className="flex justify-end pt-2">
<Button onClick={onClose}>Close</Button> <Button onClick={onClose} variant="secondary" className="hover:bg-white/20">Close Analysis</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -503,7 +503,7 @@ export default function OrderTable() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<AnimatePresence mode="popLayout"> <AnimatePresence>
{paginatedOrders.map((order, index) => { {paginatedOrders.map((order, index) => {
const StatusIcon = statusConfig[order.status as keyof typeof statusConfig]?.icon || XCircle; const StatusIcon = statusConfig[order.status as keyof typeof statusConfig]?.icon || XCircle;
const underpaidInfo = getUnderpaidInfo(order); const underpaidInfo = getUnderpaidInfo(order);