Files
ember-market-frontend/components/dashboard/promotions/PromotionsList.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

311 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/common/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/common/table';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/common/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/common/dialog';
import { toast } from '@/components/common/use-toast';
import { Badge } from '@/components/common/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)}
/>
</>
);
}