diff --git a/app/dashboard/orders/[id]/page.tsx b/app/dashboard/orders/[id]/page.tsx index 1af2a60..3540c81 100644 --- a/app/dashboard/orders/[id]/page.tsx +++ b/app/dashboard/orders/[id]/page.tsx @@ -69,6 +69,11 @@ interface Order { lastBalanceReceived?: number; cryptoTotal?: number; paymentAddress?: string; + // Promotion fields + promotion?: string; + promotionCode?: string; + discountAmount?: number; + subtotalBeforeDiscount?: number; } interface OrderInList extends Order { @@ -697,6 +702,17 @@ export default function OrderDetailsPage() { Shipping ({order?.shippingMethod.type}) £{order?.shippingMethod.price.toFixed(2)} + {order?.promotionCode && order?.discountAmount && order?.discountAmount > 0 && ( + + + + Promotion ({order.promotionCode}) + + + -£{order.discountAmount.toFixed(2)} + + + )} Total diff --git a/components/dashboard/promotions/PromotionDetailsModal.tsx b/components/dashboard/promotions/PromotionDetailsModal.tsx new file mode 100644 index 0000000..a666277 --- /dev/null +++ b/components/dashboard/promotions/PromotionDetailsModal.tsx @@ -0,0 +1,391 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription +} from '@/components/ui/dialog'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { + Calendar, + Target, + TrendingUp, + Package, + ShoppingCart, + Percent, + DollarSign, + Users, + Clock, + CheckCircle, + XCircle, + Info +} from 'lucide-react'; +import { Promotion, Product } from '@/lib/types/promotion'; +import { fetchClient } from '@/lib/api'; +import { toast } from '@/components/ui/use-toast'; + +interface PromotionDetailsModalProps { + promotion: Promotion | null; + onClose: () => void; +} + +export default function PromotionDetailsModal({ + promotion, + onClose +}: PromotionDetailsModalProps) { + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (promotion && (promotion.blacklistedProducts.length > 0 || promotion.specificProducts.length > 0)) { + loadProducts(); + } + }, [promotion]); + + const loadProducts = async () => { + setLoading(true); + try { + const response = await fetchClient('/promotions/products/all'); + setProducts(response || []); + } catch (error) { + console.error('Failed to fetch products:', error); + toast({ + title: 'Warning', + description: 'Could not load product details', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + const getProductDetails = (productIds: string[]): Product[] => { + return products.filter(product => productIds.includes(product._id)); + }; + + const formatDate = (dateString: string | null) => { + if (!dateString) return 'No end date'; + return new Date(dateString).toLocaleDateString('en-GB', { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + const calculateUsagePercentage = () => { + if (!promotion?.maxUsage) return 0; + return Math.round((promotion.usageCount / promotion.maxUsage) * 100); + }; + + const getDiscountDisplay = () => { + if (!promotion) return ''; + return promotion.discountType === 'percentage' + ? `${promotion.discountValue}%` + : `£${promotion.discountValue.toFixed(2)}`; + }; + + const getApplicabilityText = () => { + if (!promotion) return ''; + switch (promotion.applicableProducts) { + case 'all': + return 'All products (except blacklisted)'; + case 'specific': + return 'Only specific products'; + case 'exclude_specific': + return 'All products except specific ones'; + default: + return 'All products'; + } + }; + + const isActive = () => { + if (!promotion) return false; + const now = new Date(); + const startDate = new Date(promotion.startDate); + const endDate = promotion.endDate ? new Date(promotion.endDate) : null; + + return promotion.isActive && + now >= startDate && + (!endDate || now <= endDate) && + (!promotion.maxUsage || promotion.usageCount < promotion.maxUsage); + }; + + if (!promotion) return null; + + return ( + + + +
+
+ + + {promotion.code} + + + Comprehensive promotion details and analytics + +
+ + {isActive() ? 'Active' : 'Inactive'} + +
+
+ + +
+ {/* Basic Information */} + + + + + Basic Information + + + +
+
+ +

{promotion.code}

+
+
+ +

{promotion.description || 'No description provided'}

+
+
+ + + +
+
+ {promotion.discountType === 'percentage' ? ( + + ) : ( + + )} +
+ +

{getDiscountDisplay()}

+

+ {promotion.discountType === 'percentage' ? 'Percentage' : 'Fixed Amount'} +

+
+
+ +
+ +
+ +

+ {promotion.minOrderAmount > 0 ? `£${promotion.minOrderAmount.toFixed(2)}` : 'None'} +

+

Required minimum

+
+
+ +
+ +
+ +

+ {promotion.maxUsage ? promotion.maxUsage.toLocaleString() : 'Unlimited'} +

+

Maximum uses

+
+
+
+
+
+ + {/* Usage Analytics */} + + + + + Usage Analytics + + + +
+
+
+ Current Usage + + {promotion.usageCount} / {promotion.maxUsage || '∞'} + +
+
+
+
+ {promotion.maxUsage && ( +

+ {calculateUsagePercentage()}% of limit used +

+ )} +
+ +
+
+ Total Uses + {promotion.usageCount.toLocaleString()} +
+
+ Remaining Uses + + {promotion.maxUsage + ? (promotion.maxUsage - promotion.usageCount).toLocaleString() + : 'Unlimited' + } + +
+
+
+
+
+ + {/* Validity Period */} + + + + + Validity Period + + + +
+
+ +
+ +

{formatDate(promotion.startDate)}

+
+
+ +
+ {promotion.endDate ? ( + + ) : ( + + )} +
+ +

+ {promotion.endDate ? formatDate(promotion.endDate) : 'No expiry date'} +

+
+
+
+
+
+ + {/* Product Eligibility */} + + + + + Product Eligibility + + + +
+ +

{getApplicabilityText()}

+
+ + {promotion.applicableProducts === 'specific' && promotion.specificProducts.length > 0 && ( +
+ +
+ {loading ? ( +

Loading products...

+ ) : ( + getProductDetails(promotion.specificProducts).map((product) => ( +
+ + {product.name} + {!product.enabled && ( + Disabled + )} +
+ )) + )} +
+
+ )} + + {promotion.blacklistedProducts.length > 0 && ( +
+ +
+ {loading ? ( +

Loading products...

+ ) : ( + getProductDetails(promotion.blacklistedProducts).map((product) => ( +
+ + {product.name} + {!product.enabled && ( + Disabled + )} +
+ )) + )} +
+
+ )} + + {promotion.applicableProducts === 'all' && promotion.blacklistedProducts.length === 0 && ( +
+ + This promotion applies to all products in your store +
+ )} +
+
+ + {/* Metadata */} + + + Additional Information + + +
+
+ +

{formatDate(promotion.createdAt)}

+
+
+ +

{formatDate(promotion.updatedAt)}

+
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/promotions/PromotionsList.tsx b/components/dashboard/promotions/PromotionsList.tsx index 63e7f12..97c4227 100644 --- a/components/dashboard/promotions/PromotionsList.tsx +++ b/components/dashboard/promotions/PromotionsList.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { Plus, Tag, RefreshCw, Trash, Edit, Check, X } from 'lucide-react'; +import { Plus, Tag, RefreshCw, Trash, Edit, Check, X, Eye } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { @@ -34,6 +34,7 @@ import { Promotion } from '@/lib/types/promotion'; import { fetchClient } from '@/lib/api'; import NewPromotionForm from './NewPromotionForm'; import EditPromotionForm from './EditPromotionForm'; +import PromotionDetailsModal from './PromotionDetailsModal'; export default function PromotionsList() { const router = useRouter(); @@ -43,6 +44,7 @@ export default function PromotionsList() { const [editingPromotion, setEditingPromotion] = useState(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [promotionToDelete, setPromotionToDelete] = useState(null); + const [viewingPromotion, setViewingPromotion] = useState(null); // Load promotions on mount useEffect(() => { @@ -194,10 +196,19 @@ export default function PromotionsList() {
+ @@ -209,6 +220,7 @@ export default function PromotionsList() { variant="ghost" size="icon" className="text-red-500 hover:text-red-600" + title="Delete Promotion" > @@ -285,6 +297,12 @@ export default function PromotionsList() { + + {/* Promotion Details Modal */} + setViewingPromotion(null)} + /> ); } \ No newline at end of file diff --git a/components/tables/order-table.tsx b/components/tables/order-table.tsx index fba0ee9..da2b047 100644 --- a/components/tables/order-table.tsx +++ b/components/tables/order-table.tsx @@ -28,7 +28,9 @@ import { ArrowUpDown, Truck, MessageCircle, - AlertTriangle + AlertTriangle, + Tag, + Percent } from "lucide-react"; import Link from "next/link"; import { clientFetch } from '@/lib/api'; @@ -60,6 +62,11 @@ interface Order { underpaymentAmount?: number; lastBalanceReceived?: number; cryptoTotal?: number; + // Promotion fields + promotion?: string; + promotionCode?: string; + discountAmount?: number; + subtotalBeforeDiscount?: number; } type SortableColumns = "orderId" | "totalPrice" | "status" | "orderDate" | "paidAt"; @@ -430,6 +437,7 @@ export default function OrderTable() { handleSort("totalPrice")}> Total + Promotion handleSort("status")}> Status @@ -468,6 +476,29 @@ export default function OrderTable() { )}
+ + {order.promotionCode ? ( +
+
+ + + {order.promotionCode} + +
+
+ + -£{(order.discountAmount || 0).toFixed(2)} + {order.subtotalBeforeDiscount && order.subtotalBeforeDiscount > 0 && ( + + (was £{order.subtotalBeforeDiscount.toFixed(2)}) + + )} +
+
+ ) : ( + - + )} +