import { useState, useEffect, useCallback } from 'react'; import { Tag, Plus, Edit2, Trash2, Search, XCircle, CheckCircle2, User, Heart, ShoppingBag, FolderTree, Shield, HelpCircle } from 'lucide-react'; import { toast } from 'react-hot-toast'; import api from '../services/api'; import Spinner from '../components/ui/Spinner'; import Pagination from '../components/ui/Pagination'; import ConfirmModal from '../components/ui/ConfirmModal'; import PriceInput from '../components/ui/PriceInput'; import Button from '../components/ui/Button'; import Modal from '../components/ui/Modal'; export interface CouponTarget { targetType: string; targetId: string; modifierType: string; modifierValue: number | string; } export interface Coupon { id: string; code: string; type: string; value: number; minCartValue?: number; maxCartValue?: number; maxUses?: number; usedCount?: number; expiresAt?: string; isActive: boolean; targets?: CouponTarget[]; } export interface CouponFormData { code: string; type: string; value: number | string; minCartValue: string; maxCartValue: string; maxUses: string; expiresAt: string; isActive: boolean; targets: CouponTarget[]; } export default function Coupons() { const [coupons, setCoupons] = useState([]); const [isLoading, setIsLoading] = useState(true); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const limit = 10; const [isModalOpen, setIsModalOpen] = useState(false); const [editingCoupon, setEditingCoupon] = useState(null); const [deleteTargetId, setDeleteTargetId] = useState(null); const [formData, setFormData] = useState({ code: '', type: 'percent', value: 0, minCartValue: '', maxCartValue: '', maxUses: '', expiresAt: '', isActive: true, targets: [] }); const fetchData = useCallback(async () => { try { setIsLoading(true); const res = await api.get('/admin/coupons', { params: { page, limit, search } }); if (res.data?.data) { setCoupons(res.data.data); setTotalPages(res.data.meta?.lastPage || 1); } } catch (err) { console.error(err); } finally { setIsLoading(false); } }, [page, search]); useEffect(() => { const timer = setTimeout(() => fetchData(), 500); return () => clearTimeout(timer); }, [fetchData]); const openModal = (coupon: Coupon | null = null) => { if (coupon) { setEditingCoupon(coupon); setFormData({ code: coupon.code, type: coupon.type, value: Number(coupon.value), minCartValue: coupon.minCartValue ? String(coupon.minCartValue) : '', maxCartValue: coupon.maxCartValue ? String(coupon.maxCartValue) : '', maxUses: coupon.maxUses ? String(coupon.maxUses) : '', expiresAt: coupon.expiresAt ? new Date(coupon.expiresAt).toISOString().split('T')[0] : '', isActive: coupon.isActive, targets: coupon.targets || [] }); } else { setEditingCoupon(null); setFormData({ code: '', type: 'percent', value: 0, minCartValue: '', maxCartValue: '', maxUses: '', expiresAt: '', isActive: true, targets: [] }); } setIsModalOpen(true); }; const handleSave = async (e: React.FormEvent) => { e.preventDefault(); try { const payload = { ...formData, value: Number(formData.value), minCartValue: formData.minCartValue ? Number(formData.minCartValue) : null, maxCartValue: formData.maxCartValue ? Number(formData.maxCartValue) : null, maxUses: formData.maxUses ? Number(formData.maxUses) : null, expiresAt: formData.expiresAt || null, targets: formData.targets.map(t => ({ ...t, modifierValue: t.modifierValue ? Number(t.modifierValue) : null })) }; if (editingCoupon) { await api.put(`/admin/coupons/${editingCoupon.id}`, payload); toast.success('کد تخفیف با موفقیت ویرایش شد'); } else { await api.post('/admin/coupons', payload); toast.success('کد تخفیف با موفقیت ایجاد شد'); } setIsModalOpen(false); fetchData(); } catch (err) { console.error('Save error', err); toast.error('خطا در ذخیره کد تخفیف'); } }; const confirmDelete = async () => { if (!deleteTargetId) return; try { await api.delete(`/admin/coupons/${deleteTargetId}`); toast.success('کد تخفیف با موفقیت حذف شد'); fetchData(); } catch { toast.error('خطا در حذف کد تخفیف'); } finally { setDeleteTargetId(null); } }; const handleToggle = async (id: string, currentStatus: boolean) => { try { await api.put(`/admin/coupons/${id}/toggle`, { isActive: !currentStatus }); fetchData(); } catch { toast.error('خطا در تغییر وضعیت کد تخفیف'); } }; return (

تخفیف‌های پلیمورفیک (Polymorphic)

مدیریت کدهای تخفیف با اهداف چندگانه (کاربران، حیوانات، محصولات و ...)

{ setSearch(e.target.value); setPage(1); }} className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all" />
{isLoading ? ( ) : coupons.length === 0 ? ( ) : ( coupons.map((c) => ( )) )}
کد پایه تخفیف تعداد اهداف اختصاصی استفاده شده وضعیت عملیات
هیچ کدی یافت نشد
{c.code} {c.type === 'percent' ? `${c.value}٪` : `${Number(c.value).toLocaleString()} تومان`} {c.targets?.length || 0} هدف {c.usedCount} {c.maxUses && / {c.maxUses}}
{!isLoading && totalPages > 1 && (
)}
setDeleteTargetId(null)} /> {isModalOpen && setIsModalOpen(false)} isEditing={!!editingCoupon} />}
); } interface CouponModalProps { formData: CouponFormData; setFormData: React.Dispatch>; onSave: (e: React.FormEvent) => Promise; onClose: () => void; isEditing: boolean; } // Sub-component for Modal to keep code clean function CouponModal({ formData, setFormData, onSave, onClose, isEditing }: CouponModalProps) { const addTarget = () => { setFormData({ ...formData, targets: [...formData.targets, { targetType: 'USER', targetId: '', modifierType: 'override', modifierValue: '' }] }); }; const removeTarget = (index: number) => { const nt = [...formData.targets]; nt.splice(index, 1); setFormData({ ...formData, targets: nt }); }; const updateTarget = (index: number, key: keyof CouponTarget, val: string | number) => { const nt = [...formData.targets]; nt[index] = { ...nt[index], [key]: val }; setFormData({ ...formData, targets: nt }); }; const getTypeIcon = (type: string) => { switch (type) { case 'USER': return ; case 'PET': return ; case 'PRODUCT': return ; case 'CATEGORY': return ; case 'ROLE': return ; default: return null; } }; return ( } >
{/* Base Settings */}

تنظیمات پایه

setFormData({...formData, code: e.target.value.toUpperCase()})} dir="ltr" className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-center uppercase font-bold" />
{formData.type === 'fixed' ? ( setFormData({ ...formData, value: val })} placeholder="مثال: ۵۰,۰۰۰" className="px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 text-xs" /> ) : (
setFormData({ ...formData, value: e.target.value })} dir="ltr" placeholder="مثال: ۲۰" className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 font-bold" /> %
)}
setFormData({ ...formData, minCartValue: val })} placeholder="۰ = بدون محدودیت" className="px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 text-xs" />
setFormData({ ...formData, maxCartValue: val })} placeholder="بدون سقف" className="px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 text-xs" />
setFormData({...formData, maxUses: e.target.value})} dir="ltr" className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 text-xs" />
setFormData({...formData, expiresAt: e.target.value})} className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 text-xs" />
{/* Target Builder */}

اهداف اختصاصی (Targets & Modifiers)

{formData.targets.length === 0 ? (
هیچ هدف اختصاصی تعریف نشده است. این کد تخفیف با مقادیر پایه برای همه اعمال می‌شود (مگر محدودیت دیگری وجود داشته باشد).
) : (
{formData.targets.map((t: CouponTarget, i: number) => (
updateTarget(i, 'targetId', e.target.value)} className="w-full px-2.5 py-2 rounded-xl border border-gray-200 text-xs outline-none focus:border-purple-500" />
updateTarget(i, 'modifierValue', e.target.value)} dir="ltr" className="w-full px-2.5 py-2 rounded-xl border border-gray-200 text-xs outline-none focus:border-purple-500" />
))}
)}
); }