Files
ember-market-frontend/components/dashboard/promotions/PromotionsList.tsx
g 0176f89cb7 Add CSV export for orders and update UI symbols
Introduces an exportOrdersToCSV function in lib/api-client.ts to allow exporting orders by status as a CSV file. Updates various UI components to use the '•' (bullet) symbol instead of '·' (middle dot) and replaces some emoji/unicode characters for improved consistency and compatibility. Also normalizes the 'use client' directive to include a BOM in many files.
2025-12-15 17:57:18 +00:00

310 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)}
/>
</>
);
}