diff --git a/app/dashboard/storefront/customers/page.tsx b/app/dashboard/storefront/customers/page.tsx index f4099c3..df19760 100644 --- a/app/dashboard/storefront/customers/page.tsx +++ b/app/dashboard/storefront/customers/page.tsx @@ -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,117 +471,166 @@ export default function CustomerManagementPage() { {/* Customer Details Dialog */} - {selectedCustomer && ( - !open && setSelectedCustomer(null)}> - - - - Customer Details - - - -
- {/* Customer Information */} -
-
-

Customer Information

-
-
-
Username:
-
@{selectedCustomer.telegramUsername || "Unknown"}
-
-
-
Telegram ID:
-
{selectedCustomer.telegramUserId}
-
-
-
Chat ID:
-
{selectedCustomer.chatId}
+ + {selectedCustomer && ( + !open && setSelectedCustomer(null)}> + + + +
+ {selectedCustomer.telegramUsername ? selectedCustomer.telegramUsername.substring(0, 1).toUpperCase() : 'U'} +
+
+
Customer Details
+
+ @{selectedCustomer.telegramUsername || "Unknown"} + + ID: {selectedCustomer.telegramUserId}
+
+
+ +
+
+ {/* Customer Information */} + +

Contact Info

+
+
+
+
+ + Username +
+
@{selectedCustomer.telegramUsername || "Unknown"}
+
+
+
+ + User ID +
+
{selectedCustomer.telegramUserId}
+
+
+
+ + Chat ID +
+
{selectedCustomer.chatId}
+
+
+ + +
+
+ + {/* Order Statistics */} + +

Lifetime Stats

+
+
+
+
Total Spent
+
{formatCurrency(selectedCustomer.totalSpent)}
+
+
+
Total Orders
+
{selectedCustomer.totalOrders}
+
+
+ +
+
+
First Order
+
+ {formatDate(selectedCustomer.firstOrderDate)} +
+
+
+
Last Activity
+
+ {formatDate(selectedCustomer.lastOrderDate)} +
+
+
+
+
+ {/* Order Status Breakdown */} + +

Order History Breakdown

+
+
+ +

{selectedCustomer.ordersByStatus.paid}

+

Paid

+
+
+ +

{selectedCustomer.ordersByStatus.acknowledged}

+

Processing

+
+
+ +

{selectedCustomer.ordersByStatus.shipped}

+

Shipped

+
+
+ +

{selectedCustomer.ordersByStatus.completed}

+

Completed

+
+
+
+
+ + + -
- - {/* Order Statistics */} -
-

Order Statistics

-
-
-
Total Orders:
-
{selectedCustomer.totalOrders}
-
-
-
Total Spent:
-
{formatCurrency(selectedCustomer.totalSpent)}
-
-
-
First Order:
-
- {formatDate(selectedCustomer.firstOrderDate)} -
-
-
-
Last Order:
-
- {formatDate(selectedCustomer.lastOrderDate)} -
-
-
-
-
- - {/* Order Status Breakdown */} -
-

Order Status Breakdown

-
-
-

Paid

-

{selectedCustomer.ordersByStatus.paid}

-
-
-

Acknowledged

-

{selectedCustomer.ordersByStatus.acknowledged}

-
-
-

Shipped

-

{selectedCustomer.ordersByStatus.shipped}

-
-
-

Completed

-

{selectedCustomer.ordersByStatus.completed}

-
-
-
- - - - - - -
- )} + + + + )} + ); diff --git a/components/dashboard/content.tsx b/components/dashboard/content.tsx index 50ca07b..e8933d7 100644 --- a/components/dashboard/content.tsx +++ b/components/dashboard/content.tsx @@ -38,7 +38,13 @@ export default function Content({ username, orderStats }: ContentProps) { const [error, setError] = useState(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 { diff --git a/components/dashboard/recent-activity.tsx b/components/dashboard/recent-activity.tsx index 83e7e52..a4273cb 100644 --- a/components/dashboard/recent-activity.tsx +++ b/components/dashboard/recent-activity.tsx @@ -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); diff --git a/components/modals/profit-analysis-modal.tsx b/components/modals/profit-analysis-modal.tsx index 83e04fd..d82e1fd 100644 --- a/components/modals/profit-analysis-modal.tsx +++ b/components/modals/profit-analysis-modal.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 ( - + Profit Analysis - {productName}
-
-

Loading profit analysis...

+ +

Calculating metrics...

@@ -108,7 +129,7 @@ export const ProfitAnalysisModal: React.FC = ({ if (!profitData) { return ( - + Profit Analysis - {productName} @@ -122,154 +143,194 @@ export const ProfitAnalysisModal: React.FC = ({ return ( - + - - - Profit Analysis - {productName} + +
+ +
+ Profit Analysis: {productName}
-
+ {/* Summary Cards */} {profitData.summary.hasCostData ? (
- - - Average Profit - - -
- {formatCurrency(profitData.summary.averageProfit)} -
-

Per unit sold

-
-
+ + + + Average Profit + + +
+ {formatCurrency(profitData.summary.averageProfit)} +
+

Per unit sold

+
+
+
- - - Average Profit Margin - - -
- {formatPercentage(profitData.summary.averageProfitMargin)} -
-

Of selling price

-
-
+ + + + Avg. Margin + + +
+ {formatPercentage(profitData.summary.averageProfitMargin)} +
+

Of selling price

+
+
+
- - - Average Markup - - -
- {formatPercentage(profitData.summary.averageMarkup)} -
-

On cost price

-
-
+ + + + Avg. Markup + + +
+ {formatPercentage(profitData.summary.averageMarkup)} +
+

On cost price

+
+
+
) : ( - - -
- -

No Cost Data Available

-

- Add a cost per unit to this product to see profit calculations. -

- Cost Per Unit: {formatCurrency(profitData.costPerUnit)} -
-
-
+ + + +
+ +

Missing Cost Data

+

+ Add a generic "Cost Per Unit" to this product to see detailed profit calculations. +

+ + Current Cost: {formatCurrency(profitData.costPerUnit)} + +
+
+
+
)} {/* Cost Information */} - - - Cost Information - - -
- Cost Per Unit: - {formatCurrency(profitData.costPerUnit)} -
-
-
+ + + +
+
+
+ +
+ Base Cost Per Unit +
+ {formatCurrency(profitData.costPerUnit)} +
+
+
+
{/* Pricing Tier Analysis */} - - - Pricing Tier Analysis - - -
- {profitData.profitMargins - .sort((a, b) => a.minQuantity - b.minQuantity) - .map((tier, index) => { + +

Tier Breakdown

+
+ {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 ( -
-
- -
-

- {tier.minQuantity}+ units @ {formatCurrency(tier.pricePerUnit)} -

-

- Revenue for {tier.minQuantity} units: {formatCurrency(totalRevenueForMinQty)} -

-

- Cost for {tier.minQuantity} units: {formatCurrency(totalCostForMinQty)} -

+
= 0 ? 'bg-emerald-500' : 'bg-rose-500'}`} /> + +
+
+
+ + {tier.minQuantity}+ UNITS + + at + {formatCurrency(tier.pricePerUnit)} +
+
+ Rev: {formatCurrency(totalRevenueForMinQty)} + + Cost: {formatCurrency(totalCostForMinQty)} +
+
+ +
+
+
Margin
+
= 50 ? 'text-emerald-400' : 'text-blue-400'}`}> + {formatPercentage(tier.profitMargin)} +
+
+ +
+
Net Profit
+
+ {tier.profit && tier.profit > 0 ? '+' : ''}{formatCurrency(tier.profit)} +
+
+ Total: {formatCurrency(totalProfitForMinQty)} +
+
- -
-
- Total Profit: {formatCurrency(totalProfitForMinQty)} -
-
- Per unit: {formatCurrency(tier.profit)} -
-
- Margin: {formatPercentage(tier.profitMargin)} | - Markup: {formatPercentage(tier.markup)} -
-
-
+ ); })} -
- - +
+ {/* Help Text */} - - -
-

Understanding the Metrics:

-
    -
  • Profit: Selling price minus cost price
  • -
  • Profit Margin: Profit as a percentage of selling price
  • -
  • Markup: Profit as a percentage of cost price
  • -
+ +
+

+ + Quick Guide +

+
+
+ Profit + Selling Price - Cost Price +
+
+ Margin + (Profit / Selling Price) × 100 +
+
+ Markup + (Profit / Cost Price) × 100 +
- - -
+
+ + -
- +
+
diff --git a/components/tables/order-table.tsx b/components/tables/order-table.tsx index f95ce17..2f983f4 100644 --- a/components/tables/order-table.tsx +++ b/components/tables/order-table.tsx @@ -503,7 +503,7 @@ export default function OrderTable() { - + {paginatedOrders.map((order, index) => { const StatusIcon = statusConfig[order.status as keyof typeof statusConfig]?.icon || XCircle; const underpaidInfo = getUnderpaidInfo(order);