309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from '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 {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import { toast } from '@/components/ui/use-toast';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Promotion } from '@/lib/types/promotion';
|
|
import { fetchClient } from '@/lib/api';
|
|
import dynamic from 'next/dynamic';
|
|
const NewPromotionForm = dynamic(() => import('./NewPromotionForm'));
|
|
const EditPromotionForm = dynamic(() => import('./EditPromotionForm'));
|
|
const PromotionDetailsModal = dynamic(() => import('./PromotionDetailsModal'));
|
|
|
|
export default function PromotionsList() {
|
|
const router = useRouter();
|
|
const [promotions, setPromotions] = useState<Promotion[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showNewDialog, setShowNewDialog] = useState(false);
|
|
const [editingPromotion, setEditingPromotion] = useState<Promotion | null>(null);
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
const [promotionToDelete, setPromotionToDelete] = useState<string | null>(null);
|
|
const [viewingPromotion, setViewingPromotion] = useState<Promotion | null>(null);
|
|
|
|
// Load promotions on mount
|
|
useEffect(() => {
|
|
loadPromotions();
|
|
}, []);
|
|
|
|
const loadPromotions = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetchClient<Promotion[]>('/promotions');
|
|
setPromotions(response || []);
|
|
} catch (error) {
|
|
console.error('Failed to fetch promotions:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to load promotions',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const deletePromotion = async (id: string) => {
|
|
try {
|
|
await fetchClient(`/promotions/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
toast({
|
|
title: 'Success',
|
|
description: 'Promotion deleted successfully',
|
|
});
|
|
|
|
// Refresh the list
|
|
loadPromotions();
|
|
} catch (error) {
|
|
console.error('Error deleting promotion:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to delete promotion',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setShowDeleteDialog(false);
|
|
setPromotionToDelete(null);
|
|
}
|
|
};
|
|
|
|
function handleOpenEditDialog(promotion: Promotion) {
|
|
setEditingPromotion(promotion);
|
|
}
|
|
|
|
function handleCloseEditDialog() {
|
|
setEditingPromotion(null);
|
|
}
|
|
|
|
function handleCreateComplete() {
|
|
setShowNewDialog(false);
|
|
loadPromotions();
|
|
}
|
|
|
|
function handleEditComplete() {
|
|
setEditingPromotion(null);
|
|
loadPromotions();
|
|
}
|
|
|
|
function formatDate(dateString: string | null) {
|
|
if (!dateString) return 'No end date';
|
|
return new Date(dateString).toLocaleDateString();
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-semibold">Your Promotions</h2>
|
|
<div className="flex gap-2">
|
|
<Button onClick={loadPromotions} variant="outline" size="sm">
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Refresh
|
|
</Button>
|
|
<Button onClick={() => setShowNewDialog(true)} size="sm">
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
New Promotion
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
{loading ? (
|
|
<div className="flex justify-center items-center h-64">
|
|
<RefreshCw className="h-6 w-6 animate-spin" />
|
|
</div>
|
|
) : promotions.length === 0 ? (
|
|
<div className="flex flex-col justify-center items-center h-64 text-center p-6">
|
|
<Tag className="h-12 w-12 mb-4 text-muted-foreground" />
|
|
<h3 className="text-lg font-medium">No promotions yet</h3>
|
|
<p className="text-muted-foreground mt-1 mb-4">
|
|
Create your first promotion to offer discounts to your customers
|
|
</p>
|
|
<Button onClick={() => setShowNewDialog(true)}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Create Promotion
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Code</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Value</TableHead>
|
|
<TableHead>Min. Order</TableHead>
|
|
<TableHead>Usage</TableHead>
|
|
<TableHead>Valid Until</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{promotions.map((promotion) => (
|
|
<TableRow key={promotion._id}>
|
|
<TableCell className="font-medium">{promotion.code}</TableCell>
|
|
<TableCell>
|
|
{promotion.discountType === 'percentage' ? 'Percentage' : 'Fixed Amount'}
|
|
</TableCell>
|
|
<TableCell>
|
|
{promotion.discountType === 'percentage'
|
|
? `${promotion.discountValue}%`
|
|
: `£${promotion.discountValue.toFixed(2)}`}
|
|
</TableCell>
|
|
<TableCell>
|
|
{promotion.minOrderAmount > 0
|
|
? `£${promotion.minOrderAmount.toFixed(2)}`
|
|
: 'None'}
|
|
</TableCell>
|
|
<TableCell>
|
|
{promotion.usageCount} / {promotion.maxUsage || '∞'}
|
|
</TableCell>
|
|
<TableCell>{formatDate(promotion.endDate)}</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
variant={promotion.isActive ? 'default' : 'secondary'}
|
|
className={promotion.isActive ? 'bg-green-500' : ''}
|
|
>
|
|
{promotion.isActive ? 'Active' : 'Inactive'}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<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
|
|
onClick={() => handleOpenEditDialog(promotion)}
|
|
variant="ghost"
|
|
size="icon"
|
|
title="Edit Promotion"
|
|
>
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setPromotionToDelete(promotion._id);
|
|
setShowDeleteDialog(true);
|
|
}}
|
|
variant="ghost"
|
|
size="icon"
|
|
className="text-red-500 hover:text-red-600"
|
|
title="Delete Promotion"
|
|
>
|
|
<Trash className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* New Promotion Dialog */}
|
|
<Dialog open={showNewDialog} onOpenChange={setShowNewDialog}>
|
|
<DialogContent className="max-w-4xl">
|
|
<DialogHeader className="pb-4">
|
|
<DialogTitle className="text-xl">Create New Promotion</DialogTitle>
|
|
<DialogDescription>
|
|
Add a promotional code to offer discounts to your customers.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="max-h-[70vh] overflow-y-auto pr-1">
|
|
<NewPromotionForm onSuccess={handleCreateComplete} onCancel={() => setShowNewDialog(false)} />
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Edit Promotion Dialog */}
|
|
<Dialog open={!!editingPromotion} onOpenChange={() => editingPromotion && handleCloseEditDialog()}>
|
|
<DialogContent className="max-w-4xl">
|
|
<DialogHeader className="pb-4">
|
|
<DialogTitle className="text-xl">Edit Promotion</DialogTitle>
|
|
<DialogDescription>
|
|
Modify this promotional code's details.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="max-h-[70vh] overflow-y-auto pr-1">
|
|
{editingPromotion && (
|
|
<EditPromotionForm
|
|
promotion={editingPromotion}
|
|
onSuccess={handleEditComplete}
|
|
onCancel={handleCloseEditDialog}
|
|
/>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Confirm Deletion</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to delete this promotion? This action cannot be undone.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowDeleteDialog(false)}
|
|
>
|
|
<X className="h-4 w-4 mr-2" />
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={() => promotionToDelete && deletePromotion(promotionToDelete)}
|
|
>
|
|
<Trash className="h-4 w-4 mr-2" />
|
|
Delete
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Promotion Details Modal */}
|
|
<PromotionDetailsModal
|
|
promotion={viewingPromotion}
|
|
onClose={() => setViewingPromotion(null)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|