Files
ember-market-frontend/components/dashboard/promotions/PromotionDetailsModal.tsx
g fe01f31538
Some checks failed
Build Frontend / build (push) Failing after 7s
Refactor UI imports and update component paths
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
2026-01-13 05:02:13 +00:00

387 lines
15 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription
} from '@/components/common/dialog';
import { Badge } from '@/components/common/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/common/card';
import { Separator } from '@/components/common/separator';
import { ScrollArea } from '@/components/common/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/common/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="text-sm">
<label className="font-medium text-muted-foreground">Created</label>
<p>{formatDate(promotion.createdAt)}</p>
</div>
</CardContent>
</Card>
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);
}