feat(admin): standardize Buttons and Modals across CMS, Catalog, Payment, and FAQ managers
This commit is contained in:
parent
32f8b8b656
commit
a92cd7fc85
@ -42,32 +42,33 @@ export default function Button({
|
||||
const baseClasses =
|
||||
'inline-flex items-center justify-center font-bold whitespace-nowrap transition-all select-none rounded-xl cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
|
||||
// Size styling
|
||||
// Size styling - crisp, modern and compact
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
xs: 'px-2.5 py-1 text-[11px] gap-1.5',
|
||||
sm: 'px-3.5 py-1.5 text-xs gap-2',
|
||||
md: 'px-4 py-2.5 text-xs sm:text-sm gap-2',
|
||||
lg: 'px-6 py-3 text-sm sm:text-base gap-2.5 shadow-md',
|
||||
sm: 'px-3 py-1.5 text-xs gap-1.5',
|
||||
md: 'px-3.5 py-2 text-xs sm:text-sm gap-2',
|
||||
lg: 'px-5 py-2.5 text-sm sm:text-base gap-2 shadow-sm',
|
||||
};
|
||||
|
||||
// Variant styling
|
||||
const variantClasses: Record<ButtonVariant, string> = {
|
||||
primary:
|
||||
'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm shadow-indigo-200 focus:ring-indigo-500 border border-indigo-600',
|
||||
'bg-purple-600 hover:bg-purple-700 text-white shadow-xs focus:ring-purple-500 border border-purple-600',
|
||||
secondary:
|
||||
'bg-slate-100 hover:bg-slate-200 text-slate-700 focus:ring-slate-400 border border-slate-200',
|
||||
danger:
|
||||
'bg-rose-600 hover:bg-rose-700 text-white shadow-sm shadow-rose-200 focus:ring-rose-500 border border-rose-600',
|
||||
'bg-rose-600 hover:bg-rose-700 text-white shadow-xs focus:ring-rose-500 border border-rose-600',
|
||||
success:
|
||||
'bg-emerald-600 hover:bg-emerald-700 text-white shadow-sm shadow-emerald-200 focus:ring-emerald-500 border border-emerald-600',
|
||||
'bg-emerald-600 hover:bg-emerald-700 text-white shadow-xs focus:ring-emerald-500 border border-emerald-600',
|
||||
warning:
|
||||
'bg-amber-500 hover:bg-amber-600 text-white shadow-sm shadow-amber-200 focus:ring-amber-400 border border-amber-500',
|
||||
'bg-amber-500 hover:bg-amber-600 text-white shadow-xs focus:ring-amber-400 border border-amber-500',
|
||||
outline:
|
||||
'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-indigo-500',
|
||||
'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-purple-500',
|
||||
ghost:
|
||||
'bg-transparent hover:bg-gray-100 text-gray-600 focus:ring-gray-300 border border-transparent',
|
||||
};
|
||||
|
||||
|
||||
const iconSizes: Record<ButtonSize, string> = {
|
||||
xs: 'w-3 h-3',
|
||||
sm: 'w-3.5 h-3.5',
|
||||
|
||||
@ -5,8 +5,11 @@ import api, { BASE_DOMAIN } from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
import MediaSelector from '../components/ui/MediaSelector';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
import type { Banner } from '../types/admin';
|
||||
|
||||
|
||||
export default function BannersManager() {
|
||||
const [banners, setBanners] = useState<Banner[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@ -166,13 +169,14 @@ export default function BannersManager() {
|
||||
</h2>
|
||||
<p className="text-gray-500 font-medium mt-1">مدیریت اسلایدر صفحه اصلی و تبلیغات بنری</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => openModal()}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
افزودن بنر جدید
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
@ -198,7 +202,7 @@ export default function BannersManager() {
|
||||
</tr>
|
||||
) : banners.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="py-12 text-center text-gray-500 font-medium">هیچ بنری ثبت نشده است</td>
|
||||
<td colSpan={7} className="py-12 text-center text-gray-500 font-medium">هیچ بنری یافت نشد</td>
|
||||
</tr>
|
||||
) : (
|
||||
banners.map((banner, index) => (
|
||||
@ -207,71 +211,79 @@ export default function BannersManager() {
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
disabled={index === 0}
|
||||
onClick={() => handleMoveOrder(index, 'up')}
|
||||
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30"
|
||||
onClick={() => handleReorder(index, 'up')}
|
||||
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30 disabled:hover:text-gray-400 cursor-pointer"
|
||||
>
|
||||
<MoveUp className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="font-mono text-sm font-bold text-gray-600 px-1">{index + 1}</span>
|
||||
<button
|
||||
disabled={index === banners.length - 1}
|
||||
onClick={() => handleMoveOrder(index, 'down')}
|
||||
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30"
|
||||
onClick={() => handleReorder(index, 'down')}
|
||||
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30 disabled:hover:text-gray-400 cursor-pointer"
|
||||
>
|
||||
<MoveDown className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-6">
|
||||
<div className="w-20 h-12 rounded-xl bg-gray-100 border border-gray-200 overflow-hidden flex items-center justify-center">
|
||||
<td className="py-4 px-6">
|
||||
{banner.imageUrl ? (
|
||||
<img src={getImageFullUrl(banner.imageUrl)} alt={banner.title} className="w-full h-full object-cover" />
|
||||
<img
|
||||
src={getImageFullUrl(banner.imageUrl)}
|
||||
alt={banner.title || 'Banner'}
|
||||
className="w-20 h-10 object-cover rounded-lg border border-gray-100 shadow-xs"
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="w-6 h-6 text-gray-400" />
|
||||
<div className="w-20 h-10 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400">
|
||||
<ImageIcon className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 px-6">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-gray-900">{banner.title}</span>
|
||||
{banner.subtitle && <span className="text-xs text-gray-500 mt-0.5">{banner.subtitle}</span>}
|
||||
</div>
|
||||
<div className="font-bold text-gray-900">{banner.title || '-'}</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">{banner.subtitle || '-'}</div>
|
||||
</td>
|
||||
<td className="py-4 px-6">
|
||||
<span className="inline-block px-3 py-1 bg-purple-50 text-purple-700 text-xs font-bold rounded-lg border border-purple-100">
|
||||
{banner.position === 'home_hero'
|
||||
? 'اسلایدر هیرو اصلی'
|
||||
: banner.position === 'home_middle'
|
||||
? 'بنر میانی صفحه نخست'
|
||||
: banner.position === 'home_bottom'
|
||||
? 'بنر عریض پایین خانه'
|
||||
: banner.position === 'shop_top'
|
||||
? 'بالای فروشگاه و کاتالوگ'
|
||||
: banner.position === 'category_top'
|
||||
? 'بالای دستهبندی'
|
||||
: 'سایدبار'}
|
||||
<span className="px-2.5 py-1 rounded-full text-xs font-bold bg-purple-50 text-purple-700">
|
||||
{banner.position === 'home_hero' ? 'اسلایدر اصلی' : banner.position === 'home_promo' ? 'بنر تبلیغاتی میانی' : banner.position}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-6 text-gray-500 font-mono text-xs dir-ltr text-right">
|
||||
<td className="py-4 px-6 text-sm text-gray-500 font-mono" dir="ltr">
|
||||
{banner.linkUrl || '-'}
|
||||
</td>
|
||||
<td className="py-4 px-6">
|
||||
<button
|
||||
onClick={() => handleToggleActive(banner)}
|
||||
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold transition-colors cursor-pointer ${
|
||||
banner.isActive
|
||||
? 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||
: 'bg-rose-50 text-rose-700 hover:bg-rose-100'
|
||||
}`}
|
||||
>
|
||||
{banner.isActive ? (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-green-50 text-green-700 text-xs font-bold rounded-full border border-green-200">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" /> فعال
|
||||
</span>
|
||||
<>
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
فعال
|
||||
</>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-gray-100 text-gray-600 text-xs font-bold rounded-full border border-gray-200">
|
||||
<XCircle className="w-3.5 h-3.5" /> غیرفعال
|
||||
</span>
|
||||
<>
|
||||
<XCircle className="w-3.5 h-3.5" />
|
||||
غیرفعال
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="py-4 px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => openModal(banner)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||
<button
|
||||
onClick={() => openModal(banner)}
|
||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => setDeleteTargetId(banner.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||
<button
|
||||
onClick={() => setDeleteTargetId(banner.id)}
|
||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@ -284,96 +296,122 @@ export default function BannersManager() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Modal */}
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{editingBanner ? 'ویرایش بنر' : 'افزودن بنر جدید'}
|
||||
</h3>
|
||||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={editingBanner ? 'ویرایش بنر' : 'افزودن بنر جدید'}
|
||||
icon={ImageIcon}
|
||||
maxWidth="2xl"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="bannerForm"
|
||||
isLoading={isSaving}
|
||||
>
|
||||
ذخیره تغییرات
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="p-6 space-y-4">
|
||||
}
|
||||
>
|
||||
<div className="space-y-6 text-right font-vazir">
|
||||
<form id="bannerForm" onSubmit={handleSave} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">عنوان بنر *</label>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">عنوان بنر</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder="مثال: تخصص دارویی از آلمان"
|
||||
placeholder="مثال: تخفیف ویژه مکملهای غذایی"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">زیرعنوان (اختیاری)</label>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">زیرعنوان (توضیحات کوتاه)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.subtitle}
|
||||
onChange={(e) => setFormData({ ...formData, subtitle: e.target.value })}
|
||||
placeholder="مثال: بیش از ۴۰ سال تجربه"
|
||||
placeholder="مثال: تا ۳۰٪ تخفیف برای مدت محدود"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">آدرس تصویر (imageUrl) *</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
value={formData.imageUrl}
|
||||
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
|
||||
placeholder="https://... یا /uploads/..."
|
||||
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
||||
dir="ltr"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsMediaSelectorOpen(true)}
|
||||
className="px-3 py-2.5 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0"
|
||||
>
|
||||
انتخاب از رسانه
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">لینک هدف (linkUrl)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.linkUrl}
|
||||
onChange={(e) => setFormData({ ...formData, linkUrl: e.target.value })}
|
||||
placeholder="مثال: /products/joints"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">موقعیت نمایش</label>
|
||||
<select
|
||||
value={formData.position}
|
||||
onChange={(e) => setFormData({ ...formData, position: e.target.value })}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-bold bg-white"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
|
||||
>
|
||||
<option value="home_hero">اسلایدر هیرو اصلی (home_hero)</option>
|
||||
<option value="home_middle">بنر میانی صفحه نخست (home_middle)</option>
|
||||
<option value="home_bottom">بنر عریض پایین خانه (home_bottom)</option>
|
||||
<option value="shop_top">بالای فروشگاه و کاتالوگ (shop_top)</option>
|
||||
<option value="category_top">بالای دستهبندی (category_top)</option>
|
||||
<option value="product_sidebar">سایدبار صفحات محصول و مقالات (product_sidebar)</option>
|
||||
<option value="home_hero">اسلایدر اصلی (Home Hero)</option>
|
||||
<option value="home_promo">بنر تبلیغاتی میانی (Home Promo)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">ترتیب نمایش (order)</label>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">لینک هدف (URL)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.linkUrl}
|
||||
onChange={(e) => setFormData({ ...formData, linkUrl: e.target.value })}
|
||||
placeholder="/shop یا https://..."
|
||||
dir="ltr"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر بنر *</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.imageUrl}
|
||||
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
|
||||
placeholder="آدرس تصویر یا انتخاب از رسانهها..."
|
||||
dir="ltr"
|
||||
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
startIcon={ImageIcon}
|
||||
onClick={() => setIsMediaSelectorOpen(true)}
|
||||
>
|
||||
انتخاب تصویر
|
||||
</Button>
|
||||
</div>
|
||||
{formData.imageUrl && (
|
||||
<div className="mt-3">
|
||||
<img
|
||||
src={getImageFullUrl(formData.imageUrl)}
|
||||
alt="Preview"
|
||||
className="h-28 w-full object-cover rounded-xl border border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-center">
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">ترتیب نمایش</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.order}
|
||||
@ -395,29 +433,12 @@ export default function BannersManager() {
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
||||
>
|
||||
{isSaving && <Spinner size="sm" />}
|
||||
ذخیره تغییرات
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{deleteTargetId && (
|
||||
<ConfirmModal
|
||||
|
||||
@ -6,6 +6,10 @@ import Spinner from '../components/ui/Spinner';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import MediaSelector from '../components/ui/MediaSelector';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
|
||||
|
||||
export interface BlogPost {
|
||||
author?: {
|
||||
@ -134,13 +138,14 @@ export default function Blogs() {
|
||||
</h2>
|
||||
<p className="text-gray-500 font-medium mt-1">مدیریت محتوای آموزشی و مقالات علمی سایت</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => openModal()}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
مقاله جدید
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
||||
@ -196,8 +201,8 @@ export default function Blogs() {
|
||||
</td>
|
||||
<td className="py-4 px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => openModal(blog)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"><Edit2 className="w-4 h-4" /></button>
|
||||
<button onClick={() => setDeleteTargetId(blog.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2 className="w-4 h-4" /></button>
|
||||
<button onClick={() => openModal(blog)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"><Edit2 className="w-4 h-4" /></button>
|
||||
<button onClick={() => setDeleteTargetId(blog.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"><Trash2 className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -214,18 +219,34 @@ export default function Blogs() {
|
||||
</div>
|
||||
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[95vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
{editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
|
||||
</h3>
|
||||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||
<XCircle className="w-6 h-6" />
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
|
||||
icon={FileText}
|
||||
maxWidth="4xl"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="blogForm"
|
||||
>
|
||||
ذخیره مقاله
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 overflow-y-auto flex-1">
|
||||
}
|
||||
>
|
||||
<div className="space-y-6 text-right font-vazir">
|
||||
<form id="blogForm" onSubmit={handleSave} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
@ -254,7 +275,14 @@ export default function Blogs() {
|
||||
<ImageIcon className="w-6 h-6" />
|
||||
</div>
|
||||
)}
|
||||
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="px-4 py-2 border-2 border-dashed border-gray-300 rounded-xl text-gray-600 font-bold hover:border-purple-500 hover:text-purple-600">انتخاب از گالری</button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsMediaSelectorOpen(true)}
|
||||
>
|
||||
انتخاب از گالری
|
||||
</Button>
|
||||
</div>
|
||||
<label className="flex items-center gap-3 cursor-pointer p-4 border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors">
|
||||
<input type="checkbox" checked={formData.isPublished} onChange={(e) => setFormData({ ...formData, isPublished: e.target.checked })} className="w-5 h-5 text-purple-600 rounded focus:ring-purple-500" />
|
||||
@ -278,18 +306,12 @@ export default function Blogs() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">انصراف</button>
|
||||
<button type="submit" form="blogForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">ذخیره مقاله</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
|
||||
<MediaSelector isOpen={isMediaSelectorOpen} onClose={() => setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({ ...formData, imageUrl: url })} />
|
||||
|
||||
<ConfirmModal
|
||||
|
||||
@ -6,6 +6,10 @@ import Spinner from '../components/ui/Spinner';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import MediaSelector from '../components/ui/MediaSelector';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
@ -127,15 +131,17 @@ export default function Categories() {
|
||||
</h2>
|
||||
<p className="text-gray-500 font-medium mt-1">ایجاد دستهبندی محصولات و تنظیمات سئو (SEO)</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => openModal()}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
دستهبندی جدید
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
@ -197,10 +203,10 @@ export default function Categories() {
|
||||
</td>
|
||||
<td className="py-4 px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||
<button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||
<button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@ -220,18 +226,34 @@ export default function Categories() {
|
||||
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
{editingCategory ? 'ویرایش دستهبندی' : 'افزودن دستهبندی جدید'}
|
||||
</h3>
|
||||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={editingCategory ? 'ویرایش دستهبندی' : 'افزودن دستهبندی جدید'}
|
||||
icon={FolderTree}
|
||||
maxWidth="2xl"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="categoryForm"
|
||||
>
|
||||
ذخیره دستهبندی
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 overflow-y-auto flex-1">
|
||||
}
|
||||
>
|
||||
<div className="space-y-6 text-right font-vazir">
|
||||
<form id="categoryForm" onSubmit={handleSave} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
@ -264,16 +286,17 @@ export default function Categories() {
|
||||
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `${BASE_DOMAIN}${formData.imageUrl}`} alt="preview" className="w-16 h-16 rounded-xl object-cover border border-gray-200" />
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
|
||||
<ImageIcon className="w-6 h-6" />
|
||||
<ImageIcon className="w-5 h-5" />
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsMediaSelectorOpen(true)}
|
||||
className="px-4 py-2 border-2 border-dashed border-gray-300 rounded-xl text-gray-600 font-bold hover:border-purple-500 hover:text-purple-600 transition-colors"
|
||||
>
|
||||
انتخاب از گالری
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -312,28 +335,11 @@ export default function Categories() {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="categoryForm"
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200"
|
||||
>
|
||||
ذخیره دستهبندی
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Media Selector Modal */}
|
||||
|
||||
<MediaSelector
|
||||
isOpen={isMediaSelectorOpen}
|
||||
onClose={() => setIsMediaSelectorOpen(false)}
|
||||
|
||||
@ -165,12 +165,17 @@ export default function Coupons() {
|
||||
</h2>
|
||||
<p className="text-gray-500 font-medium mt-1">مدیریت کدهای تخفیف با اهداف چندگانه (کاربران، حیوانات، محصولات و ...)</p>
|
||||
</div>
|
||||
<button onClick={() => openModal()} className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200">
|
||||
<Plus className="w-5 h-5" />
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => openModal()}
|
||||
>
|
||||
کد تخفیف جدید
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
|
||||
@ -4,6 +4,9 @@ import { toast } from 'react-hot-toast';
|
||||
import api from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
|
||||
interface FAQ {
|
||||
id: string;
|
||||
@ -129,13 +132,14 @@ export default function FAQManager() {
|
||||
سوالات و پاسخهایی که در صفحه اصلی سایت نمایش داده میشوند — با Schema.org FAQPage برای SEO
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => openModal()}
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-orange-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
افزودن سوال جدید
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
@ -147,7 +151,7 @@ export default function FAQManager() {
|
||||
<div className="space-y-3">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<Spinner size="lg" className="text-orange-500" />
|
||||
<Spinner size="lg" className="text-purple-600" />
|
||||
</div>
|
||||
) : faqs.length === 0 ? (
|
||||
<div className="bg-white rounded-2xl border border-gray-200 py-16 text-center">
|
||||
@ -155,56 +159,71 @@ export default function FAQManager() {
|
||||
<p className="text-gray-500 font-medium">هیچ سوالی ثبت نشده است</p>
|
||||
<button
|
||||
onClick={() => openModal()}
|
||||
className="mt-4 text-orange-500 font-bold hover:underline text-sm"
|
||||
className="mt-4 text-purple-600 font-bold hover:underline text-sm cursor-pointer"
|
||||
>
|
||||
اولین سوال را اضافه کنید
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
faqs.map((faq, idx) => (
|
||||
faqs.map((faq) => (
|
||||
<div
|
||||
key={faq.id}
|
||||
className={`bg-white rounded-2xl border p-5 transition-all ${
|
||||
faq.isActive ? 'border-gray-200' : 'border-dashed border-gray-300 opacity-60'
|
||||
faq.isActive ? 'border-gray-200 shadow-sm' : 'border-gray-100 opacity-60 bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center gap-2 shrink-0 mt-1">
|
||||
<GripVertical className="w-4 h-4 text-gray-300" />
|
||||
<span className="text-xs font-black text-gray-400 w-5 text-center">{idx + 1}</span>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div className="p-2 bg-orange-50 text-orange-600 rounded-xl mt-0.5 shrink-0">
|
||||
<HelpCircle className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-black text-gray-900 text-sm">{faq.question}</span>
|
||||
{!faq.isActive && (
|
||||
<span className="px-2 py-0.5 bg-gray-100 text-gray-500 text-[10px] font-bold rounded">
|
||||
غیرفعال
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs font-bold px-2 py-0.5 bg-gray-100 text-gray-600 rounded-md">
|
||||
ترتیب: {faq.order}
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs font-bold px-2 py-0.5 rounded-md ${
|
||||
faq.isActive
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{faq.isActive ? 'فعال' : 'غیرفعال'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2">{faq.answer}</p>
|
||||
<h3 className="font-bold text-gray-900 text-base">{faq.question}</h3>
|
||||
<p className="text-gray-600 text-sm leading-relaxed whitespace-pre-line">
|
||||
{faq.answer}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => toggleActive(faq)}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
title={faq.isActive ? 'غیرفعالسازی' : 'فعالسازی'}
|
||||
className={`p-2 rounded-xl transition-colors cursor-pointer ${
|
||||
faq.isActive
|
||||
? 'text-green-600 hover:bg-green-50'
|
||||
: 'text-gray-400 hover:bg-gray-50'
|
||||
: 'text-gray-400 hover:bg-gray-100'
|
||||
}`}
|
||||
title={faq.isActive ? 'غیرفعال کردن' : 'فعال کردن'}
|
||||
>
|
||||
{faq.isActive ? <ToggleRight className="w-5 h-5" /> : <ToggleLeft className="w-5 h-5" />}
|
||||
{faq.isActive ? (
|
||||
<ToggleRight className="w-5 h-5" />
|
||||
) : (
|
||||
<ToggleLeft className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openModal(faq)}
|
||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-xl transition-colors cursor-pointer"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteTargetId(faq.id)}
|
||||
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||
className="p-2 text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@ -215,31 +234,49 @@ export default function FAQManager() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Form Modal */}
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'}
|
||||
</h3>
|
||||
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg">
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'}
|
||||
icon={HelpCircle}
|
||||
maxWidth="2xl"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="faqForm"
|
||||
isLoading={isSaving}
|
||||
>
|
||||
ذخیره سوال
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="p-6 space-y-4">
|
||||
}
|
||||
>
|
||||
<div className="space-y-6 text-right font-vazir">
|
||||
<form id="faqForm" onSubmit={handleSave} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">
|
||||
سوال <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
type="text"
|
||||
value={formData.question}
|
||||
onChange={(e) => setFormData({ ...formData, question: e.target.value })}
|
||||
placeholder="مثال: آیا محصولات کنینا برای سگهای حساس مناسب است؟"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -253,7 +290,7 @@ export default function FAQManager() {
|
||||
value={formData.answer}
|
||||
onChange={(e) => setFormData({ ...formData, answer: e.target.value })}
|
||||
placeholder="پاسخ کامل، واضح و مفید..."
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm resize-none"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -264,44 +301,28 @@ export default function FAQManager() {
|
||||
type="number"
|
||||
value={formData.order}
|
||||
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<label className="flex items-center justify-between p-3 bg-orange-50 rounded-xl border border-orange-200 cursor-pointer w-full">
|
||||
<span className="text-xs font-bold text-orange-700">✅ فعال و قابل نمایش</span>
|
||||
<label className="flex items-center justify-between p-3 bg-purple-50 rounded-xl border border-purple-200 cursor-pointer w-full">
|
||||
<span className="text-xs font-bold text-purple-700">✅ فعال و قابل نمایش</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||||
className="rounded text-orange-500 focus:ring-orange-400"
|
||||
className="rounded text-purple-600 focus:ring-purple-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
className="px-6 py-2.5 rounded-xl bg-orange-500 text-white font-bold hover:bg-orange-600 transition-colors shadow-md shadow-orange-200 flex items-center gap-2"
|
||||
>
|
||||
{isSaving && <Spinner size="sm" />}
|
||||
ذخیره
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{deleteTargetId && (
|
||||
<ConfirmModal
|
||||
isOpen={true}
|
||||
|
||||
@ -7,6 +7,10 @@ import { toast } from 'react-hot-toast';
|
||||
import api from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
@ -174,15 +178,17 @@ export default function MenuManager() {
|
||||
مدیریت آیتمهای هدر و فوتر سایت — تغییرات بلادرنگ اعمال میشوند
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => openModal()}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-blue-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
آیتم جدید
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{MENU_TABS.map((tab) => (
|
||||
@ -292,27 +298,45 @@ export default function MenuManager() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form Modal */}
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl w-full max-w-xl shadow-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'}
|
||||
</h3>
|
||||
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg">
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'}
|
||||
icon={Menu}
|
||||
maxWidth="xl"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="menuForm"
|
||||
isLoading={isSaving}
|
||||
>
|
||||
ذخیره
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="p-6 space-y-4">
|
||||
}
|
||||
>
|
||||
<div className="space-y-6 text-right font-vazir">
|
||||
<form id="menuForm" onSubmit={handleSave} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-1">موقعیت در منو</label>
|
||||
<select
|
||||
value={formData.menuType}
|
||||
onChange={(e) => setFormData({ ...formData, menuType: e.target.value as MenuType, parentId: null })}
|
||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm bg-white"
|
||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
|
||||
>
|
||||
{MENU_TABS.map((tab) => (
|
||||
<option key={tab.key} value={tab.key}>{tab.label}</option>
|
||||
@ -324,7 +348,7 @@ export default function MenuManager() {
|
||||
<select
|
||||
value={formData.parentId || ''}
|
||||
onChange={(e) => setFormData({ ...formData, parentId: e.target.value || null })}
|
||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm bg-white"
|
||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
|
||||
>
|
||||
<option value="">— آیتم اصلی —</option>
|
||||
{parentItems.map((p) => (
|
||||
@ -342,7 +366,7 @@ export default function MenuManager() {
|
||||
value={formData.label}
|
||||
onChange={(e) => setFormData({ ...formData, label: e.target.value })}
|
||||
placeholder="مثال: فروشگاه تخصصی"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -355,7 +379,7 @@ export default function MenuManager() {
|
||||
onChange={(e) => setFormData({ ...formData, href: e.target.value })}
|
||||
placeholder="مثال: /shop یا https://..."
|
||||
dir="ltr"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm font-mono"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -367,7 +391,7 @@ export default function MenuManager() {
|
||||
value={formData.icon}
|
||||
onChange={(e) => setFormData({ ...formData, icon: e.target.value })}
|
||||
placeholder="🐕 یا نام آیکون"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -377,7 +401,7 @@ export default function MenuManager() {
|
||||
value={formData.badge}
|
||||
onChange={(e) => setFormData({ ...formData, badge: e.target.value })}
|
||||
placeholder="مثال: جدید"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -389,7 +413,7 @@ export default function MenuManager() {
|
||||
type="number"
|
||||
value={formData.order}
|
||||
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center justify-between p-3 bg-green-50 rounded-xl border border-green-200 cursor-pointer col-span-1 mt-auto">
|
||||
@ -411,29 +435,12 @@ export default function MenuManager() {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
className="px-6 py-2.5 rounded-xl bg-blue-600 text-white font-bold hover:bg-blue-700 shadow-md shadow-blue-200 flex items-center gap-2"
|
||||
>
|
||||
{isSaving && <Spinner size="sm" />}
|
||||
ذخیره
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
|
||||
{deleteTargetId && (
|
||||
<ConfirmModal
|
||||
isOpen={true}
|
||||
|
||||
@ -15,6 +15,9 @@ import {
|
||||
import { toast } from 'react-hot-toast';
|
||||
import api from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import Button from '../components/ui/Button';
|
||||
|
||||
|
||||
|
||||
export default function PaymentGatewaysPage() {
|
||||
const [activeTab, setActiveTab] = useState<'gateways' | 'wallets' | 'ebank' | 'submerchants'>('gateways');
|
||||
@ -490,18 +493,20 @@ export default function PaymentGatewaysPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-indigo-200 cursor-pointer text-sm"
|
||||
variant="primary"
|
||||
size="md"
|
||||
startIcon={Save}
|
||||
isLoading={isSaving}
|
||||
>
|
||||
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
|
||||
<span>ذخیره کلیه تنظیمات درگاه زیبال</span>
|
||||
</button>
|
||||
ذخیره کلیه تنظیمات درگاه زیبال
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
|
||||
{activeTab === 'wallets' && (
|
||||
<div className="space-y-6">
|
||||
{/* Wallets Overview */}
|
||||
|
||||
@ -8,6 +8,9 @@ import Pagination from '../components/ui/Pagination';
|
||||
import MediaSelector from '../components/ui/MediaSelector';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
import PriceInput from '../components/ui/PriceInput';
|
||||
import Button from '../components/ui/Button';
|
||||
|
||||
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
@ -435,15 +438,17 @@ export default function Products() {
|
||||
</h2>
|
||||
<p className="text-gray-500 font-medium mt-1">افزودن و ویرایش پیشرفته محصولات با تنظیمات سئو</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => openModal()}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
محصول جدید
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Filters Bar */}
|
||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
@ -1650,18 +1655,29 @@ export default function Products() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-2.5 rounded-b-2xl">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button type="submit" form="productForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="productForm"
|
||||
>
|
||||
ذخیره محصول
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Media Selector Modal */}
|
||||
<MediaSelector
|
||||
isOpen={isMediaSelectorOpen}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { TrendingUp, DollarSign, ShoppingBag, Users, HeartHandshake, Download } from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
import Button from '../components/ui/Button';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
|
||||
|
||||
|
||||
import {
|
||||
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
|
||||
BarChart, Bar, PieChart, Pie, Cell, Legend
|
||||
@ -97,7 +101,10 @@ export default function Reports() {
|
||||
</h2>
|
||||
<p className="text-gray-500 font-medium mt-1">آمار فروش، رفتار کاربران و عملکرد تخفیفها</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
startIcon={Download}
|
||||
onClick={() => {
|
||||
if (!reportData) return;
|
||||
const rows = [
|
||||
@ -119,13 +126,12 @@ export default function Reports() {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}}
|
||||
className="bg-white border-2 border-purple-200 text-purple-700 hover:bg-purple-50 px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-sm"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
خروجی اکسل (Excel)
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Overview Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-gradient-to-br from-purple-500 to-indigo-600 p-6 rounded-2xl text-white shadow-lg shadow-purple-200">
|
||||
|
||||
@ -18,6 +18,10 @@ import {
|
||||
import { toast } from 'react-hot-toast';
|
||||
import api from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
|
||||
|
||||
interface ProductReview {
|
||||
id: string;
|
||||
@ -434,21 +438,34 @@ export default function Reviews() {
|
||||
|
||||
{/* Reply Modal */}
|
||||
{replyingReview && (
|
||||
<div className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl p-6 w-full max-w-lg shadow-2xl space-y-4 font-vazir animate-in fade-in zoom-in-95 duration-150" dir="rtl">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-3">
|
||||
<h3 className="text-base font-black text-gray-900 flex items-center gap-2">
|
||||
<Reply className="w-5 h-5 text-purple-600" />
|
||||
پاسخ به دیدگاه {replyingReview.userName}
|
||||
</h3>
|
||||
<button
|
||||
<Modal
|
||||
isOpen={!!replyingReview}
|
||||
onClose={() => setReplyingReview(null)}
|
||||
title={`پاسخ به دیدگاه ${replyingReview.userName}`}
|
||||
icon={Reply}
|
||||
maxWidth="lg"
|
||||
footer={
|
||||
<div className="flex items-center justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setReplyingReview(null)}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
isLoading={isSavingReply}
|
||||
onClick={handleSaveReply}
|
||||
>
|
||||
ثبت و انتشار پاسخ
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
}
|
||||
>
|
||||
<div className="space-y-4 font-vazir text-right">
|
||||
<div className="p-3 bg-gray-50 rounded-xl border border-gray-100 text-xs text-gray-600 space-y-1">
|
||||
<span className="font-bold text-gray-700 block">متن دیدگاه خریدار:</span>
|
||||
<p className="leading-relaxed whitespace-pre-wrap">{replyingReview.comment}</p>
|
||||
@ -466,28 +483,10 @@ export default function Reviews() {
|
||||
className="w-full p-3 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500 resize-none leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReplyingReview(null)}
|
||||
className="px-4 py-2.5 text-xs font-bold text-gray-600 hover:bg-gray-100 rounded-xl transition-all"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSavingReply}
|
||||
onClick={handleSaveReply}
|
||||
className="px-5 py-2.5 text-xs font-bold bg-purple-600 hover:bg-purple-700 text-white rounded-xl shadow-lg shadow-purple-600/20 transition-all flex items-center gap-1.5"
|
||||
>
|
||||
{isSavingReply && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
||||
<span>ثبت و انتشار پاسخ</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
interface TicketMessage {
|
||||
id: string;
|
||||
@ -340,47 +342,63 @@ export default function Tickets() {
|
||||
|
||||
{/* Ticket Details & Admin Reply Modal */}
|
||||
{selectedTicket && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-white w-full max-w-3xl rounded-3xl p-6 sm:p-8 shadow-2xl space-y-6 max-h-[90vh] flex flex-col" dir="rtl">
|
||||
{/* Modal Header */}
|
||||
<div className="flex items-start justify-between pb-4 border-b border-gray-100">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-black text-gray-900">{selectedTicket.subject}</h3>
|
||||
<span className="px-2.5 py-0.5 bg-canina-blue/10 text-canina-blue font-mono font-bold text-xs rounded-lg">
|
||||
{selectedTicket.ticketNumber}
|
||||
</span>
|
||||
<Modal
|
||||
isOpen={!!selectedTicket}
|
||||
onClose={() => setSelectedTicket(null)}
|
||||
title={`تیکت ${selectedTicket.ticketNumber}: ${selectedTicket.subject}`}
|
||||
icon={MessageSquare}
|
||||
maxWidth="3xl"
|
||||
footer={
|
||||
<div className="flex items-center justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setSelectedTicket(null)}
|
||||
>
|
||||
بستن
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="ticketReplyForm"
|
||||
startIcon={Send}
|
||||
isLoading={sendingReply}
|
||||
disabled={!replyMessage.trim()}
|
||||
>
|
||||
ارسال پاسخ به کاربر
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500 font-medium mt-1">
|
||||
}
|
||||
>
|
||||
<div className="space-y-5 text-right font-vazir">
|
||||
{/* Header meta */}
|
||||
<div className="p-3 bg-gray-50 rounded-2xl border border-gray-100 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="text-xs text-gray-600 font-medium">
|
||||
<span>کاربر: {selectedTicket.user?.firstName} {selectedTicket.user?.lastName} ({selectedTicket.user?.mobile})</span>
|
||||
{selectedTicket.pet && (
|
||||
<span className="text-purple-600 font-bold">• 🐾 {selectedTicket.pet.name} ({selectedTicket.pet.breed || selectedTicket.pet.type})</span>
|
||||
<span className="text-purple-600 font-bold block sm:inline sm:mr-2">🐾 {selectedTicket.pet.name} ({selectedTicket.pet.breed || selectedTicket.pet.type})</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-gray-500">تغییر وضعیت:</span>
|
||||
<select
|
||||
value={selectedTicket.status}
|
||||
onChange={(e) => handleUpdateStatus(e.target.value)}
|
||||
className="px-2.5 py-1.5 border border-gray-200 rounded-xl text-xs font-bold bg-gray-50 focus:border-canina-blue outline-none"
|
||||
className="px-2.5 py-1.5 border border-gray-200 rounded-xl text-xs font-bold bg-white focus:ring-2 focus:ring-purple-500 outline-none"
|
||||
>
|
||||
<option value="OPEN">در انتظار پاسخ</option>
|
||||
<option value="IN_PROGRESS">در حال بررسی</option>
|
||||
<option value="ANSWERED">پاسخ داده شد</option>
|
||||
<option value="CLOSED">بسته شده</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedTicket(null)}
|
||||
className="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-600 flex items-center justify-center font-bold text-sm cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages Chat Scroll */}
|
||||
<div className="flex-1 overflow-y-auto space-y-4 p-2 min-h-[250px]">
|
||||
<div className="overflow-y-auto space-y-3.5 p-1 max-h-[340px]">
|
||||
{(selectedTicket.messages || []).map((msg) => {
|
||||
const isAdminOrVet = msg.senderRole === 'ADMIN' || msg.senderRole === 'VET_DOCTOR';
|
||||
return (
|
||||
@ -388,7 +406,7 @@ export default function Tickets() {
|
||||
key={msg.id}
|
||||
className={`p-4 rounded-2xl max-w-[85%] text-xs font-medium leading-relaxed ${
|
||||
isAdminOrVet
|
||||
? 'bg-emerald-50 border border-emerald-200 text-emerald-950 mr-auto'
|
||||
? 'bg-purple-50 border border-purple-200 text-purple-950 mr-auto'
|
||||
: 'bg-blue-50 border border-blue-200 text-blue-950 ml-auto'
|
||||
}`}
|
||||
>
|
||||
@ -396,8 +414,8 @@ export default function Tickets() {
|
||||
<span className="font-black text-[11px] flex items-center gap-1.5">
|
||||
{isAdminOrVet ? (
|
||||
<>
|
||||
<Stethoscope className="w-3.5 h-3.5 text-emerald-600" />
|
||||
<span className="text-emerald-700">{msg.senderName} (تیم پزشکی / ادمین)</span>
|
||||
<Stethoscope className="w-3.5 h-3.5 text-purple-600" />
|
||||
<span className="text-purple-700">{msg.senderName} (تیم پزشکی / ادمین)</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -417,7 +435,7 @@ export default function Tickets() {
|
||||
</div>
|
||||
|
||||
{/* Admin Response Composer */}
|
||||
<form onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3">
|
||||
<form id="ticketReplyForm" onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<label className="font-bold text-gray-700">ارسال پاسخ دامپزشک / پشتیبانی:</label>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -439,29 +457,13 @@ export default function Tickets() {
|
||||
placeholder="متن پاسخ تخصصی به کاربر را وارد کنید..."
|
||||
value={replyMessage}
|
||||
onChange={(e) => setReplyMessage(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-medium focus:border-canina-blue outline-none"
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-medium focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none resize-none leading-relaxed"
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedTicket(null)}
|
||||
className="px-4 py-2 rounded-xl text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 cursor-pointer"
|
||||
>
|
||||
بستن
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={sendingReply || !replyMessage.trim()}
|
||||
className="px-6 py-2 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-dark disabled:opacity-50 transition-all flex items-center gap-2 cursor-pointer shadow-md shadow-canina-blue/20"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
<span>{sendingReply ? 'در حال ارسال...' : 'ارسال پاسخ به کاربر'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -265,13 +265,15 @@ export default function Users() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto">
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="w-full sm:w-auto flex items-center justify-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2.5 rounded-xl font-bold text-xs shadow-md shadow-purple-200 transition-all cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
افزودن کاربر جدید
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
|
||||
<select
|
||||
className="bg-white border border-gray-200 text-gray-700 px-4 py-2.5 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto text-xs"
|
||||
|
||||
@ -4,6 +4,9 @@ import { toast } from 'react-hot-toast';
|
||||
import api, { BASE_DOMAIN } from '../services/api';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
import MediaSelector from '../components/ui/MediaSelector';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
|
||||
interface Video {
|
||||
id: string;
|
||||
@ -179,15 +182,17 @@ export default function Videos() {
|
||||
آپلود و انتخاب مستقیم ویدئو/کاور از گالری رسانه یا درج کد آیفرم آپارات/یوتیوب برای مشاوره ویدئویی دامپزشکان کنینا
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => handleOpenModal()}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-3 rounded-xl font-black text-sm flex items-center gap-2 shadow-lg shadow-purple-600/20 transition-all hover:scale-105 shrink-0 cursor-pointer"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>افزودن ویدئوی جدید</span>
|
||||
</button>
|
||||
افزودن ویدئوی جدید
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-xs flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="w-5 h-5 text-gray-400 absolute right-4 top-1/2 -translate-y-1/2" />
|
||||
@ -310,22 +315,35 @@ export default function Videos() {
|
||||
)}
|
||||
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir">
|
||||
<div className="bg-white rounded-3xl max-w-2xl w-full p-6 sm:p-8 shadow-2xl border border-gray-100 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between pb-4 border-b border-gray-100 mb-6">
|
||||
<h3 className="text-lg font-black text-gray-900 flex items-center gap-2">
|
||||
<VideoIcon className="w-5 h-5 text-purple-600" />
|
||||
{editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'}
|
||||
</h3>
|
||||
<button
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'}
|
||||
icon={VideoIcon}
|
||||
maxWidth="2xl"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="videoForm"
|
||||
>
|
||||
ذخیره اطلاعات
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="space-y-5">
|
||||
}
|
||||
>
|
||||
<div className="space-y-5 text-right font-vazir">
|
||||
<form id="videoForm" onSubmit={handleSave} className="space-y-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-black text-gray-700 block">عنوان ویدئو *</label>
|
||||
<input
|
||||
@ -404,14 +422,15 @@ export default function Videos() {
|
||||
placeholder="آدرس تصویر یا انتخاب از گالری..."
|
||||
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-purple-500/20"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
startIcon={ImageIcon}
|
||||
onClick={() => setIsThumbnailMediaOpen(true)}
|
||||
className="px-4 py-3 bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-xl text-xs font-black flex items-center gap-1.5 border border-purple-200 shrink-0 cursor-pointer"
|
||||
>
|
||||
<ImageIcon className="w-4 h-4" />
|
||||
<span>انتخاب از گالری</span>
|
||||
</button>
|
||||
انتخاب کاور
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -437,14 +456,15 @@ export default function Videos() {
|
||||
placeholder="https://... یا کد آیفرم آپارات/یوتیوب"
|
||||
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-purple-500/20"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
startIcon={Film}
|
||||
onClick={() => setIsVideoMediaOpen(true)}
|
||||
className="px-4 py-3 bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-xl text-xs font-black flex items-center gap-1.5 border border-purple-200 shrink-0 cursor-pointer"
|
||||
>
|
||||
<Film className="w-4 h-4" />
|
||||
<span>انتخاب ویدئو</span>
|
||||
</button>
|
||||
انتخاب ویدئو
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -471,27 +491,12 @@ export default function Videos() {
|
||||
نمایش به عنوان ویدئوی ویژه در بخش «مشاوره ویدئویی» صفحه اصلی سایت
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold text-sm hover:bg-gray-50 cursor-pointer"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold text-sm hover:bg-purple-700 shadow-md cursor-pointer"
|
||||
>
|
||||
ذخیره اطلاعات
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
|
||||
{/* Cover Thumbnail Media Selector Modal */}
|
||||
<MediaSelector
|
||||
isOpen={isThumbnailMediaOpen}
|
||||
|
||||
@ -5,6 +5,9 @@ import api from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
|
||||
export interface WikiTerm {
|
||||
key: string;
|
||||
@ -138,13 +141,14 @@ export default function Wiki() {
|
||||
</h2>
|
||||
<p className="text-gray-500 font-medium mt-1">مدیریت اصطلاحات علمی، مواد تشکیلدهنده و خواص درمانی</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Plus}
|
||||
onClick={() => openModal()}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
مفهوم جدید
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
||||
@ -184,8 +188,8 @@ export default function Wiki() {
|
||||
<td className="py-4 px-6 text-gray-500 text-sm max-w-xs truncate">{t.definition}</td>
|
||||
<td className="py-4 px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => openModal(t)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"><Edit2 className="w-4 h-4" /></button>
|
||||
<button onClick={() => setDeleteTargetKey(t.key)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2 className="w-4 h-4" /></button>
|
||||
<button onClick={() => openModal(t)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"><Edit2 className="w-4 h-4" /></button>
|
||||
<button onClick={() => setDeleteTargetKey(t.key)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"><Trash2 className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -202,18 +206,34 @@ export default function Wiki() {
|
||||
</div>
|
||||
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[95vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
{editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
|
||||
</h3>
|
||||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||
<XCircle className="w-6 h-6" />
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
|
||||
icon={BookOpen}
|
||||
maxWidth="2xl"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="wikiForm"
|
||||
>
|
||||
ذخیره
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 overflow-y-auto flex-1">
|
||||
}
|
||||
>
|
||||
<div className="space-y-6 text-right font-vazir">
|
||||
<form id="wikiForm" onSubmit={handleSave} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
@ -231,9 +251,9 @@ export default function Wiki() {
|
||||
<textarea required value={formData.definition} onChange={(e) => setFormData({ ...formData, definition: e.target.value })} rows={5} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
|
||||
</div>
|
||||
|
||||
{/* Related Products Selector (TASK-2.23) */}
|
||||
{/* Related Products Selector */}
|
||||
<div className="space-y-2 border border-gray-200 p-4 rounded-xl bg-purple-50/40">
|
||||
<label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی (TASK-2.23)</label>
|
||||
<label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی</label>
|
||||
<select
|
||||
onChange={(e) => {
|
||||
const prodId = e.target.value;
|
||||
@ -258,7 +278,7 @@ export default function Wiki() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, relatedProducts: formData.relatedProducts.filter(id => id !== pId) })}
|
||||
className="hover:text-red-600 font-black text-sm"
|
||||
className="hover:text-red-600 font-black text-sm cursor-pointer"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@ -283,18 +303,12 @@ export default function Wiki() {
|
||||
<textarea rows={2} value={formData.metaDescription} onChange={(e) => setFormData({ ...formData, metaDescription: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">انصراف</button>
|
||||
<button type="submit" form="wikiForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">ذخیره</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={!!deleteTargetKey}
|
||||
title="حذف اصطلاح علمی"
|
||||
|
||||
@ -26,6 +26,9 @@ import {
|
||||
import { toast } from 'react-hot-toast';
|
||||
import api from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import Button from '../components/ui/Button';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
|
||||
export default function ZibalPortalPage() {
|
||||
const [activeMainTab, setActiveMainTab] = useState<'refunds' | 'checkouts' | 'queue' | 'gateway_transactions'>('refunds');
|
||||
@ -378,25 +381,27 @@ export default function ZibalPortalPage() {
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
{/* Top Bar with Filter & Search Controls */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Download}
|
||||
onClick={() => downloadCSV(refunds, 'zibal-refunds')}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-sm transition-colors cursor-pointer"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>دانلود گزارش</span>
|
||||
</button>
|
||||
دانلود گزارش
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={fetchRefunds}
|
||||
disabled={loadingRefunds}
|
||||
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
|
||||
isLoading={loadingRefunds}
|
||||
startIcon={RefreshCw}
|
||||
title="بروزرسانی"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loadingRefunds ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Filter Inputs Bar */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Date Filter */}
|
||||
@ -634,25 +639,27 @@ export default function ZibalPortalPage() {
|
||||
{activeMainTab === 'checkouts' && (
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Download}
|
||||
onClick={() => downloadCSV(checkouts, 'zibal-checkouts')}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-sm transition-colors cursor-pointer"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>دانلود گزارش</span>
|
||||
</button>
|
||||
دانلود گزارش
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={fetchCheckouts}
|
||||
disabled={loadingCheckouts}
|
||||
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
|
||||
isLoading={loadingCheckouts}
|
||||
startIcon={RefreshCw}
|
||||
title="بروزرسانی"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loadingCheckouts ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Filter Inputs Bar */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
|
||||
@ -851,25 +858,27 @@ export default function ZibalPortalPage() {
|
||||
{activeMainTab === 'gateway_transactions' && (
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={Download}
|
||||
onClick={() => downloadCSV(gatewayTxList, 'zibal-ipg-transactions')}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-sm transition-colors cursor-pointer"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>دانلود گزارش درگاه</span>
|
||||
</button>
|
||||
دانلود گزارش درگاه
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={fetchGatewayTx}
|
||||
disabled={loadingGatewayTx}
|
||||
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
|
||||
isLoading={loadingGatewayTx}
|
||||
startIcon={RefreshCw}
|
||||
title="بروزرسانی"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loadingGatewayTx ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Filter Inputs */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
|
||||
@ -1013,25 +1022,40 @@ export default function ZibalPortalPage() {
|
||||
)}
|
||||
|
||||
{/* ======================================================== */}
|
||||
{/* MODAL: SUBMIT NEW REFUND / REVERSE */}
|
||||
{/* MODAL: NEW REFUND SUBMIT */}
|
||||
{/* ======================================================== */}
|
||||
{newRefundModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-3xl w-full max-w-lg border border-gray-200 shadow-2xl p-6 sm:p-8 space-y-6">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
||||
<h3 className="text-base font-black text-gray-900 flex items-center gap-2">
|
||||
<RotateCcw className="w-5 h-5 text-rose-600" />
|
||||
ثبت درخواست استرداد وجه (Refund / Reverse)
|
||||
</h3>
|
||||
<button
|
||||
<Modal
|
||||
isOpen={newRefundModal}
|
||||
onClose={() => setNewRefundModal(false)}
|
||||
title="ثبت و ارسال استرداد وجه جدید (Refund / Reverse)"
|
||||
icon={RotateCcw}
|
||||
maxWidth="lg"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2.5 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setNewRefundModal(false)}
|
||||
className="text-gray-400 hover:text-gray-700 font-bold text-lg cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
type="submit"
|
||||
form="zibalRefundForm"
|
||||
startIcon={Send}
|
||||
isLoading={isSubmittingRefund}
|
||||
>
|
||||
ارسال به زیبال
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleCreateRefundSubmit} className="space-y-4 text-xs">
|
||||
}
|
||||
>
|
||||
<div className="space-y-4 text-xs font-vazir text-right">
|
||||
<form id="zibalRefundForm" onSubmit={handleCreateRefundSubmit} className="space-y-4 text-xs">
|
||||
<div>
|
||||
<label className="block font-bold text-gray-700 mb-1">شماره تراکنش شاپرک (Track ID) *</label>
|
||||
<input
|
||||
@ -1092,47 +1116,38 @@ export default function ZibalPortalPage() {
|
||||
اولویت با Reverse باشد (برگشت آنی قبل از تسویه شاپرک)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setNewRefundModal(false)}
|
||||
className="px-5 py-2.5 rounded-xl font-bold bg-gray-100 hover:bg-gray-200 text-gray-600 cursor-pointer"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmittingRefund}
|
||||
className="bg-rose-600 hover:bg-rose-700 text-white font-bold px-6 py-2.5 rounded-xl transition-colors flex items-center gap-2 cursor-pointer shadow-md shadow-rose-200"
|
||||
>
|
||||
{isSubmittingRefund ? <Spinner size="sm" /> : <Send className="w-4 h-4" />}
|
||||
<span>ارسال به زیبال</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* ======================================================== */}
|
||||
{/* MODAL: DETAIL / INQUIRY VIEWER */}
|
||||
{/* ======================================================== */}
|
||||
{detailModalItem && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-3xl w-full max-w-lg border border-gray-200 shadow-2xl p-6 space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-3">
|
||||
<h3 className="font-black text-sm text-gray-900">{detailModalItem.title}</h3>
|
||||
<button onClick={() => setDetailModalItem(null)} className="text-gray-400 font-bold text-lg">
|
||||
✕
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={!!detailModalItem}
|
||||
onClose={() => setDetailModalItem(null)}
|
||||
title={detailModalItem.title}
|
||||
maxWidth="lg"
|
||||
footer={
|
||||
<div className="flex justify-end w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setDetailModalItem(null)}
|
||||
>
|
||||
بستن
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<pre className="bg-slate-900 text-emerald-400 p-4 rounded-xl text-[11px] font-mono overflow-x-auto dir-ltr">
|
||||
{JSON.stringify(detailModalItem.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user