Enhance customer and profit analysis dialogs UI/UX
All checks were successful
Build Frontend / build (push) Successful in 1m14s
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:
@@ -43,7 +43,9 @@ import {
|
||||
X,
|
||||
CreditCard,
|
||||
Calendar,
|
||||
ShoppingBag
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
CheckCircle,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -469,40 +471,65 @@ export default function CustomerManagementPage() {
|
||||
</Card>
|
||||
|
||||
{/* Customer Details Dialog */}
|
||||
<AnimatePresence>
|
||||
{selectedCustomer && (
|
||||
<Dialog open={!!selectedCustomer} onOpenChange={(open) => !open && setSelectedCustomer(null)}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-2xl w-full overflow-y-auto max-h-[90vh] z-[80]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">
|
||||
Customer Details
|
||||
<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">
|
||||
<DialogHeader className="p-6 pb-2 border-b border-white/5">
|
||||
<DialogTitle className="text-xl flex items-center gap-3">
|
||||
<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">
|
||||
{selectedCustomer.telegramUsername ? selectedCustomer.telegramUsername.substring(0, 1).toUpperCase() : 'U'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold">Customer Details</div>
|
||||
<div className="text-sm font-normal text-muted-foreground flex items-center gap-2">
|
||||
@{selectedCustomer.telegramUsername || "Unknown"}
|
||||
<span className="w-1 h-1 rounded-full bg-indigo-500" />
|
||||
<span className="font-mono text-xs opacity-70">ID: {selectedCustomer.telegramUserId}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 py-4">
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Customer Information */}
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium mb-2">Customer Information</h3>
|
||||
<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">
|
||||
<div className="text-muted-foreground">Username:</div>
|
||||
<div className="font-medium">@{selectedCustomer.telegramUsername || "Unknown"}</div>
|
||||
<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="flex justify-between items-center text-sm">
|
||||
<div className="text-muted-foreground">Telegram ID:</div>
|
||||
<div className="font-medium">{selectedCustomer.telegramUserId}</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">
|
||||
<div className="text-muted-foreground">Chat ID:</div>
|
||||
<div className="font-medium">{selectedCustomer.chatId}</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"
|
||||
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');
|
||||
}}
|
||||
@@ -511,75 +538,99 @@ export default function CustomerManagementPage() {
|
||||
Open Telegram Chat
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Order Statistics */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">Order Statistics</h3>
|
||||
<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>
|
||||
<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="flex justify-between items-center text-sm">
|
||||
<div className="text-muted-foreground">Total Spent:</div>
|
||||
<div className="font-medium">{formatCurrency(selectedCustomer.totalSpent)}</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">First Order:</div>
|
||||
<div className="font-medium">
|
||||
<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">Last Order:</div>
|
||||
<div className="font-medium">
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
<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/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 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/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 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-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 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>
|
||||
<DialogFooter className="p-6 pt-2 border-t border-white/5 bg-white/[0.02]">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
onClick={() => setSelectedCustomer(null)}
|
||||
className="hover:bg-white/5 text-muted-foreground hover:text-white"
|
||||
>
|
||||
Close
|
||||
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" />
|
||||
Start Chat
|
||||
Message Customer
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,13 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
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 () => {
|
||||
try {
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function RecentActivity() {
|
||||
useEffect(() => {
|
||||
async function fetchRecentOrders() {
|
||||
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 || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch recent activity:", error);
|
||||
|
||||
@@ -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,89 +143,107 @@ 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>
|
||||
<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">Average Profit</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-emerald-400">Average Profit</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
<div className="text-3xl font-bold text-emerald-500">
|
||||
{formatCurrency(profitData.summary.averageProfit)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Per unit sold</p>
|
||||
<p className="text-xs text-emerald-400/60 mt-1">Per unit sold</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<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">Average Profit Margin</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-blue-400">Avg. Margin</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
<div className="text-3xl font-bold text-blue-500">
|
||||
{formatPercentage(profitData.summary.averageProfitMargin)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Of selling price</p>
|
||||
<p className="text-xs text-blue-400/60 mt-1">Of selling price</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<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">Average Markup</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-indigo-400">Avg. Markup</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
<div className="text-3xl font-bold text-indigo-500">
|
||||
{formatPercentage(profitData.summary.averageMarkup)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">On cost price</p>
|
||||
<p className="text-xs text-indigo-400/60 mt-1">On cost price</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card className="border-dashed border-2 border-muted bg-muted/20">
|
||||
<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.
|
||||
<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">Cost Per Unit: {formatCurrency(profitData.costPerUnit)}</Badge>
|
||||
<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>
|
||||
<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">
|
||||
<span className="text-sm font-medium">Cost Per Unit:</span>
|
||||
<span className="text-lg font-semibold">{formatCurrency(profitData.costPerUnit)}</span>
|
||||
<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">
|
||||
<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) => {
|
||||
@@ -215,61 +254,83 @@ export const ProfitAnalysisModal: React.FC<ProfitAnalysisModalProps> = ({
|
||||
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="text-right space-y-1">
|
||||
<div className={`font-medium ${getProfitColor(totalProfitForMinQty)}`}>
|
||||
Total Profit: {formatCurrency(totalProfitForMinQty)}
|
||||
<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 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 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>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@@ -503,7 +503,7 @@ export default function OrderTable() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<AnimatePresence mode="popLayout">
|
||||
<AnimatePresence>
|
||||
{paginatedOrders.map((order, index) => {
|
||||
const StatusIcon = statusConfig[order.status as keyof typeof statusConfig]?.icon || XCircle;
|
||||
const underpaidInfo = getUnderpaidInfo(order);
|
||||
|
||||
Reference in New Issue
Block a user