Add promotion details and modal to orders and promotions
Introduces promotion-related fields to the Order interface and displays promotion discounts in both the order details and order table. Adds a PromotionDetailsModal component for viewing detailed promotion analytics and eligibility, and integrates it into the PromotionsList with a new 'view details' action.
This commit is contained in:
@@ -69,6 +69,11 @@ interface Order {
|
|||||||
lastBalanceReceived?: number;
|
lastBalanceReceived?: number;
|
||||||
cryptoTotal?: number;
|
cryptoTotal?: number;
|
||||||
paymentAddress?: string;
|
paymentAddress?: string;
|
||||||
|
// Promotion fields
|
||||||
|
promotion?: string;
|
||||||
|
promotionCode?: string;
|
||||||
|
discountAmount?: number;
|
||||||
|
subtotalBeforeDiscount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OrderInList extends Order {
|
interface OrderInList extends Order {
|
||||||
@@ -697,6 +702,17 @@ export default function OrderDetailsPage() {
|
|||||||
<TableCell className="text-right font-medium">Shipping ({order?.shippingMethod.type})</TableCell>
|
<TableCell className="text-right font-medium">Shipping ({order?.shippingMethod.type})</TableCell>
|
||||||
<TableCell className="text-right">£{order?.shippingMethod.price.toFixed(2)}</TableCell>
|
<TableCell className="text-right">£{order?.shippingMethod.price.toFixed(2)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
{order?.promotionCode && order?.discountAmount && order?.discountAmount > 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2} />
|
||||||
|
<TableCell className="text-right font-medium text-green-600">
|
||||||
|
Promotion ({order.promotionCode})
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-green-600">
|
||||||
|
-£{order.discountAmount.toFixed(2)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} />
|
<TableCell colSpan={2} />
|
||||||
<TableCell className="text-right font-bold">Total</TableCell>
|
<TableCell className="text-right font-bold">Total</TableCell>
|
||||||
|
|||||||
391
components/dashboard/promotions/PromotionDetailsModal.tsx
Normal file
391
components/dashboard/promotions/PromotionDetailsModal.tsx
Normal file
@@ -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<Product[]>([]);
|
||||||
|
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<Product[]>('/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 (
|
||||||
|
<Dialog open={!!promotion} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<DialogTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<Target className="h-6 w-6" />
|
||||||
|
{promotion.code}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="mt-1">
|
||||||
|
Comprehensive promotion details and analytics
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant={isActive() ? 'default' : 'secondary'}
|
||||||
|
className={`text-sm px-3 py-1 ${isActive() ? 'bg-green-500' : 'bg-gray-500'}`}
|
||||||
|
>
|
||||||
|
{isActive() ? 'Active' : 'Inactive'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<ScrollArea className="max-h-[70vh] pr-4">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Basic Information */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Info className="h-5 w-5" />
|
||||||
|
Basic Information
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">Promotion Code</label>
|
||||||
|
<p className="text-lg font-mono font-bold">{promotion.code}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">Description</label>
|
||||||
|
<p className="text-sm">{promotion.description || 'No description provided'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{promotion.discountType === 'percentage' ? (
|
||||||
|
<Percent className="h-8 w-8 text-blue-500" />
|
||||||
|
) : (
|
||||||
|
<DollarSign className="h-8 w-8 text-green-500" />
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">Discount</label>
|
||||||
|
<p className="text-xl font-bold">{getDiscountDisplay()}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{promotion.discountType === 'percentage' ? 'Percentage' : 'Fixed Amount'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ShoppingCart className="h-8 w-8 text-orange-500" />
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">Min. Order</label>
|
||||||
|
<p className="text-xl font-bold">
|
||||||
|
{promotion.minOrderAmount > 0 ? `£${promotion.minOrderAmount.toFixed(2)}` : 'None'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Required minimum</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Users className="h-8 w-8 text-purple-500" />
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">Usage Limit</label>
|
||||||
|
<p className="text-xl font-bold">
|
||||||
|
{promotion.maxUsage ? promotion.maxUsage.toLocaleString() : 'Unlimited'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Maximum uses</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Usage Analytics */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-5 w-5" />
|
||||||
|
Usage Analytics
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm font-medium">Current Usage</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{promotion.usageCount} / {promotion.maxUsage || '∞'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
width: promotion.maxUsage
|
||||||
|
? `${Math.min(calculateUsagePercentage(), 100)}%`
|
||||||
|
: '0%'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
{promotion.maxUsage && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{calculateUsagePercentage()}% of limit used
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium">Total Uses</span>
|
||||||
|
<span className="text-sm font-bold">{promotion.usageCount.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium">Remaining Uses</span>
|
||||||
|
<span className="text-sm font-bold">
|
||||||
|
{promotion.maxUsage
|
||||||
|
? (promotion.maxUsage - promotion.usageCount).toLocaleString()
|
||||||
|
: 'Unlimited'
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Validity Period */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Calendar className="h-5 w-5" />
|
||||||
|
Validity Period
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Clock className="h-8 w-8 text-green-500" />
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">Start Date</label>
|
||||||
|
<p className="font-medium">{formatDate(promotion.startDate)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{promotion.endDate ? (
|
||||||
|
<Clock className="h-8 w-8 text-red-500" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle className="h-8 w-8 text-blue-500" />
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">End Date</label>
|
||||||
|
<p className="font-medium">
|
||||||
|
{promotion.endDate ? formatDate(promotion.endDate) : 'No expiry date'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Product Eligibility */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Package className="h-5 w-5" />
|
||||||
|
Product Eligibility
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">Applies To</label>
|
||||||
|
<p className="font-medium">{getApplicabilityText()}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{promotion.applicableProducts === 'specific' && promotion.specificProducts.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">
|
||||||
|
Specific Products ({promotion.specificProducts.length})
|
||||||
|
</label>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Loading products...</p>
|
||||||
|
) : (
|
||||||
|
getProductDetails(promotion.specificProducts).map((product) => (
|
||||||
|
<div key={product._id} className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||||
|
<span className="text-sm">{product.name}</span>
|
||||||
|
{!product.enabled && (
|
||||||
|
<Badge variant="secondary" className="text-xs">Disabled</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{promotion.blacklistedProducts.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground">
|
||||||
|
Excluded Products ({promotion.blacklistedProducts.length})
|
||||||
|
</label>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Loading products...</p>
|
||||||
|
) : (
|
||||||
|
getProductDetails(promotion.blacklistedProducts).map((product) => (
|
||||||
|
<div key={product._id} className="flex items-center gap-2">
|
||||||
|
<XCircle className="h-4 w-4 text-red-500" />
|
||||||
|
<span className="text-sm">{product.name}</span>
|
||||||
|
{!product.enabled && (
|
||||||
|
<Badge variant="secondary" className="text-xs">Disabled</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{promotion.applicableProducts === 'all' && promotion.blacklistedProducts.length === 0 && (
|
||||||
|
<div className="flex items-center gap-2 text-green-600">
|
||||||
|
<CheckCircle className="h-4 w-4" />
|
||||||
|
<span className="text-sm">This promotion applies to all products in your store</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Metadata */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Additional Information</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<label className="font-medium text-muted-foreground">Created</label>
|
||||||
|
<p>{formatDate(promotion.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="font-medium text-muted-foreground">Last Updated</label>
|
||||||
|
<p>{formatDate(promotion.updatedAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
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 { useRouter } from 'next/navigation';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +34,7 @@ import { Promotion } from '@/lib/types/promotion';
|
|||||||
import { fetchClient } from '@/lib/api';
|
import { fetchClient } from '@/lib/api';
|
||||||
import NewPromotionForm from './NewPromotionForm';
|
import NewPromotionForm from './NewPromotionForm';
|
||||||
import EditPromotionForm from './EditPromotionForm';
|
import EditPromotionForm from './EditPromotionForm';
|
||||||
|
import PromotionDetailsModal from './PromotionDetailsModal';
|
||||||
|
|
||||||
export default function PromotionsList() {
|
export default function PromotionsList() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -43,6 +44,7 @@ export default function PromotionsList() {
|
|||||||
const [editingPromotion, setEditingPromotion] = useState<Promotion | null>(null);
|
const [editingPromotion, setEditingPromotion] = useState<Promotion | null>(null);
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [promotionToDelete, setPromotionToDelete] = useState<string | null>(null);
|
const [promotionToDelete, setPromotionToDelete] = useState<string | null>(null);
|
||||||
|
const [viewingPromotion, setViewingPromotion] = useState<Promotion | null>(null);
|
||||||
|
|
||||||
// Load promotions on mount
|
// Load promotions on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -194,10 +196,19 @@ export default function PromotionsList() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => setViewingPromotion(promotion)}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="View Details"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleOpenEditDialog(promotion)}
|
onClick={() => handleOpenEditDialog(promotion)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
title="Edit Promotion"
|
||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -209,6 +220,7 @@ export default function PromotionsList() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="text-red-500 hover:text-red-600"
|
className="text-red-500 hover:text-red-600"
|
||||||
|
title="Delete Promotion"
|
||||||
>
|
>
|
||||||
<Trash className="h-4 w-4" />
|
<Trash className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -285,6 +297,12 @@ export default function PromotionsList() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Promotion Details Modal */}
|
||||||
|
<PromotionDetailsModal
|
||||||
|
promotion={viewingPromotion}
|
||||||
|
onClose={() => setViewingPromotion(null)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,9 @@ import {
|
|||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
Truck,
|
Truck,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
AlertTriangle
|
AlertTriangle,
|
||||||
|
Tag,
|
||||||
|
Percent
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { clientFetch } from '@/lib/api';
|
import { clientFetch } from '@/lib/api';
|
||||||
@@ -60,6 +62,11 @@ interface Order {
|
|||||||
underpaymentAmount?: number;
|
underpaymentAmount?: number;
|
||||||
lastBalanceReceived?: number;
|
lastBalanceReceived?: number;
|
||||||
cryptoTotal?: number;
|
cryptoTotal?: number;
|
||||||
|
// Promotion fields
|
||||||
|
promotion?: string;
|
||||||
|
promotionCode?: string;
|
||||||
|
discountAmount?: number;
|
||||||
|
subtotalBeforeDiscount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortableColumns = "orderId" | "totalPrice" | "status" | "orderDate" | "paidAt";
|
type SortableColumns = "orderId" | "totalPrice" | "status" | "orderDate" | "paidAt";
|
||||||
@@ -430,6 +437,7 @@ export default function OrderTable() {
|
|||||||
<TableHead className="cursor-pointer" onClick={() => handleSort("totalPrice")}>
|
<TableHead className="cursor-pointer" onClick={() => handleSort("totalPrice")}>
|
||||||
Total <ArrowUpDown className="ml-2 inline h-4 w-4" />
|
Total <ArrowUpDown className="ml-2 inline h-4 w-4" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
<TableHead>Promotion</TableHead>
|
||||||
<TableHead className="cursor-pointer" onClick={() => handleSort("status")}>
|
<TableHead className="cursor-pointer" onClick={() => handleSort("status")}>
|
||||||
Status <ArrowUpDown className="ml-2 inline h-4 w-4" />
|
Status <ArrowUpDown className="ml-2 inline h-4 w-4" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -468,6 +476,29 @@ export default function OrderTable() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{order.promotionCode ? (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Tag className="h-3 w-3 text-green-500" />
|
||||||
|
<span className="text-xs font-mono bg-green-100 text-green-800 px-2 py-0.5 rounded">
|
||||||
|
{order.promotionCode}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs text-green-600">
|
||||||
|
<Percent className="h-3 w-3" />
|
||||||
|
<span>-£{(order.discountAmount || 0).toFixed(2)}</span>
|
||||||
|
{order.subtotalBeforeDiscount && order.subtotalBeforeDiscount > 0 && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
(was £{order.subtotalBeforeDiscount.toFixed(2)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">-</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm ${
|
<div className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm ${
|
||||||
|
|||||||
Reference in New Issue
Block a user