Compare commits

..

No commits in common. "1350267ffb4feef6e9fe1856ccbed7f956c86c10" and "32f8b8b6564988ff7588bea7d74ccd498aa68437" have entirely different histories.

34 changed files with 1632 additions and 1911 deletions

View File

@ -42,33 +42,32 @@ export default function Button({
const baseClasses = 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'; '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 - crisp, modern and compact // Size styling
const sizeClasses: Record<ButtonSize, string> = { const sizeClasses: Record<ButtonSize, string> = {
xs: 'px-2.5 py-1 text-[11px] gap-1.5', xs: 'px-2.5 py-1 text-[11px] gap-1.5',
sm: 'px-3 py-1.5 text-xs gap-1.5', sm: 'px-3.5 py-1.5 text-xs gap-2',
md: 'px-3.5 py-2 text-xs sm:text-sm gap-2', md: 'px-4 py-2.5 text-xs sm:text-sm gap-2',
lg: 'px-5 py-2.5 text-sm sm:text-base gap-2 shadow-sm', lg: 'px-6 py-3 text-sm sm:text-base gap-2.5 shadow-md',
}; };
// Variant styling // Variant styling
const variantClasses: Record<ButtonVariant, string> = { const variantClasses: Record<ButtonVariant, string> = {
primary: primary:
'bg-purple-600 hover:bg-purple-700 text-white shadow-xs focus:ring-purple-500 border border-purple-600', 'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm shadow-indigo-200 focus:ring-indigo-500 border border-indigo-600',
secondary: secondary:
'bg-slate-100 hover:bg-slate-200 text-slate-700 focus:ring-slate-400 border border-slate-200', 'bg-slate-100 hover:bg-slate-200 text-slate-700 focus:ring-slate-400 border border-slate-200',
danger: danger:
'bg-rose-600 hover:bg-rose-700 text-white shadow-xs focus:ring-rose-500 border border-rose-600', 'bg-rose-600 hover:bg-rose-700 text-white shadow-sm shadow-rose-200 focus:ring-rose-500 border border-rose-600',
success: success:
'bg-emerald-600 hover:bg-emerald-700 text-white shadow-xs focus:ring-emerald-500 border border-emerald-600', 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-sm shadow-emerald-200 focus:ring-emerald-500 border border-emerald-600',
warning: warning:
'bg-amber-500 hover:bg-amber-600 text-white shadow-xs focus:ring-amber-400 border border-amber-500', 'bg-amber-500 hover:bg-amber-600 text-white shadow-sm shadow-amber-200 focus:ring-amber-400 border border-amber-500',
outline: outline:
'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-purple-500', 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-indigo-500',
ghost: ghost:
'bg-transparent hover:bg-gray-100 text-gray-600 focus:ring-gray-300 border border-transparent', 'bg-transparent hover:bg-gray-100 text-gray-600 focus:ring-gray-300 border border-transparent',
}; };
const iconSizes: Record<ButtonSize, string> = { const iconSizes: Record<ButtonSize, string> = {
xs: 'w-3 h-3', xs: 'w-3 h-3',
sm: 'w-3.5 h-3.5', sm: 'w-3.5 h-3.5',

View File

@ -3,13 +3,8 @@ import { Building2, CheckCircle2, XCircle, Clock, PhoneCall, Filter, X, Plus, Sh
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { B2BInquiry, PartnerAccount } from '../types/admin'; import type { B2BInquiry, PartnerAccount } from '../types/admin';
export default function B2BManager() { export default function B2BManager() {
const [activeTab, setActiveTab] = useState<'inquiries' | 'partners'>('inquiries'); const [activeTab, setActiveTab] = useState<'inquiries' | 'partners'>('inquiries');
@ -282,13 +277,12 @@ export default function B2BManager() {
)} )}
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<Button <button
variant="primary"
size="xs"
onClick={() => openInquiryReview(inq)} onClick={() => openInquiryReview(inq)}
className="px-3 py-1.5 bg-purple-600 text-white hover:bg-purple-700 rounded-lg text-xs font-bold transition-all shadow-sm"
> >
تغییر وضعیت تغییر وضعیت
</Button> </button>
</td> </td>
</tr> </tr>
)) ))
@ -302,14 +296,13 @@ export default function B2BManager() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h3 className="font-bold text-gray-800">فهرست حساب‌های تایید شده همکار</h3> <h3 className="font-bold text-gray-800">فهرست حساب‌های تایید شده همکار</h3>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => setIsPartnerModalOpen(true)} onClick={() => setIsPartnerModalOpen(true)}
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-xl flex items-center gap-2 font-bold text-xs shadow-md shadow-purple-200"
> >
<Plus className="w-4 h-4" />
افزودن همکار B2B جدید افزودن همکار B2B جدید
</Button> </button>
</div> </div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
@ -367,41 +360,21 @@ export default function B2BManager() {
{/* Inquiry Review Modal */} {/* Inquiry Review Modal */}
{selectedInquiry && ( {selectedInquiry && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={!!selectedInquiry} <div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
onClose={() => setSelectedInquiry(null)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={`بررسی درخواست B2B (${selectedInquiry.companyName})`} <h3 className="text-lg font-bold text-gray-900">
icon={Building2} بررسی درخواست B2B ({selectedInquiry.companyName})
maxWidth="lg" </h3>
footer={ <button onClick={() => setSelectedInquiry(null)} className="text-gray-400 hover:text-red-500 transition-colors">
<div className="flex justify-end gap-2.5 w-full"> <X className="w-6 h-6" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setSelectedInquiry(null)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="inquiryForm"
isLoading={isInquirySubmitting}
>
بروزرسانی وضعیت
</Button>
</div> </div>
}
> <form onSubmit={handleInquiryReviewSubmit} className="p-6 space-y-4">
<div className="space-y-4 text-right font-vazir">
<form id="inquiryForm" onSubmit={handleInquiryReviewSubmit} className="space-y-4">
<div className="p-4 bg-purple-50/50 rounded-xl border border-purple-100 space-y-1 text-xs"> <div className="p-4 bg-purple-50/50 rounded-xl border border-purple-100 space-y-1 text-xs">
<div><span className="text-gray-500">رابط: </span><span className="font-bold text-gray-800">{selectedInquiry.contactName}</span></div> <div><span className="font-bold text-gray-600">شخص رابط: </span>{selectedInquiry.contactName} ({selectedInquiry.phone})</div>
<div><span className="text-gray-500">تلفن / ایمیل: </span><span className="font-mono text-gray-800">{selectedInquiry.phone} - {selectedInquiry.email}</span></div> <div><span className="font-bold text-gray-600">پیام درخواست: </span>{selectedInquiry.message}</div>
<div><span className="text-gray-500">نوع کسب‌وکار: </span><span className="font-bold text-purple-700">{selectedInquiry.businessType}</span></div>
{selectedInquiry.message && <div className="mt-2 text-gray-700 bg-white p-2.5 rounded-lg border border-gray-200">{selectedInquiry.message}</div>}
</div> </div>
<div> <div>
@ -428,43 +401,43 @@ export default function B2BManager() {
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 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>
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
<button
type="button"
onClick={() => setSelectedInquiry(null)}
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={isInquirySubmitting}
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"
>
{isInquirySubmitting && <Spinner size="sm" />}
بروزرسانی وضعیت
</button>
</div>
</form> </form>
</div> </div>
</Modal> </div>
)} )}
{/* Partner Account Setup Modal */} {/* Partner Account Setup Modal */}
{isPartnerModalOpen && ( {isPartnerModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isPartnerModalOpen} <div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
onClose={() => setIsPartnerModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title="تعریف حساب همکار B2B جدید" <h3 className="text-lg font-bold text-gray-900">
icon={Building2} تعریف حساب همکار B2B جدید
maxWidth="lg" </h3>
footer={ <button onClick={() => setIsPartnerModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<div className="flex justify-end gap-2.5 w-full"> <X className="w-6 h-6" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setIsPartnerModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="partnerForm"
isLoading={isPartnerSubmitting}
>
ایجاد حساب همکار
</Button>
</div> </div>
}
> <form onSubmit={handlePartnerSave} className="p-6 space-y-4">
<div className="space-y-4 text-right font-vazir">
<form id="partnerForm" onSubmit={handlePartnerSave} className="space-y-4">
<div> <div>
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه کاربر (User ID) *</label> <label className="block text-sm font-bold text-gray-700 mb-1">شناسه کاربر (User ID) *</label>
<input <input
@ -525,11 +498,28 @@ export default function B2BManager() {
<option value="tier3">سطح ۳ (۳۵٪ تخفیف)</option> <option value="tier3">سطح ۳ (۳۵٪ تخفیف)</option>
</select> </select>
</div> </div>
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
<button
type="button"
onClick={() => setIsPartnerModalOpen(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={isPartnerSubmitting}
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"
>
{isPartnerSubmitting && <Spinner size="sm" />}
ایجاد حساب همکار
</button>
</div>
</form> </form>
</div> </div>
</Modal> </div>
)} )}
</div> </div>
); );
} }

View File

@ -5,11 +5,8 @@ import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { Banner } from '../types/admin'; import type { Banner } from '../types/admin';
export default function BannersManager() { export default function BannersManager() {
const [banners, setBanners] = useState<Banner[]>([]); const [banners, setBanners] = useState<Banner[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -169,14 +166,13 @@ export default function BannersManager() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">مدیریت اسلایدر صفحه اصلی و تبلیغات بنری</p> <p className="text-gray-500 font-medium mt-1">مدیریت اسلایدر صفحه اصلی و تبلیغات بنری</p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} 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>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
@ -202,7 +198,7 @@ export default function BannersManager() {
</tr> </tr>
) : banners.length === 0 ? ( ) : banners.length === 0 ? (
<tr> <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> </tr>
) : ( ) : (
banners.map((banner, index) => ( banners.map((banner, index) => (
@ -211,79 +207,71 @@ export default function BannersManager() {
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
disabled={index === 0} disabled={index === 0}
onClick={() => handleReorder(index, 'up')} onClick={() => handleMoveOrder(index, 'up')}
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30 disabled:hover:text-gray-400 cursor-pointer" className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30"
> >
<MoveUp className="w-4 h-4" /> <MoveUp className="w-4 h-4" />
</button> </button>
<span className="font-mono text-sm font-bold text-gray-600 px-1">{index + 1}</span>
<button <button
disabled={index === banners.length - 1} disabled={index === banners.length - 1}
onClick={() => handleReorder(index, 'down')} onClick={() => handleMoveOrder(index, 'down')}
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30 disabled:hover:text-gray-400 cursor-pointer" className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30"
> >
<MoveDown className="w-4 h-4" /> <MoveDown className="w-4 h-4" />
</button> </button>
</div> </div>
</td> </td>
<td className="py-4 px-6"> <td className="py-3 px-6">
{banner.imageUrl ? ( <div className="w-20 h-12 rounded-xl bg-gray-100 border border-gray-200 overflow-hidden flex items-center justify-center">
<img {banner.imageUrl ? (
src={getImageFullUrl(banner.imageUrl)} <img src={getImageFullUrl(banner.imageUrl)} alt={banner.title} className="w-full h-full object-cover" />
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>
<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>
)}
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="font-bold text-gray-900">{banner.title || '-'}</div> <div className="flex flex-col">
<div className="text-xs text-gray-400 mt-0.5">{banner.subtitle || '-'}</div> <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>
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<span className="px-2.5 py-1 rounded-full text-xs font-bold bg-purple-50 text-purple-700"> <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_promo' ? 'بنر تبلیغاتی میانی' : banner.position} {banner.position === 'home_hero'
? 'اسلایدر هیرو اصلی'
: banner.position === 'home_middle'
? 'بنر میانی صفحه نخست'
: banner.position === 'home_bottom'
? 'بنر عریض پایین خانه'
: banner.position === 'shop_top'
? 'بالای فروشگاه و کاتالوگ'
: banner.position === 'category_top'
? 'بالای دسته‌بندی'
: 'سایدبار'}
</span> </span>
</td> </td>
<td className="py-4 px-6 text-sm text-gray-500 font-mono" dir="ltr"> <td className="py-4 px-6 text-gray-500 font-mono text-xs dir-ltr text-right">
{banner.linkUrl || '-'} {banner.linkUrl || '-'}
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<button {banner.isActive ? (
onClick={() => handleToggleActive(banner)} <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">
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold transition-colors cursor-pointer ${ <CheckCircle2 className="w-3.5 h-3.5" /> فعال
banner.isActive </span>
? 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100' ) : (
: 'bg-rose-50 text-rose-700 hover:bg-rose-100' <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>
{banner.isActive ? ( )}
<>
<CheckCircle2 className="w-3.5 h-3.5" />
فعال
</>
) : (
<>
<XCircle className="w-3.5 h-3.5" />
غیرفعال
</>
)}
</button>
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button onClick={() => openModal(banner)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
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" /> <Edit2 className="w-4 h-4" />
</button> </button>
<button <button onClick={() => setDeleteTargetId(banner.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
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" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
@ -296,122 +284,96 @@ export default function BannersManager() {
</div> </div>
</div> </div>
{/* Modal */} {/* Form Modal */}
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isModalOpen} <div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
onClose={() => setIsModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={editingBanner ? 'ویرایش بنر' : 'افزودن بنر جدید'} <h3 className="text-lg font-bold text-gray-900">
icon={ImageIcon} {editingBanner ? 'ویرایش بنر' : 'افزودن بنر جدید'}
maxWidth="2xl" </h3>
footer={ <button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<div className="flex justify-end gap-2.5 w-full"> <X className="w-6 h-6" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="bannerForm"
isLoading={isSaving}
>
ذخیره تغییرات
</Button>
</div> </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> <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 <input
required
type="text" type="text"
value={formData.title} value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })} 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" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/> />
</div> </div>
<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 <input
type="text" type="text"
value={formData.subtitle} value={formData.subtitle}
onChange={(e) => setFormData({ ...formData, subtitle: e.target.value })} 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" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/> />
</div> </div>
<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">آدرس تصویر (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> <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>
<select <select
value={formData.position} value={formData.position}
onChange={(e) => setFormData({ ...formData, position: e.target.value })} 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 bg-white" 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"
> >
<option value="home_hero">اسلایدر اصلی (Home Hero)</option> <option value="home_hero">اسلایدر هیرو اصلی (home_hero)</option>
<option value="home_promo">بنر تبلیغاتی میانی (Home Promo)</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>
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-bold text-gray-700 mb-1">لینک هدف (URL)</label> <label className="block text-sm font-bold text-gray-700 mb-1">ترتیب نمایش (order)</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 <input
type="number" type="number"
value={formData.order} value={formData.order}
@ -433,12 +395,29 @@ 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> <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> </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-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> </form>
</div> </div>
</Modal> </div>
)} )}
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{deleteTargetId && ( {deleteTargetId && (
<ConfirmModal <ConfirmModal

View File

@ -6,10 +6,6 @@ import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination'; import Pagination from '../components/ui/Pagination';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface BlogPost { export interface BlogPost {
author?: { author?: {
@ -138,14 +134,13 @@ export default function Blogs() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">مدیریت محتوای آموزشی و مقالات علمی سایت</p> <p className="text-gray-500 font-medium mt-1">مدیریت محتوای آموزشی و مقالات علمی سایت</p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} 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>
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4"> <div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
@ -201,8 +196,8 @@ export default function Blogs() {
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-2"> <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 cursor-pointer"><Edit2 className="w-4 h-4" /></button> <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 cursor-pointer"><Trash2 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>
</div> </div>
</td> </td>
</tr> </tr>
@ -219,99 +214,82 @@ export default function Blogs() {
</div> </div>
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isModalOpen} <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">
onClose={() => setIsModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'} <h3 className="text-xl font-bold text-gray-900">
icon={FileText} {editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
maxWidth="4xl" </h3>
footer={ <button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<div className="flex justify-end gap-2.5 w-full"> <XCircle className="w-6 h-6" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="blogForm"
>
ذخیره مقاله
</Button>
</div> </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">
<form id="blogForm" onSubmit={handleSave} className="space-y-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="space-y-2">
<label className="text-sm font-bold text-gray-700">عنوان مقاله *</label>
<input required type="text" value={formData.title} onChange={(e) => setFormData({ ...formData, title: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">اسلاگ (Slug) *</label>
<input required type="text" value={formData.slug} dir="ltr" onChange={(e) => setFormData({ ...formData, slug: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" />
</div>
</div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-bold text-gray-700">عنوان مقاله *</label> <label className="text-sm font-bold text-gray-700">محتوای اصلی مقاله *</label>
<input required type="text" value={formData.title} onChange={(e) => setFormData({ ...formData, title: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" /> <textarea required value={formData.content} onChange={(e) => setFormData({ ...formData, content: e.target.value })} rows={10} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" dir="rtl" placeholder="محتوای مقاله (متن ساده یا HTML)"></textarea>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">اسلاگ (Slug) *</label>
<input required type="text" value={formData.slug} dir="ltr" onChange={(e) => setFormData({ ...formData, slug: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" />
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">محتوای اصلی مقاله *</label>
<textarea required value={formData.content} onChange={(e) => setFormData({ ...formData, content: e.target.value })} rows={10} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" dir="rtl" placeholder="محتوای مقاله (متن ساده یا HTML)"></textarea>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<label className="text-sm font-bold text-gray-700">تصویر شاخص</label>
<div className="flex items-center gap-4">
{formData.imageUrl ? (
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `${BASE_DOMAIN}${formData.imageUrl}`} alt="preview" className="w-24 h-16 rounded-xl object-cover border border-gray-200" />
) : (
<div className="w-24 h-16 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
<ImageIcon className="w-6 h-6" />
</div>
)}
<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" />
<span className="font-bold text-gray-700">انتشار فوری در سایت</span>
</label>
</div> </div>
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4> <div className="space-y-4">
<div className="space-y-2"> <label className="text-sm font-bold text-gray-700">تصویر شاخص</label>
<label className="text-xs font-bold text-gray-700">عنوان متا</label> <div className="flex items-center gap-4">
<input type="text" value={formData.metaTitle} onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" /> {formData.imageUrl ? (
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `${BASE_DOMAIN}${formData.imageUrl}`} alt="preview" className="w-24 h-16 rounded-xl object-cover border border-gray-200" />
) : (
<div className="w-24 h-16 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
<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>
</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" />
<span className="font-bold text-gray-700">انتشار فوری در سایت</span>
</label>
</div> </div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">کلمات کلیدی</label> <div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
<input type="text" value={formData.keywords} onChange={(e) => setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" /> <h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
</div> <div className="space-y-2">
<div className="space-y-2"> <label className="text-xs font-bold text-gray-700">عنوان متا</label>
<label className="text-xs font-bold text-gray-700">توضیحات متا</label> <input type="text" value={formData.metaTitle} onChange={(e) => setFormData({ ...formData, metaTitle: 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 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 className="space-y-2">
<label className="text-xs font-bold text-gray-700">کلمات کلیدی</label>
<input type="text" value={formData.keywords} onChange={(e) => setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">توضیحات متا</label>
<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> </div>
</div> </div>
</div>
</form> </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> </div>
)} )}
<MediaSelector isOpen={isMediaSelectorOpen} onClose={() => setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({ ...formData, imageUrl: url })} /> <MediaSelector isOpen={isMediaSelectorOpen} onClose={() => setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({ ...formData, imageUrl: url })} />
<ConfirmModal <ConfirmModal

View File

@ -6,10 +6,6 @@ import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination'; import Pagination from '../components/ui/Pagination';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface Category { export interface Category {
id: string; id: string;
@ -131,17 +127,15 @@ export default function Categories() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">ایجاد دسته‌بندی محصولات و تنظیمات سئو (SEO)</p> <p className="text-gray-500 font-medium mt-1">ایجاد دسته‌بندی محصولات و تنظیمات سئو (SEO)</p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} 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>
{/* Filters */} {/* 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="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"> <div className="relative flex-1">
@ -203,10 +197,10 @@ export default function Categories() {
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-2"> <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 cursor-pointer"> <button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</button> </button>
<button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"> <button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
@ -226,120 +220,120 @@ export default function Categories() {
{/* Modal */} {/* Modal */}
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isModalOpen} <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">
onClose={() => setIsModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={editingCategory ? 'ویرایش دسته‌بندی' : 'افزودن دسته‌بندی جدید'} <h3 className="text-xl font-bold text-gray-900">
icon={FolderTree} {editingCategory ? 'ویرایش دسته‌بندی' : 'افزودن دسته‌بندی جدید'}
maxWidth="2xl" </h3>
footer={ <button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<div className="flex justify-end gap-2.5 w-full"> <X className="w-6 h-6" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="categoryForm"
>
ذخیره دسته‌بندی
</Button>
</div> </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">
<form id="categoryForm" onSubmit={handleSave} className="space-y-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">نام دسته‌بندی *</label>
<input
required
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">اسلاگ (Slug) *</label>
<input
required
type="text"
value={formData.slug}
dir="ltr"
onChange={(e) => setFormData({...formData, slug: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all font-mono"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">تصویر دسته‌بندی</label>
<div className="flex items-center gap-4">
{formData.imageUrl ? (
<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-5 h-5" />
</div>
)}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setIsMediaSelectorOpen(true)}
>
انتخاب از گالری
</Button>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیحات دسته‌بندی</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
rows={3}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
></textarea>
</div>
<div className="pt-4 border-t border-gray-100">
<h4 className="font-bold text-purple-700 mb-4">تنظیمات سئو (SEO)</h4>
<div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-bold text-gray-700">عنوان متا (Meta Title)</label> <label className="text-sm font-bold text-gray-700">نام دسته‌بندی *</label>
<input <input
required
type="text" type="text"
value={formData.metaTitle} value={formData.name}
onChange={(e) => setFormData({...formData, metaTitle: e.target.value})} onChange={(e) => setFormData({...formData, name: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all" className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیحات متا (Meta Description)</label> <label className="text-sm font-bold text-gray-700">اسلاگ (Slug) *</label>
<textarea <input
value={formData.metaDescription} required
onChange={(e) => setFormData({...formData, metaDescription: e.target.value})} type="text"
rows={2} value={formData.slug}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all" dir="ltr"
></textarea> onChange={(e) => setFormData({...formData, slug: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all font-mono"
/>
</div> </div>
</div> </div>
</div>
</form> <div className="space-y-2">
<label className="text-sm font-bold text-gray-700">تصویر دسته‌بندی</label>
<div className="flex items-center gap-4">
{formData.imageUrl ? (
<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" />
</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 transition-colors"
>
انتخاب از گالری
</button>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیحات دسته‌بندی</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
rows={3}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
></textarea>
</div>
<div className="pt-4 border-t border-gray-100">
<h4 className="font-bold text-purple-700 mb-4">تنظیمات سئو (SEO)</h4>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">عنوان متا (Meta Title)</label>
<input
type="text"
value={formData.metaTitle}
onChange={(e) => setFormData({...formData, metaTitle: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیحات متا (Meta Description)</label>
<textarea
value={formData.metaDescription}
onChange={(e) => setFormData({...formData, metaDescription: e.target.value})}
rows={2}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
></textarea>
</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="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> </div>
)} )}
{/* Media Selector Modal */} {/* Media Selector Modal */}
<MediaSelector <MediaSelector
isOpen={isMediaSelectorOpen} isOpen={isMediaSelectorOpen}
onClose={() => setIsMediaSelectorOpen(false)} onClose={() => setIsMediaSelectorOpen(false)}

View File

@ -1,10 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Mail, MessageSquare } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface ContactSubmission { interface ContactSubmission {
id: string; id: string;
@ -157,17 +153,16 @@ export default function ContactSubmissions() {
</span> </span>
</td> </td>
<td className="p-4"> <td className="p-4">
<Button <button
variant="secondary"
size="xs"
onClick={() => { onClick={() => {
setSelectedSub(sub); setSelectedSub(sub);
setStatus(sub.status); setStatus(sub.status);
setAdminNotes(sub.adminNotes || ''); setAdminNotes(sub.adminNotes || '');
}} }}
className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-800 rounded-lg text-xs font-medium transition"
> >
مشاهده & تغییر وضعیت مشاهده & تغییر وضعیت
</Button> </button>
</td> </td>
</tr> </tr>
))} ))}
@ -225,45 +220,21 @@ export default function ContactSubmissions() {
</div> </div>
</div> </div>
))} ))}
<Button <button
variant="primary"
size="sm"
onClick={handleSaveContactInfo} onClick={handleSaveContactInfo}
className="px-6 py-2.5 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-medium transition shadow-sm"
> >
ذخیره تغییرات اطلاعات نمایندگی ذخیره تغییرات اطلاعات نمایندگی
</Button> </button>
</div> </div>
</div> </div>
)} )}
{/* Modal for Submission Detail & Status Update */} {/* Modal for Submission Detail & Status Update */}
{selectedSub && ( {selectedSub && (
<Modal <div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4 z-50">
isOpen={!!selectedSub} <div className="bg-white rounded-2xl p-6 max-w-lg w-full shadow-xl">
onClose={() => setSelectedSub(null)} <h3 className="text-lg font-bold text-gray-900 mb-4">جزئیات پیام {selectedSub.name}</h3>
title={`جزئیات پیام ${selectedSub.name}`}
icon={Mail}
maxWidth="lg"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
onClick={() => setSelectedSub(null)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
onClick={handleUpdateStatus}
>
ثبت تغییرات
</Button>
</div>
}
>
<div className="space-y-4 text-right font-vazir">
<div className="space-y-3 text-sm text-gray-700 mb-6 bg-gray-50 p-4 rounded-xl"> <div className="space-y-3 text-sm text-gray-700 mb-6 bg-gray-50 p-4 rounded-xl">
<div><span className="font-semibold text-gray-900">تلفن:</span> {selectedSub.phone}</div> <div><span className="font-semibold text-gray-900">تلفن:</span> {selectedSub.phone}</div>
{selectedSub.email && <div><span className="font-semibold text-gray-900">ایمیل:</span> {selectedSub.email}</div>} {selectedSub.email && <div><span className="font-semibold text-gray-900">ایمیل:</span> {selectedSub.email}</div>}
@ -297,11 +268,24 @@ export default function ContactSubmissions() {
className="w-full px-3 py-2 border rounded-lg text-sm" className="w-full px-3 py-2 border rounded-lg text-sm"
/> />
</div> </div>
<div className="flex justify-end gap-2 pt-2">
<button
onClick={() => setSelectedSub(null)}
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm"
>
انصراف
</button>
<button
onClick={handleUpdateStatus}
className="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg text-sm font-medium"
>
ثبت تغییرات
</button>
</div>
</div> </div>
</div> </div>
</Modal> </div>
)} )}
</div> </div>
); );
} }

View File

@ -165,17 +165,12 @@ export default function Coupons() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">مدیریت کدهای تخفیف با اهداف چندگانه (کاربران، حیوانات، محصولات و ...)</p> <p className="text-gray-500 font-medium mt-1">مدیریت کدهای تخفیف با اهداف چندگانه (کاربران، حیوانات، محصولات و ...)</p>
</div> </div>
<Button <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">
variant="primary" <Plus className="w-5 h-5" />
size="sm"
startIcon={Plus}
onClick={() => openModal()}
>
کد تخفیف جدید کد تخفیف جدید
</Button> </button>
</div> </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="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"> <div className="relative flex-1">
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" /> <Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />

View File

@ -5,9 +5,6 @@ import api, { BASE_DOMAIN } from '../services/api';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface Doctor { export interface Doctor {
id: string; id: string;
@ -168,14 +165,13 @@ export default function DoctorsManager() {
ثبت اطلاعات و رزومه (CV) دامپزشکان جهت انتساب به ویدئوها، مشاوره‌ها و معرفی کادر تخصصی ثبت اطلاعات و رزومه (CV) دامپزشکان جهت انتساب به ویدئوها، مشاوره‌ها و معرفی کادر تخصصی
</p> </p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => handleOpenModal()} onClick={() => handleOpenModal()}
className="flex items-center justify-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-5 py-3 rounded-2xl font-bold transition-all shadow-md shadow-purple-200 cursor-pointer text-sm"
> >
افزودن پزشک جدید <Plus className="w-5 h-5" />
</Button> <span>افزودن پزشک جدید</span>
</button>
</div> </div>
{/* Filter / Search */} {/* Filter / Search */}
@ -207,14 +203,13 @@ export default function DoctorsManager() {
</div> </div>
<h3 className="text-lg font-bold text-gray-800 mb-1">پزشکی یافت نشد</h3> <h3 className="text-lg font-bold text-gray-800 mb-1">پزشکی یافت نشد</h3>
<p className="text-sm text-gray-400 mb-6">اولین پزشک یا متخصص کادر علمی را به سامانه اضافه کنید.</p> <p className="text-sm text-gray-400 mb-6">اولین پزشک یا متخصص کادر علمی را به سامانه اضافه کنید.</p>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => handleOpenModal()} onClick={() => handleOpenModal()}
className="inline-flex items-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl font-bold text-sm shadow-sm"
> >
<Plus className="w-4 h-4" />
افزودن اولین پزشک افزودن اولین پزشک
</Button> </button>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@ -233,23 +228,23 @@ export default function DoctorsManager() {
<button <button
onClick={() => handleOpenModal(doc)} onClick={() => handleOpenModal(doc)}
className="p-2 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-xl transition-colors cursor-pointer" className="p-2 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-xl transition-colors cursor-pointer"
title="ویرایش اطلاعات" title="ویرایش"
> >
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => setDeleteTargetId(doc.id)} onClick={() => setDeleteTargetId(doc.id)}
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer" className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
title="حذف پزشک" title="حذف"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
</div> </div>
{/* Avatar & Main Info */} {/* Profile Header */}
<div className="flex items-center gap-4 mb-4"> <div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 rounded-2xl bg-purple-50 border border-purple-100 overflow-hidden flex items-center justify-center shrink-0"> <div className="w-16 h-16 rounded-2xl bg-purple-50 border border-purple-100 flex items-center justify-center overflow-hidden flex-shrink-0 relative">
{doc.avatarUrl ? ( {doc.avatarUrl ? (
<img <img
src={doc.avatarUrl.startsWith('http') ? doc.avatarUrl : `${BASE_DOMAIN}${doc.avatarUrl}`} src={doc.avatarUrl.startsWith('http') ? doc.avatarUrl : `${BASE_DOMAIN}${doc.avatarUrl}`}
@ -257,228 +252,199 @@ export default function DoctorsManager() {
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
<UserCheck className="w-8 h-8 text-purple-400" /> <UserCheck className="w-7 h-7 text-purple-400" />
)} )}
</div> </div>
<div> <div>
<h4 className="font-extrabold text-gray-900 text-base">{doc.name}</h4> <h3 className="font-extrabold text-gray-900 text-base leading-tight mb-1">{doc.name}</h3>
<p className="text-xs text-purple-600 font-bold mt-0.5">{doc.title || 'پزشک متخصص'}</p> <p className="text-xs text-purple-700 font-bold">{doc.title || 'دامپزشک'}</p>
{doc.clinicName && ( {doc.clinicName && (
<p className="text-[11px] text-gray-400 mt-0.5 flex items-center gap-1"> <p className="text-xs text-gray-400 font-medium mt-0.5">{doc.clinicName}</p>
<ShieldCheck className="w-3 h-3 text-gray-400" />
{doc.clinicName}
</p>
)} )}
</div> </div>
</div> </div>
{/* Bio Summary */} {/* Bio snippet */}
{doc.bio && ( {doc.bio && (
<p className="text-xs text-gray-500 line-clamp-2 leading-relaxed mb-4 bg-gray-50/50 p-2.5 rounded-xl border border-gray-100"> <p className="text-xs text-gray-500 leading-relaxed line-clamp-2 mb-4 bg-gray-50/70 p-3 rounded-2xl border border-gray-100">
{doc.bio} {doc.bio}
</p> </p>
)} )}
{/* Contact & CV Trigger */} {/* CV Button */}
<div className="flex items-center gap-2 pt-2 border-t border-gray-50 text-xs text-gray-400"> {doc.cv && (
{doc.phone && ( <button
<span className="flex items-center gap-1 bg-gray-50 px-2 py-1 rounded-lg"> onClick={() => setIsCvDetailsOpen(doc)}
<Phone className="w-3 h-3 text-purple-500" /> className="w-full mb-4 py-2 px-3 bg-purple-50 hover:bg-purple-100 text-purple-700 rounded-xl text-xs font-extrabold flex items-center justify-center gap-1.5 transition-colors cursor-pointer"
<span className="font-mono text-[11px]">{doc.phone}</span> >
</span> <FileText className="w-3.5 h-3.5" />
)} <span>مشاهده رزومه و سوابق علمی (CV)</span>
{doc.instagram && ( </button>
<a )}
href={`https://instagram.com/${doc.instagram.replace('@', '')}`} </div>
target="_blank"
rel="noreferrer" {/* Footer info */}
className="flex items-center gap-1 bg-pink-50 text-pink-600 hover:bg-pink-100 px-2 py-1 rounded-lg transition-colors" <div className="pt-4 border-t border-gray-100 flex items-center justify-between text-xs text-gray-400 font-medium">
> <span>{doc._count?.videos || 0} ویدئوی آموزشی</span>
<Globe className="w-3 h-3" /> {doc.phone && <span className="font-mono">{doc.phone}</span>}
<span className="font-mono text-[11px]">{doc.instagram}</span>
</a>
)}
{doc.cv && (
<button
onClick={() => setIsCvDetailsOpen(doc)}
className="mr-auto flex items-center gap-1 text-purple-600 hover:text-purple-700 bg-purple-50 hover:bg-purple-100 px-2.5 py-1 rounded-lg font-bold transition-colors cursor-pointer text-[11px]"
>
<FileText className="w-3 h-3" />
مشاهده رزومه
</button>
)}
</div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
)} )}
{/* Modal Add/Edit */} {/* Create / Edit Modal */}
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs">
isOpen={isModalOpen} <div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden border border-gray-100 animate-in fade-in zoom-in-95 duration-200">
onClose={() => setIsModalOpen(false)} {/* Modal Header */}
title={editingDoctor ? 'ویرایش اطلاعات پزشک' : 'افزودن پزشک جدید به کادر علمی'} <div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50">
icon={UserCheck} <div className="flex items-center gap-3">
maxWidth="2xl" <div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
footer={ <UserCheck className="w-5 h-5" />
<div className="flex justify-end gap-2.5 w-full"> </div>
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="doctorForm"
>
{editingDoctor ? 'ذخیره تغییرات' : 'افزودن پزشک'}
</Button>
</div>
}
>
<div className="space-y-6 text-right font-vazir">
<form id="doctorForm" onSubmit={handleSave} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1.5">نام و نام خانوادگی *</label> <h3 className="text-lg font-bold text-gray-900">
{editingDoctor ? 'ویرایش اطلاعات پزشک' : 'افزودن پزشک جدید'}
</h3>
<p className="text-xs text-gray-500 font-medium">مشخصات هویتی، آواتار و رزومه علمی پزشک</p>
</div>
</div>
<button
onClick={() => setIsModalOpen(false)}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Form */}
<form onSubmit={handleSave} className="p-6 overflow-y-auto flex-1 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Name */}
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">
نام و نام خانوادگی پزشک <span className="text-red-500">*</span>
</label>
<input <input
type="text" type="text"
required required
placeholder="مثال: دکتر سارا صادقی" placeholder="مثال: دکتر کلاوس هنینگ"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 font-bold" className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
/> />
</div> </div>
{/* Title */}
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1.5">عنوان / تخصص *</label> <label className="block text-xs font-bold text-gray-700 mb-1">تخصص یا عنوان علمی</label>
<input <input
type="text" type="text"
required placeholder="مثال: متخصص ارتوپدی و تغذیه دامپزشکی"
placeholder="مثال: متخصص تغذیه و فیزیولوژی حیوانات خانگی"
value={formData.title} value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })} onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 font-bold" className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {/* Clinic Name */}
<div>
<label className="block text-xs font-bold text-gray-700 mb-1.5">نام کلینیک / بیمارستان</label>
<input
type="text"
placeholder="مثال: بیمارستان دامپزشکی تهران"
value={formData.clinicName}
onChange={(e) => setFormData({ ...formData, clinicName: e.target.value })}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-700 mb-1.5">ترتیب نمایش در سایت</label>
<input
type="number"
value={formData.order}
onChange={(e) => setFormData({ ...formData, order: parseInt(e.target.value) || 0 })}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 font-bold"
/>
</div>
</div>
{/* Avatar Picker */}
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1.5">تصویر آواتار / عکس پرتره</label> <label className="block text-xs font-bold text-gray-700 mb-1">نام بیمارستان / کلینیک یا دانشگاه</label>
<input
type="text"
placeholder="مثال: کلینیک تخصصی دامپزشکی برلین"
value={formData.clinicName}
onChange={(e) => setFormData({ ...formData, clinicName: e.target.value })}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
/>
</div>
{/* Avatar Selector */}
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">تصویر پرتره / آواتار</label>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gray-100 border border-gray-200 overflow-hidden flex items-center justify-center shrink-0">
{formData.avatarUrl ? (
<img
src={formData.avatarUrl.startsWith('http') ? formData.avatarUrl : `${BASE_DOMAIN}${formData.avatarUrl}`}
alt="Avatar preview"
className="w-full h-full object-cover"
/>
) : (
<ImageIcon className="w-5 h-5 text-gray-400" />
)}
</div>
<input <input
type="text" type="text"
placeholder="آدرس تصویر یا انتخاب از رسانه‌ها..." placeholder="https://..."
value={formData.avatarUrl} value={formData.avatarUrl}
onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })} onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })}
className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono" className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
dir="ltr"
/> />
<Button <button
type="button" type="button"
variant="outline"
size="sm"
onClick={() => setIsAvatarMediaOpen(true)} onClick={() => setIsAvatarMediaOpen(true)}
className="px-4 py-2.5 bg-purple-100 hover:bg-purple-200 text-purple-700 rounded-xl text-xs font-bold flex items-center gap-1.5 transition-colors cursor-pointer flex-shrink-0"
> >
گالری رسانه <ImageIcon className="w-4 h-4" />
</Button> <span>انتخاب از رسانه‌ها</span>
</button>
</div> </div>
{formData.avatarUrl && (
<div className="mt-2 w-16 h-16 rounded-xl border border-gray-200 overflow-hidden bg-gray-50">
<img
src={formData.avatarUrl.startsWith('http') ? formData.avatarUrl : `${BASE_DOMAIN}${formData.avatarUrl}`}
alt="Preview"
className="w-full h-full object-cover"
/>
</div>
)}
</div> </div>
{/* Bio & CV */} {/* Bio */}
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1.5">معرفی کوتاه (Bio)</label> <label className="block text-xs font-bold text-gray-700 mb-1">بیوگرافی کوتاه (معرفی سریع)</label>
<textarea <textarea
rows={2} rows={2}
placeholder="خلاصه‌ای از سوابق و حوزه کاری..." placeholder="معرفی کوتاه پزشک و فعالیت‌ها..."
value={formData.bio} value={formData.bio}
onChange={(e) => setFormData({ ...formData, bio: e.target.value })} onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
className="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 leading-relaxed" className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
/> />
</div> </div>
{/* CV */}
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1.5">رزومه کامل و مقالات علمی (CV)</label> <label className="block text-xs font-bold text-gray-700 mb-1">
رزومه، سوابق تحصیلی و مقالات علمی (CV)
</label>
<textarea <textarea
rows={4} rows={4}
placeholder="سوابق تحصیلی، مقالات چاپ‌شده، مدارک بین‌المللی و تجربیات کلینیکی..." placeholder="سوابق تحصیلی، دوره‌های تخصصی، مقالات منتشر شده، تخصص‌های درمانی و..."
value={formData.cv} value={formData.cv}
onChange={(e) => setFormData({ ...formData, cv: e.target.value })} onChange={(e) => setFormData({ ...formData, cv: e.target.value })}
className="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 leading-relaxed font-sans" className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600 leading-relaxed"
/> />
</div> </div>
{/* Social & Contact */} {/* Contacts */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1">تلفن تماس کلینیک</label> <label className="block text-xs font-bold text-gray-700 mb-1">تلفن تماس / واتساپ</label>
<input <input
type="text" type="text"
dir="ltr" placeholder="۰۹۱۲..."
placeholder="02188888888"
value={formData.phone} value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })} onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono" className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1">ایمیل</label> <label className="block text-xs font-bold text-gray-700 mb-1">ایمیل</label>
<input <input
type="email" type="email"
dir="ltr" placeholder="doctor@example.com"
placeholder="dr.sadeghi@canina.ir"
value={formData.email} value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono" className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1">آیدی اینستاگرام</label> <label className="block text-xs font-bold text-gray-700 mb-1">اینستاگرام</label>
<input <input
type="text" type="text"
dir="ltr" placeholder="@dr_..."
placeholder="@dr.sadeghi"
value={formData.instagram} value={formData.instagram}
onChange={(e) => setFormData({ ...formData, instagram: e.target.value })} onChange={(e) => setFormData({ ...formData, instagram: e.target.value })}
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono" className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
@ -498,38 +464,56 @@ export default function DoctorsManager() {
<span className="text-xs font-bold text-gray-700">پزشک فعال است و در لیست نمایش داده شود</span> <span className="text-xs font-bold text-gray-700">پزشک فعال است و در لیست نمایش داده شود</span>
</label> </label>
</div> </div>
{/* Actions */}
<div className="flex justify-end gap-3 pt-4 border-t border-gray-100">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-5 py-2.5 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-xl text-xs font-bold transition-colors cursor-pointer"
>
انصراف
</button>
<button
type="submit"
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-bold shadow-md shadow-purple-200 transition-colors cursor-pointer"
>
{editingDoctor ? 'ذخیره تغییرات' : 'افزودن پزشک'}
</button>
</div>
</form> </form>
</div> </div>
</Modal> </div>
)} )}
{/* CV Detail Modal */} {/* CV Detail Modal */}
{isCvDetailsOpen && ( {isCvDetailsOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs">
isOpen={!!isCvDetailsOpen} <div className="bg-white rounded-3xl max-w-xl w-full p-6 shadow-2xl border border-gray-100 animate-in fade-in zoom-in-95">
onClose={() => setIsCvDetailsOpen(null)} <div className="flex items-center justify-between border-b border-gray-100 pb-4 mb-4">
title={`رزومه علمی — ${isCvDetailsOpen.name}`} <div className="flex items-center gap-3">
icon={FileText} <div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
maxWidth="lg" <FileText className="w-5 h-5" />
footer={ </div>
<div className="flex justify-end w-full"> <div>
<Button <h3 className="font-extrabold text-gray-900 text-base">{isCvDetailsOpen.name}</h3>
variant="secondary" <p className="text-xs text-purple-600 font-bold">{isCvDetailsOpen.title}</p>
size="sm" </div>
</div>
<button
onClick={() => setIsCvDetailsOpen(null)} onClick={() => setIsCvDetailsOpen(null)}
className="p-2 text-gray-400 hover:text-red-500 rounded-xl transition-colors cursor-pointer"
> >
بستن <X className="w-5 h-5" />
</Button> </button>
</div>
<div className="bg-gray-50 p-4 rounded-2xl max-h-96 overflow-y-auto whitespace-pre-wrap text-xs text-gray-700 leading-relaxed font-sans">
{isCvDetailsOpen.cv}
</div> </div>
}
>
<div className="bg-gray-50 p-4 rounded-2xl max-h-96 overflow-y-auto whitespace-pre-wrap text-xs text-gray-700 leading-relaxed font-sans text-right font-vazir">
{isCvDetailsOpen.cv}
</div> </div>
</Modal> </div>
)} )}
{/* Media Selector Modal for Avatar */} {/* Media Selector Modal for Avatar */}
<MediaSelector <MediaSelector
isOpen={isAvatarMediaOpen} isOpen={isAvatarMediaOpen}

View File

@ -4,9 +4,6 @@ import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface FAQ { interface FAQ {
id: string; id: string;
@ -132,14 +129,13 @@ export default function FAQManager() {
سوالات و پاسخ‌هایی که در صفحه اصلی سایت نمایش داده می‌شوند — با Schema.org FAQPage برای SEO سوالات و پاسخ‌هایی که در صفحه اصلی سایت نمایش داده می‌شوند — با Schema.org FAQPage برای SEO
</p> </p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} 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> </div>
{/* Info Box */} {/* Info Box */}
@ -151,7 +147,7 @@ export default function FAQManager() {
<div className="space-y-3"> <div className="space-y-3">
{isLoading ? ( {isLoading ? (
<div className="flex justify-center py-12"> <div className="flex justify-center py-12">
<Spinner size="lg" className="text-purple-600" /> <Spinner size="lg" className="text-orange-500" />
</div> </div>
) : faqs.length === 0 ? ( ) : faqs.length === 0 ? (
<div className="bg-white rounded-2xl border border-gray-200 py-16 text-center"> <div className="bg-white rounded-2xl border border-gray-200 py-16 text-center">
@ -159,71 +155,56 @@ export default function FAQManager() {
<p className="text-gray-500 font-medium">هیچ سوالی ثبت نشده است</p> <p className="text-gray-500 font-medium">هیچ سوالی ثبت نشده است</p>
<button <button
onClick={() => openModal()} onClick={() => openModal()}
className="mt-4 text-purple-600 font-bold hover:underline text-sm cursor-pointer" className="mt-4 text-orange-500 font-bold hover:underline text-sm"
> >
اولین سوال را اضافه کنید اولین سوال را اضافه کنید
</button> </button>
</div> </div>
) : ( ) : (
faqs.map((faq) => ( faqs.map((faq, idx) => (
<div <div
key={faq.id} key={faq.id}
className={`bg-white rounded-2xl border p-5 transition-all ${ className={`bg-white rounded-2xl border p-5 transition-all ${
faq.isActive ? 'border-gray-200 shadow-sm' : 'border-gray-100 opacity-60 bg-gray-50' faq.isActive ? 'border-gray-200' : 'border-dashed border-gray-300 opacity-60'
}`} }`}
> >
<div className="flex items-start justify-between gap-4"> <div className="flex items-start gap-4">
<div className="flex items-start gap-3 flex-1"> <div className="flex items-center gap-2 shrink-0 mt-1">
<div className="p-2 bg-orange-50 text-orange-600 rounded-xl mt-0.5 shrink-0"> <GripVertical className="w-4 h-4 text-gray-300" />
<HelpCircle className="w-5 h-5" /> <span className="text-xs font-black text-gray-400 w-5 text-center">{idx + 1}</span>
</div>
<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>
<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> </div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 shrink-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">
غیرفعال
</span>
)}
</div>
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2">{faq.answer}</p>
</div>
<div className="flex items-center gap-1 shrink-0">
<button <button
onClick={() => toggleActive(faq)} onClick={() => toggleActive(faq)}
title={faq.isActive ? 'غیرفعال‌سازی' : 'فعال‌سازی'} className={`p-2 rounded-lg transition-colors ${
className={`p-2 rounded-xl transition-colors cursor-pointer ${
faq.isActive faq.isActive
? 'text-green-600 hover:bg-green-50' ? 'text-green-600 hover:bg-green-50'
: 'text-gray-400 hover:bg-gray-100' : 'text-gray-400 hover:bg-gray-50'
}`} }`}
title={faq.isActive ? 'غیرفعال کردن' : 'فعال کردن'}
> >
{faq.isActive ? ( {faq.isActive ? <ToggleRight className="w-5 h-5" /> : <ToggleLeft className="w-5 h-5" />}
<ToggleRight className="w-5 h-5" />
) : (
<ToggleLeft className="w-5 h-5" />
)}
</button> </button>
<button <button
onClick={() => openModal(faq)} onClick={() => openModal(faq)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-xl transition-colors cursor-pointer" className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
> >
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => setDeleteTargetId(faq.id)} onClick={() => setDeleteTargetId(faq.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer" className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
@ -234,49 +215,31 @@ export default function FAQManager() {
)} )}
</div> </div>
{/* Modal */} {/* Form Modal */}
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isModalOpen} <div className="bg-white rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden">
onClose={() => setIsModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'} <h3 className="text-lg font-bold text-gray-900">
icon={HelpCircle} {editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'}
maxWidth="2xl" </h3>
footer={ <button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg">
<div className="flex justify-end gap-2.5 w-full"> <X className="w-5 h-5 text-gray-500" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="faqForm"
isLoading={isSaving}
>
ذخیره سوال
</Button>
</div> </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> <div>
<label className="block text-sm font-bold text-gray-700 mb-1"> <label className="block text-sm font-bold text-gray-700 mb-1">
سوال <span className="text-red-500">*</span> سوال <span className="text-red-500">*</span>
</label> </label>
<input <input
required
type="text" type="text"
required
value={formData.question} value={formData.question}
onChange={(e) => setFormData({ ...formData, question: e.target.value })} onChange={(e) => setFormData({ ...formData, question: 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" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
/> />
</div> </div>
@ -290,7 +253,7 @@ export default function FAQManager() {
value={formData.answer} value={formData.answer}
onChange={(e) => setFormData({ ...formData, answer: e.target.value })} onChange={(e) => setFormData({ ...formData, answer: 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 resize-none" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm resize-none"
/> />
</div> </div>
@ -301,28 +264,44 @@ export default function FAQManager() {
type="number" type="number"
value={formData.order} value={formData.order}
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })} 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-purple-500 outline-none text-sm" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
/> />
</div> </div>
<div className="flex items-end"> <div className="flex items-end">
<label className="flex items-center justify-between p-3 bg-purple-50 rounded-xl border border-purple-200 cursor-pointer w-full"> <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-purple-700">✅ فعال و قابل نمایش</span> <span className="text-xs font-bold text-orange-700">✅ فعال و قابل نمایش</span>
<input <input
type="checkbox" type="checkbox"
checked={formData.isActive} checked={formData.isActive}
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })} onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
className="rounded text-purple-600 focus:ring-purple-500" className="rounded text-orange-500 focus:ring-orange-400"
/> />
</label> </label>
</div> </div>
</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> </form>
</div> </div>
</Modal> </div>
)} )}
{deleteTargetId && ( {deleteTargetId && (
<ConfirmModal <ConfirmModal
isOpen={true} isOpen={true}

View File

@ -4,10 +4,8 @@ import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import PriceInput from '../components/ui/PriceInput'; import PriceInput from '../components/ui/PriceInput';
import Button from '../components/ui/Button';
import type { FinancialSettings } from '../types/admin'; import type { FinancialSettings } from '../types/admin';
export default function FinancialSettingsPage() { export default function FinancialSettingsPage() {
const [financial, setFinancial] = useState<FinancialSettings>({ const [financial, setFinancial] = useState<FinancialSettings>({
taxPercentage: 10, taxPercentage: 10,
@ -171,15 +169,13 @@ export default function FinancialSettingsPage() {
onChange={(val) => setDonationInput(val)} onChange={(val) => setDonationInput(val)}
className="px-4 py-2 rounded-xl border border-gray-200 text-xs" className="px-4 py-2 rounded-xl border border-gray-200 text-xs"
/> />
<Button <button
type="button" type="button"
variant="primary"
size="xs"
startIcon={Plus}
onClick={addDonationOption} onClick={addDonationOption}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-bold flex items-center gap-1 shrink-0 cursor-pointer"
> >
افزودن <Plus className="w-4 h-4" /> افزودن
</Button> </button>
</div> </div>
<div className="flex flex-wrap gap-2 pt-2"> <div className="flex flex-wrap gap-2 pt-2">
@ -270,19 +266,17 @@ export default function FinancialSettingsPage() {
</div> </div>
<div className="flex justify-end pt-4 border-t border-gray-100"> <div className="flex justify-end pt-4 border-t border-gray-100">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isSaving}
size="md" className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-emerald-200 cursor-pointer"
startIcon={Save}
isLoading={isSaving}
> >
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
ذخیره تنظیمات مالی ذخیره تنظیمات مالی
</Button> </button>
</div> </div>
</form> </form>
)} )}
</div> </div>
); );
} }

View File

@ -5,11 +5,8 @@ import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { Ingredient } from '../types/admin'; import type { Ingredient } from '../types/admin';
export default function IngredientsManager() { export default function IngredientsManager() {
const [ingredients, setIngredients] = useState<Ingredient[]>([]); const [ingredients, setIngredients] = useState<Ingredient[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -167,17 +164,15 @@ export default function IngredientsManager() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">مدیریت مواد موثره، ترکیبات دارویی و خواص درمانی محصولات کنینا</p> <p className="text-gray-500 font-medium mt-1">مدیریت مواد موثره، ترکیبات دارویی و خواص درمانی محصولات کنینا</p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} 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>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-right"> <table className="w-full text-right">
@ -249,39 +244,21 @@ export default function IngredientsManager() {
</div> </div>
</div> </div>
{/* Modal */} {/* Form Modal */}
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isModalOpen} <div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200 max-h-[90vh] flex flex-col">
onClose={() => setIsModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={editingIngredient ? 'ویرایش ترکیب دانشنامه' : 'افزودن ترکیب جدید'} <h3 className="text-lg font-bold text-gray-900">
icon={FlaskConical} {editingIngredient ? 'ویرایش ترکیب دانشنامه' : 'افزودن ترکیب جدید'}
maxWidth="2xl" </h3>
footer={ <button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<div className="flex justify-end gap-2.5 w-full"> <X className="w-6 h-6" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="ingredientForm"
isLoading={isSaving}
>
ذخیره تغییرات
</Button>
</div> </div>
}
> <form onSubmit={handleSave} className="p-6 space-y-4 overflow-y-auto flex-1">
<div className="space-y-6 text-right font-vazir"> <div className="grid grid-cols-2 gap-4">
<form id="ingredientForm" onSubmit={handleSave} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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 <input
@ -308,7 +285,7 @@ export default function IngredientsManager() {
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه یکتا (slug)</label> <label className="block text-sm font-bold text-gray-700 mb-1">شناسه یکتا (slug)</label>
<input <input
@ -335,29 +312,28 @@ export default function IngredientsManager() {
</div> </div>
<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">تصویر ترکیب (imageUrl)</label>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={formData.imageUrl} value={formData.imageUrl}
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })} onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
placeholder="آدرس تصویر یا انتخاب از رسانه‌ها..." 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" 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" dir="ltr"
/> />
<Button <button
type="button" type="button"
variant="outline"
size="sm"
onClick={() => setIsMediaSelectorOpen(true)} 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> </button>
</div> </div>
</div> </div>
<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">خواص و مزایا (benefits array)</label>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
@ -371,23 +347,22 @@ export default function IngredientsManager() {
} }
}} }}
placeholder="تایپ کنید و اینتر بزنید (مثال: ترمیم غضروف)" placeholder="تایپ کنید و اینتر بزنید (مثال: ترمیم غضروف)"
className="flex-1 px-4 py-2 rounded-xl border border-gray-200 text-xs outline-none focus:border-purple-500" className="flex-1 px-4 py-2 rounded-xl border border-gray-200 text-xs outline-none"
/> />
<Button <button
type="button" type="button"
variant="primary"
size="xs"
onClick={addBenefitTag} onClick={addBenefitTag}
className="px-3 py-2 bg-purple-600 text-white rounded-xl text-xs font-bold"
> >
افزودن + افزودن
</Button> </button>
</div> </div>
<div className="flex flex-wrap gap-1.5 p-2 bg-gray-50 rounded-xl border border-gray-100 min-h-[40px]"> <div className="flex flex-wrap gap-1.5 p-2 bg-gray-50 rounded-xl border border-gray-100 min-h-[40px]">
{formData.benefits.map((b, i) => ( {formData.benefits.map((b, i) => (
<span key={i} className="inline-flex items-center gap-1 px-2.5 py-1 bg-purple-100 text-purple-800 text-xs font-bold rounded-lg"> <span key={i} className="inline-flex items-center gap-1 px-2.5 py-1 bg-purple-100 text-purple-800 text-xs font-bold rounded-lg">
<Tag className="w-3 h-3" /> <Tag className="w-3 h-3" />
{b} {b}
<button type="button" onClick={() => removeBenefitTag(b)} className="hover:text-red-500 cursor-pointer"> <button type="button" onClick={() => removeBenefitTag(b)} className="hover:text-red-500">
<X className="w-3 h-3" /> <X className="w-3 h-3" />
</button> </button>
</span> </span>
@ -406,12 +381,29 @@ export default function IngredientsManager() {
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 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>
<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> </form>
</div> </div>
</Modal> </div>
)} )}
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{deleteTargetId && ( {deleteTargetId && (
<ConfirmModal <ConfirmModal

View File

@ -6,12 +6,6 @@ import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination'; import Pagination from '../components/ui/Pagination';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import ImagePreviewModal from '../components/ui/ImagePreviewModal'; import ImagePreviewModal from '../components/ui/ImagePreviewModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface Media { interface Media {
id: string; id: string;
@ -305,20 +299,26 @@ export default function MediaManager() {
multiple multiple
className="hidden" className="hidden"
/> />
<Button <button
variant="primary"
size="sm"
startIcon={Upload}
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={isUploading} disabled={isUploading}
isLoading={isUploading} className="bg-purple-600 hover:bg-purple-700 disabled:opacity-70 text-white px-4 py-2.5 rounded-xl flex items-center gap-2 text-sm font-bold transition-all shadow-md shadow-purple-200 cursor-pointer"
> >
{isUploading ? `در حال آپلود ${uploadProgress ? `(${uploadProgress}٪)` : ''}...` : 'آپلود فایل جدید'} {isUploading ? (
</Button> <>
<Spinner size="sm" />
<span>در حال آپلود {uploadProgress ? `(${uploadProgress}٪)` : ''}...</span>
</>
) : (
<>
<Upload className="w-4 h-4" />
<span>آپلود فایل جدید (عکس، ویدئو، پادکست، PDF)</span>
</>
)}
</button>
</div> </div>
</div> </div>
{/* Upload Progress Bar if active */} {/* Upload Progress Bar if active */}
{isUploading && uploadProgress !== null && ( {isUploading && uploadProgress !== null && (
<div className="w-full bg-gray-100 rounded-full h-2 overflow-hidden shadow-inner"> <div className="w-full bg-gray-100 rounded-full h-2 overflow-hidden shadow-inner">
@ -596,82 +596,82 @@ export default function MediaManager() {
</div> </div>
)} )}
{/* Edit SEO Modal */}
{editingSeoMedia && ( {editingSeoMedia && (
<Modal <div className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
isOpen={!!editingSeoMedia} <div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden p-6 space-y-4">
onClose={() => setEditingSeoMedia(null)} <div className="flex items-center justify-between border-b pb-3">
title="تنظیمات سئوی تصویر" <h3 className="text-lg font-bold text-gray-900">تنظیمات سئوی تصویر</h3>
icon={ImageIcon} <button onClick={() => setEditingSeoMedia(null)} className="text-gray-400 hover:text-red-500">
maxWidth="md" <X className="w-5 h-5" />
footer={ </button>
<div className="flex justify-end gap-2.5 w-full"> </div>
<Button
variant="secondary" <div className="space-y-3 text-right">
size="sm" <div>
<label className="text-xs font-bold text-gray-700 block mb-1">متن جایگزین (Alt Text) *</label>
<input
type="text"
value={seoFormData.altText}
onChange={(e) => setSeoFormData({ ...seoFormData, altText: e.target.value })}
placeholder="مثال: عکس مکمل کانیدروکس گپ کنینا"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">عنوان تصویر (Title)</label>
<input
type="text"
value={seoFormData.title}
onChange={(e) => setSeoFormData({ ...seoFormData, title: e.target.value })}
placeholder="عنوان تصویر برای تولتیپ هور"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">توضیحات (Description)</label>
<textarea
rows={2}
value={seoFormData.description}
onChange={(e) => setSeoFormData({ ...seoFormData, description: e.target.value })}
placeholder="توضیحات کامل سئو..."
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">زیرنویس (Caption)</label>
<input
type="text"
value={seoFormData.caption}
onChange={(e) => setSeoFormData({ ...seoFormData, caption: e.target.value })}
placeholder="متن کپشن زیر عکس در مقالات"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
</div>
<div className="flex justify-end gap-2 pt-2 border-t">
<button
type="button"
onClick={() => setEditingSeoMedia(null)} onClick={() => setEditingSeoMedia(null)}
className="px-4 py-2 rounded-xl text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200"
> >
انصراف انصراف
</Button> </button>
<Button <button
variant="primary" type="button"
size="sm"
onClick={handleSaveSeo} onClick={handleSaveSeo}
className="px-5 py-2 rounded-xl text-xs font-bold text-white bg-purple-600 hover:bg-purple-700 shadow-sm"
> >
ذخیره سئو ذخیره سئو
</Button> </button>
</div>
}
>
<div className="space-y-3 text-right font-vazir">
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">متن جایگزین (Alt Text) *</label>
<input
type="text"
value={seoFormData.altText}
onChange={(e) => setSeoFormData({ ...seoFormData, altText: e.target.value })}
placeholder="مثال: عکس مکمل کانیدروکس گپ کنینا"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">عنوان تصویر (Title)</label>
<input
type="text"
value={seoFormData.title}
onChange={(e) => setSeoFormData({ ...seoFormData, title: e.target.value })}
placeholder="عنوان تصویر برای تولتیپ هور"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">توضیحات (Description)</label>
<textarea
rows={2}
value={seoFormData.description}
onChange={(e) => setSeoFormData({ ...seoFormData, description: e.target.value })}
placeholder="توضیحات کامل سئو..."
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">زیرنویس (Caption)</label>
<input
type="text"
value={seoFormData.caption}
onChange={(e) => setSeoFormData({ ...seoFormData, caption: e.target.value })}
placeholder="متن کپشن زیر عکس در مقالات"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div> </div>
</div> </div>
</Modal> </div>
)} )}
<ConfirmModal <ConfirmModal
isOpen={!!deleteTargetId} isOpen={!!deleteTargetId}
title="حذف تصویر" title="حذف تصویر"

View File

@ -7,10 +7,6 @@ import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface MenuItem { interface MenuItem {
id: string; id: string;
@ -178,17 +174,15 @@ export default function MenuManager() {
مدیریت آیتم‌های هدر و فوتر سایت — تغییرات بلادرنگ اعمال می‌شوند مدیریت آیتم‌های هدر و فوتر سایت — تغییرات بلادرنگ اعمال می‌شوند
</p> </p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} 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> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{MENU_TABS.map((tab) => ( {MENU_TABS.map((tab) => (
@ -298,45 +292,27 @@ export default function MenuManager() {
</div> </div>
)} )}
{/* Modal */} {/* Form Modal */}
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isModalOpen} <div className="bg-white rounded-2xl w-full max-w-xl shadow-2xl overflow-hidden">
onClose={() => setIsModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'} <h3 className="text-lg font-bold text-gray-900">
icon={Menu} {editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'}
maxWidth="xl" </h3>
footer={ <button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg">
<div className="flex justify-end gap-2.5 w-full"> <X className="w-5 h-5 text-gray-500" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="menuForm"
isLoading={isSaving}
>
ذخیره
</Button>
</div> </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 className="grid grid-cols-2 gap-4">
<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>
<select <select
value={formData.menuType} value={formData.menuType}
onChange={(e) => setFormData({ ...formData, menuType: e.target.value as MenuType, parentId: null })} 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-purple-500 outline-none text-sm bg-white" className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm bg-white"
> >
{MENU_TABS.map((tab) => ( {MENU_TABS.map((tab) => (
<option key={tab.key} value={tab.key}>{tab.label}</option> <option key={tab.key} value={tab.key}>{tab.label}</option>
@ -348,7 +324,7 @@ export default function MenuManager() {
<select <select
value={formData.parentId || ''} value={formData.parentId || ''}
onChange={(e) => setFormData({ ...formData, parentId: e.target.value || null })} 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-purple-500 outline-none text-sm bg-white" className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm bg-white"
> >
<option value="">— آیتم اصلی —</option> <option value="">— آیتم اصلی —</option>
{parentItems.map((p) => ( {parentItems.map((p) => (
@ -366,7 +342,7 @@ export default function MenuManager() {
value={formData.label} value={formData.label}
onChange={(e) => setFormData({ ...formData, label: e.target.value })} onChange={(e) => setFormData({ ...formData, label: 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" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
/> />
</div> </div>
@ -379,7 +355,7 @@ export default function MenuManager() {
onChange={(e) => setFormData({ ...formData, href: e.target.value })} onChange={(e) => setFormData({ ...formData, href: e.target.value })}
placeholder="مثال: /shop یا https://..." placeholder="مثال: /shop یا https://..."
dir="ltr" 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" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm font-mono"
/> />
</div> </div>
@ -391,7 +367,7 @@ export default function MenuManager() {
value={formData.icon} value={formData.icon}
onChange={(e) => setFormData({ ...formData, icon: e.target.value })} onChange={(e) => setFormData({ ...formData, icon: 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" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
/> />
</div> </div>
<div> <div>
@ -401,7 +377,7 @@ export default function MenuManager() {
value={formData.badge} value={formData.badge}
onChange={(e) => setFormData({ ...formData, badge: e.target.value })} onChange={(e) => setFormData({ ...formData, badge: 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" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
/> />
</div> </div>
</div> </div>
@ -413,7 +389,7 @@ export default function MenuManager() {
type="number" type="number"
value={formData.order} value={formData.order}
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })} 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-purple-500 outline-none text-sm" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
/> />
</div> </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"> <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">
@ -435,12 +411,29 @@ export default function MenuManager() {
/> />
</label> </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"
>
انصراف
</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> </form>
</div> </div>
</Modal> </div>
)} )}
{deleteTargetId && ( {deleteTargetId && (
<ConfirmModal <ConfirmModal
isOpen={true} isOpen={true}

View File

@ -15,9 +15,6 @@ import {
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
export default function PaymentGatewaysPage() { export default function PaymentGatewaysPage() {
const [activeTab, setActiveTab] = useState<'gateways' | 'wallets' | 'ebank' | 'submerchants'>('gateways'); const [activeTab, setActiveTab] = useState<'gateways' | 'wallets' | 'ebank' | 'submerchants'>('gateways');
@ -493,20 +490,18 @@ export default function PaymentGatewaysPage() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isSaving}
size="md" 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"
startIcon={Save}
isLoading={isSaving}
> >
ذخیره کلیه تنظیمات درگاه زیبال {isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
</Button> <span>ذخیره کلیه تنظیمات درگاه زیبال</span>
</button>
</div> </div>
</form> </form>
)} )}
{activeTab === 'wallets' && ( {activeTab === 'wallets' && (
<div className="space-y-6"> <div className="space-y-6">
{/* Wallets Overview */} {/* Wallets Overview */}

View File

@ -5,9 +5,6 @@ import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination'; import Pagination from '../components/ui/Pagination';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface Pet { export interface Pet {
id: string; id: string;
@ -218,130 +215,133 @@ export default function Pets() {
{/* Pet Medical Dossier Details Modal */} {/* Pet Medical Dossier Details Modal */}
{selectedPetForView && ( {selectedPetForView && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir" dir="rtl">
isOpen={!!selectedPetForView} <div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-gray-100 flex flex-col">
onClose={() => setSelectedPetForView(null)} <div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between sticky top-0 bg-white z-10">
title={`پرونده پزشکی و پایش سلامت: ${selectedPetForView.name}`} <div className="flex items-center gap-3">
icon={selectedPetForView.type === 'گربه' ? Cat : Dog} <div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-700 flex items-center justify-center font-black">
maxWidth="2xl" {selectedPetForView.type === 'گربه' ? <Cat className="w-5 h-5" /> : <Dog className="w-5 h-5" />}
footer={ </div>
<div className="flex justify-end w-full"> <div>
<Button <h3 className="text-base font-black text-gray-900">پرونده پزشکی و پایش سلامت: {selectedPetForView.name}</h3>
variant="secondary" <p className="text-xs text-gray-400 font-mono">شناسه: {selectedPetForView.id}</p>
size="sm" </div>
</div>
<button
type="button"
onClick={() => setSelectedPetForView(null)} onClick={() => setSelectedPetForView(null)}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
> >
بستن پرونده <X className="w-5 h-5" />
</Button> </button>
</div> </div>
}
> <div className="p-6 space-y-6">
<div className="space-y-6 text-right font-vazir"> {/* Pet Info Banner */}
{/* Pet Info Banner */} <div className="p-4 bg-purple-50/60 rounded-2xl border border-purple-100 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 text-xs">
<div className="p-4 bg-purple-50/60 rounded-2xl border border-purple-100 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 text-xs"> <div>
<div className="font-black text-gray-900 text-sm">{selectedPetForView.name} ({selectedPetForView.breed || selectedPetForView.type})</div>
<div className="text-gray-600 mt-1">
سرپرست: {selectedPetForView.user?.firstName} {selectedPetForView.user?.lastName} | همراه: <span className="font-mono">{selectedPetForView.user?.mobile || '-'}</span>
</div>
</div>
<div className="text-purple-900 font-bold bg-white px-3 py-1.5 rounded-xl border border-purple-200">
{selectedPetForView.age} سال | {selectedPetForView.weight} کیلوگرم | تحرک {selectedPetForView.activityLevel || 'متوسط'}
</div>
</div>
{/* Medical Conditions */}
<div> <div>
<div className="font-black text-gray-900 text-sm">{selectedPetForView.name} ({selectedPetForView.breed || selectedPetForView.type})</div> <h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<div className="text-gray-600 mt-1"> <ShieldCheck className="w-4 h-4 text-purple-600" />
سرپرست: {selectedPetForView.user?.firstName} {selectedPetForView.user?.lastName} | همراه: <span className="font-mono">{selectedPetForView.user?.mobile || '-'}</span> سوابق و حساسیت‌های پزشکی (Medical Conditions)
</div> </h4>
</div> {selectedPetForView.medicalConditions && selectedPetForView.medicalConditions.length > 0 ? (
<div className="text-purple-900 font-bold bg-white px-3 py-1.5 rounded-xl border border-purple-200"> <div className="flex flex-wrap gap-2">
{selectedPetForView.age} سال | {selectedPetForView.weight} کیلوگرم | تحرک {selectedPetForView.activityLevel || 'متوسط'} {selectedPetForView.medicalConditions.map((mc, idx) => (
</div> <span key={idx} className="px-3 py-1 bg-gray-100 text-gray-800 text-xs font-bold rounded-xl border border-gray-200">
</div> {mc.condition}
{/* Medical Conditions */}
<div>
<h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<ShieldCheck className="w-4 h-4 text-purple-600" />
سوابق و حساسیت‌های پزشکی (Medical Conditions)
</h4>
{selectedPetForView.medicalConditions && selectedPetForView.medicalConditions.length > 0 ? (
<div className="flex flex-wrap gap-2">
{selectedPetForView.medicalConditions.map((mc, idx) => (
<span key={idx} className="px-3 py-1 bg-gray-100 text-gray-800 text-xs font-bold rounded-xl border border-gray-200">
{mc.condition}
</span>
))}
</div>
) : (
<p className="text-xs text-gray-400">مورد پزشکی خاصی ثبت نشده است.</p>
)}
</div>
{/* Active Reminders */}
<div>
<h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<Clock className="w-4 h-4 text-purple-600" />
یادآورهای فعال (Reminders)
</h4>
{selectedPetForView.reminders && selectedPetForView.reminders.length > 0 ? (
<div className="space-y-2">
{selectedPetForView.reminders.map((rem) => (
<div key={rem.id} className="p-3 bg-gray-50 rounded-xl border border-gray-200 flex items-center justify-between text-xs">
<span className="font-bold text-gray-800">{rem.title}</span>
<span className="text-purple-700 font-mono font-bold bg-white px-2 py-0.5 rounded-md border border-purple-100">
ساعت {rem.time} ({rem.frequency})
</span> </span>
</div> ))}
))} </div>
</div> ) : (
) : ( <p className="text-xs text-gray-400">مورد پزشکی خاصی ثبت نشده است.</p>
<p className="text-xs text-gray-400">یادآوری فعالی ثبت نشده است.</p> )}
)} </div>
</div>
{/* Health Logs */} {/* Active Reminders */}
<div> <div>
<h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2"> <h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<Activity className="w-4 h-4 text-purple-600" /> <Clock className="w-4 h-4 text-purple-600" />
آخرین گزارشات روزانه سلامت (Health Logs) یادآورهای فعال (Reminders)
</h4> </h4>
{selectedPetForView.healthLogs && selectedPetForView.healthLogs.length > 0 ? ( {selectedPetForView.reminders && selectedPetForView.reminders.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
{selectedPetForView.healthLogs.map((log) => ( {selectedPetForView.reminders.map((rem) => (
<div key={log.id} className="p-3 bg-gray-50 rounded-xl border border-gray-200 text-xs space-y-1"> <div key={rem.id} className="p-3 bg-gray-50 rounded-xl border border-gray-200 flex items-center justify-between text-xs">
<div className="flex items-center justify-between text-gray-500 font-bold"> <span className="font-bold text-gray-800">{rem.title}</span>
<span>تاریخ: {new Date(log.loggedDate || log.createdAt).toLocaleDateString('fa-IR')}</span> <span className="text-purple-700 font-mono font-bold bg-white px-2 py-0.5 rounded-md border border-purple-100">
<span className="text-gray-700 font-black">اشتها: {log.appetite} | انرژی: {log.energy} | گوارش: {log.digestion}</span> ساعت {rem.time} ({rem.frequency})
</span>
</div> </div>
{log.note && <p className="text-gray-700 font-medium">{log.note}</p>} ))}
</div> </div>
))} ) : (
</div> <p className="text-xs text-gray-400">یادآوری فعالی ثبت نشده است.</p>
) : ( )}
<p className="text-xs text-gray-400">گزارش سلامتی ثبت نشده است.</p> </div>
)}
</div>
{/* Prescriptions */} {/* Health Logs */}
<div> <div>
<h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2"> <h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<FileText className="w-4 h-4 text-purple-600" /> <Activity className="w-4 h-4 text-purple-600" />
نسخه‌های پزشکی این همدم (Prescriptions) آخرین گزارشات روزانه سلامت (Health Logs)
</h4> </h4>
{selectedPetForView.prescriptions && selectedPetForView.prescriptions.length > 0 ? ( {selectedPetForView.healthLogs && selectedPetForView.healthLogs.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
{selectedPetForView.prescriptions.map((rx) => ( {selectedPetForView.healthLogs.map((log) => (
<div key={rx.id} className="p-3 bg-purple-50/50 rounded-xl border border-purple-100 text-xs space-y-1"> <div key={log.id} className="p-3 bg-gray-50 rounded-xl border border-gray-200 text-xs space-y-1">
<div className="flex items-center justify-between font-bold"> <div className="flex items-center justify-between text-gray-500 font-bold">
<span className="font-mono">نسخه #{rx.id.slice(0, 8)}</span> <span>تاریخ: {new Date(log.loggedDate || log.createdAt).toLocaleDateString('fa-IR')}</span>
<span className="text-purple-700 font-black">{rx.status}</span> <span className="text-gray-700 font-black">اشتها: {log.appetite} | انرژی: {log.energy} | گوارش: {log.digestion}</span>
</div>
{log.note && <p className="text-gray-700 font-medium">{log.note}</p>}
</div> </div>
{rx.notes && <p className="text-gray-600">یادداشت کاربر: {rx.notes}</p>} ))}
{rx.adminNotes && <p className="text-purple-900 font-bold">دستور پزشک: {rx.adminNotes}</p>} </div>
</div> ) : (
))} <p className="text-xs text-gray-400">گزارش سلامتی ثبت نشده است.</p>
</div> )}
) : ( </div>
<p className="text-xs text-gray-400">نسخه‌ای برای این پت ثبت نشده است.</p>
)} {/* Prescriptions */}
<div>
<h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<FileText className="w-4 h-4 text-purple-600" />
نسخه‌های پزشکی این همدم (Prescriptions)
</h4>
{selectedPetForView.prescriptions && selectedPetForView.prescriptions.length > 0 ? (
<div className="space-y-2">
{selectedPetForView.prescriptions.map((rx) => (
<div key={rx.id} className="p-3 bg-purple-50/50 rounded-xl border border-purple-100 text-xs space-y-1">
<div className="flex items-center justify-between font-bold">
<span className="font-mono">نسخه #{rx.id.slice(0, 8)}</span>
<span className="text-purple-700 font-black">{rx.status}</span>
</div>
{rx.notes && <p className="text-gray-600">یادداشت کاربر: {rx.notes}</p>}
{rx.adminNotes && <p className="text-purple-900 font-bold">دستور پزشک: {rx.adminNotes}</p>}
</div>
))}
</div>
) : (
<p className="text-xs text-gray-400">نسخه‌ای برای این پت ثبت نشده است.</p>
)}
</div>
</div> </div>
</div> </div>
</Modal> </div>
)} )}
<ConfirmModal <ConfirmModal
isOpen={!!deleteTargetId} isOpen={!!deleteTargetId}
title="حذف حیوان خانگی" title="حذف حیوان خانگی"

View File

@ -5,11 +5,8 @@ import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import ImagePreviewModal from '../components/ui/ImagePreviewModal'; import ImagePreviewModal from '../components/ui/ImagePreviewModal';
import Badge from '../components/ui/Badge'; import Badge from '../components/ui/Badge';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { Prescription } from '../types/admin'; import type { Prescription } from '../types/admin';
interface ProductItem { interface ProductItem {
id: string; id: string;
name: string; name: string;
@ -246,13 +243,12 @@ export default function PrescriptionsManager() {
)} )}
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<Button <button
variant="primary"
size="xs"
onClick={() => openReviewModal(rx)} onClick={() => openReviewModal(rx)}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-bold transition-all shadow-xs cursor-pointer"
> >
بررسی و تجویز بررسی و تجویز
</Button> </button>
</td> </td>
</tr> </tr>
); );
@ -265,36 +261,27 @@ export default function PrescriptionsManager() {
{/* Review Modal */} {/* Review Modal */}
{selectedRx && ( {selectedRx && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir" dir="rtl">
isOpen={!!selectedRx} <div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-gray-100 flex flex-col">
onClose={() => setSelectedRx(null)} <div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50 sticky top-0 bg-white z-10">
title="بررسی و ثبت نظر تخصصی نسخه" <div className="flex items-center gap-3">
icon={FileText} <div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
maxWidth="2xl" <FileText className="w-5 h-5" />
footer={ </div>
<div className="flex justify-end gap-2.5 w-full"> <div>
<Button <h3 className="text-lg font-bold text-gray-900">بررسی و ثبت نظر تخصصی نسخه</h3>
variant="secondary" <p className="text-xs text-gray-500">کد نسخه: {selectedRx.id}</p>
size="sm" </div>
type="button" </div>
<button
onClick={() => setSelectedRx(null)} onClick={() => setSelectedRx(null)}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
> >
انصراف <X className="w-5 h-5" />
</Button> </button>
<Button
variant="primary"
size="sm"
type="submit"
form="prescriptionForm"
isLoading={isSubmitting}
>
ثبت نهایی و ارسال پیامک
</Button>
</div> </div>
}
> <form onSubmit={handleReviewSubmit} className="p-6 space-y-5">
<div className="space-y-5 text-right font-vazir">
<form id="prescriptionForm" onSubmit={handleReviewSubmit} className="space-y-5">
{/* Pet & User Details Card */} {/* Pet & User Details Card */}
<div className="p-4 bg-gray-50 rounded-2xl border border-gray-200 text-xs space-y-2"> <div className="p-4 bg-gray-50 rounded-2xl border border-gray-200 text-xs space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -308,68 +295,58 @@ export default function PrescriptionsManager() {
</div> </div>
)} )}
{selectedRx.notes && ( {selectedRx.notes && (
<div className="pt-2 border-t border-gray-200 text-gray-500"> <div className="pt-2 border-t border-gray-200 text-gray-700">
<span className="font-bold text-gray-700">توضیحات کاربر: </span> <strong className="block mb-0.5 text-gray-900">یادداشت کاربر:</strong>
{selectedRx.notes} {selectedRx.notes}
</div> </div>
)} )}
<div className="pt-2 border-t border-gray-200 flex justify-between items-center"> </div>
<Button
{/* Status Selector */}
<div>
<label className="block text-xs font-black text-gray-700 mb-2">تغییر وضعیت نسخه *</label>
<div className="grid grid-cols-2 gap-3">
<button
type="button" type="button"
variant="outline" onClick={() => setReviewStatus('APPROVED')}
size="xs" className={`py-3 px-4 rounded-xl border font-bold text-xs flex items-center justify-center gap-2 transition-all cursor-pointer ${
startIcon={Eye} reviewStatus === 'APPROVED'
onClick={() => setPreviewImageUrl(getFileFullUrl(selectedRx.fileUrl))} ? 'border-green-500 bg-green-50 text-green-700 shadow-xs font-black'
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
}`}
> >
مشاهده تصویر نسخه بارگذاری شده <CheckCircle2 className="w-4 h-4" /> تایید و ثبت تجویز (APPROVED)
</Button> </button>
<span className="text-[11px] text-gray-400 font-mono">
تاریخ ثبت: {selectedRx.createdAt ? new Date(selectedRx.createdAt).toLocaleDateString('fa-IR') : '-'} <button
</span> type="button"
onClick={() => setReviewStatus('REJECTED')}
className={`py-3 px-4 rounded-xl border font-bold text-xs flex items-center justify-center gap-2 transition-all cursor-pointer ${
reviewStatus === 'REJECTED'
? 'border-red-500 bg-red-50 text-red-700 shadow-xs font-black'
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
}`}
>
<XCircle className="w-4 h-4" /> عدم تایید / رد نسخه (REJECTED)
</button>
</div> </div>
</div> </div>
{/* Action Decision */} {/* Prescribed Products Selector */}
<div> <div>
<label className="block text-xs font-black text-gray-700 mb-2">تصمیم بالینی و وضعیت تایید *</label> <label className="block text-xs font-black text-gray-700 mb-1 flex items-center justify-between">
<div className="grid grid-cols-3 gap-3"> <span>محصولات و مکمل‌های تجویزی پزشک (جهت خرید آسان کاربر)</span>
{[ <span className="text-[10px] text-gray-400 font-normal">{selectedProductIds.length} محصول انتخاب شده</span>
{ id: 'APPROVED', label: 'تایید نسخه و پیشنهاد دارو', color: 'border-green-500 bg-green-50 text-green-700' },
{ id: 'REJECTED', label: 'عدم تایید / ناخوانا', color: 'border-red-500 bg-red-50 text-red-700' },
{ id: 'PENDING', label: 'در انتظار پیگیری بیمار', color: 'border-amber-500 bg-amber-50 text-amber-700' },
].map((s) => (
<button
type="button"
key={s.id}
onClick={() => setReviewStatus(s.id as any)}
className={`p-3 rounded-2xl border text-xs font-bold text-center transition-all cursor-pointer ${
reviewStatus === s.id
? `${s.color} shadow-xs ring-2 ring-purple-600/20`
: 'border-gray-200 text-gray-600 hover:border-gray-300'
}`}
>
{s.label}
</button>
))}
</div>
</div>
{/* Recommended Products Selector */}
<div>
<label className="block text-xs font-black text-gray-700 mb-1">
داروها و مکمل‌های تجویزی (انتخاب از محصولات سایت برای خرید فوری سرپرست)
</label> </label>
<p className="text-[11px] text-gray-400 mb-2">محصولاتی که تیک می‌زنید مستقیماً در پنل کاربر به عنوان سبد پیشنهادی نسخه قرار می‌گیرند.</p> <div className="max-h-40 overflow-y-auto border border-gray-200 rounded-xl p-2 space-y-1 bg-white">
{products.map(p => {
<div className="max-h-48 overflow-y-auto border border-gray-200 rounded-2xl p-2 space-y-1 bg-white">
{products.map((p) => {
const isSelected = selectedProductIds.includes(p.id); const isSelected = selectedProductIds.includes(p.id);
return ( return (
<div <div
key={p.id} key={p.id}
onClick={() => { onClick={() => {
setSelectedProductIds((prev) => setSelectedProductIds(prev =>
isSelected ? prev.filter((id) => id !== p.id) : [...prev, p.id] isSelected ? prev.filter(id => id !== p.id) : [...prev, p.id]
); );
}} }}
className={`p-2 rounded-lg text-xs font-bold flex items-center justify-between cursor-pointer transition-colors ${ className={`p-2 rounded-lg text-xs font-bold flex items-center justify-between cursor-pointer transition-colors ${
@ -402,12 +379,29 @@ export default function PrescriptionsManager() {
className="w-full px-3.5 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs leading-relaxed" className="w-full px-3.5 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs leading-relaxed"
/> />
</div> </div>
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
<button
type="button"
onClick={() => setSelectedRx(null)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 text-xs font-bold hover:bg-gray-50 transition-colors cursor-pointer"
>
انصراف
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white text-xs font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2 cursor-pointer"
>
{isSubmitting && <Spinner size="sm" />}
ثبت نهایی و ارسال پیامک
</button>
</div>
</form> </form>
</div> </div>
</Modal> </div>
)} )}
{/* Fullscreen Lightbox for Prescription Image */} {/* Fullscreen Lightbox for Prescription Image */}
<ImagePreviewModal <ImagePreviewModal
isOpen={!!previewImageUrl} isOpen={!!previewImageUrl}

View File

@ -8,9 +8,6 @@ import Pagination from '../components/ui/Pagination';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import PriceInput from '../components/ui/PriceInput'; import PriceInput from '../components/ui/PriceInput';
import Button from '../components/ui/Button';
export interface Category { export interface Category {
id: string; id: string;
@ -438,17 +435,15 @@ export default function Products() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">افزودن و ویرایش پیشرفته محصولات با تنظیمات سئو</p> <p className="text-gray-500 font-medium mt-1">افزودن و ویرایش پیشرفته محصولات با تنظیمات سئو</p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} 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>
{/* Filters Bar */} {/* 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="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"> <div className="relative flex-1">
@ -1655,29 +1650,18 @@ export default function Products() {
</form> </form>
</div> </div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-2.5 rounded-b-2xl"> <div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
<Button <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">
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف انصراف
</Button> </button>
<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">
variant="primary"
size="sm"
type="submit"
form="productForm"
>
ذخیره محصول ذخیره محصول
</Button> </button>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* Media Selector Modal */} {/* Media Selector Modal */}
<MediaSelector <MediaSelector
isOpen={isMediaSelectorOpen} isOpen={isMediaSelectorOpen}

View File

@ -1,11 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { TrendingUp, DollarSign, ShoppingBag, Users, HeartHandshake, Download } from 'lucide-react'; import { TrendingUp, DollarSign, ShoppingBag, Users, HeartHandshake, Download } from 'lucide-react';
import api from '../services/api'; import api from '../services/api';
import Button from '../components/ui/Button';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import { import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
BarChart, Bar, PieChart, Pie, Cell, Legend BarChart, Bar, PieChart, Pie, Cell, Legend
@ -101,10 +97,7 @@ export default function Reports() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">آمار فروش، رفتار کاربران و عملکرد تخفیف‌ها</p> <p className="text-gray-500 font-medium mt-1">آمار فروش، رفتار کاربران و عملکرد تخفیف‌ها</p>
</div> </div>
<Button <button
variant="outline"
size="sm"
startIcon={Download}
onClick={() => { onClick={() => {
if (!reportData) return; if (!reportData) return;
const rows = [ const rows = [
@ -126,12 +119,13 @@ export default function Reports() {
link.click(); link.click();
document.body.removeChild(link); 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) خروجی اکسل (Excel)
</Button> </button>
</div> </div>
{/* Overview Cards */} {/* Overview Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <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"> <div className="bg-gradient-to-br from-purple-500 to-indigo-600 p-6 rounded-2xl text-white shadow-lg shadow-purple-200">

View File

@ -18,10 +18,6 @@ import {
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface ProductReview { interface ProductReview {
id: string; id: string;
@ -438,34 +434,21 @@ export default function Reviews() {
{/* Reply Modal */} {/* Reply Modal */}
{replyingReview && ( {replyingReview && (
<Modal <div className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4">
isOpen={!!replyingReview} <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">
onClose={() => setReplyingReview(null)} <div className="flex items-center justify-between border-b border-gray-100 pb-3">
title={`پاسخ به دیدگاه ${replyingReview.userName}`} <h3 className="text-base font-black text-gray-900 flex items-center gap-2">
icon={Reply} <Reply className="w-5 h-5 text-purple-600" />
maxWidth="lg" پاسخ به دیدگاه {replyingReview.userName}
footer={ </h3>
<div className="flex items-center justify-end gap-2.5 w-full"> <button
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setReplyingReview(null)} 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>
}
>
<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"> <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> <span className="font-bold text-gray-700 block">متن دیدگاه خریدار:</span>
<p className="leading-relaxed whitespace-pre-wrap">{replyingReview.comment}</p> <p className="leading-relaxed whitespace-pre-wrap">{replyingReview.comment}</p>
@ -483,10 +466,28 @@ 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" 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>
<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>
)} )}
</div> </div>
); );
} }

View File

@ -4,10 +4,8 @@ import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import type { SeoSettings } from '../types/admin'; import type { SeoSettings } from '../types/admin';
export default function SeoSettingsPage() { export default function SeoSettingsPage() {
const [seo, setSeo] = useState<SeoSettings>({ const [seo, setSeo] = useState<SeoSettings>({
brandNameFa: 'کنینا ایران', brandNameFa: 'کنینا ایران',
@ -273,34 +271,31 @@ export default function SeoSettingsPage() {
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono" className="flex-1 px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
dir="ltr" dir="ltr"
/> />
<Button <button
type="button" type="button"
variant="outline"
size="sm"
onClick={() => setIsMediaSelectorOpen(true)} onClick={() => setIsMediaSelectorOpen(true)}
className="px-3 py-3 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0 cursor-pointer"
> >
انتخاب از رسانه انتخاب از رسانه
</Button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-end pt-4 border-t border-gray-100"> <div className="flex justify-end pt-4 border-t border-gray-100">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isSaving}
size="md" className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
startIcon={Save}
isLoading={isSaving}
> >
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
ذخیره تنظیمات سئو و ساختار عناوین ذخیره تنظیمات سئو و ساختار عناوین
</Button> </button>
</div> </div>
</form> </form>
)} )}
{isMediaSelectorOpen && ( {isMediaSelectorOpen && (
<MediaSelector <MediaSelector
isOpen={isMediaSelectorOpen} isOpen={isMediaSelectorOpen}

View File

@ -20,9 +20,6 @@ import {
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
export default function Settings() { export default function Settings() {
const [activeTab, setActiveTab] = useState<'brand' | 'contact' | 'links'>('brand'); const [activeTab, setActiveTab] = useState<'brand' | 'contact' | 'links'>('brand');
@ -361,15 +358,14 @@ export default function Settings() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isSaving}
size="md" className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
startIcon={Save}
isLoading={isSaving}
> >
ذخیره هویت برند {isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
</Button> <span>ذخیره هویت برند</span>
</button>
</div> </div>
</form> </form>
) : activeTab === 'contact' ? ( ) : activeTab === 'contact' ? (
@ -472,18 +468,16 @@ export default function Settings() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isSaving}
size="md" className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
startIcon={Save}
isLoading={isSaving}
> >
ذخیره اطلاعات تماس {isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
</Button> <span>ذخیره اطلاعات تماس</span>
</button>
</div> </div>
</form> </form>
) : ( ) : (
/* Tab 3: Dedicated Modules Grid */ /* Tab 3: Dedicated Modules Grid */
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">

View File

@ -4,9 +4,6 @@ import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import PriceInput from '../components/ui/PriceInput'; import PriceInput from '../components/ui/PriceInput';
import Button from '../components/ui/Button';
export default function ShippingSettingsPage() { export default function ShippingSettingsPage() {
const [shipping, setShipping] = useState({ const [shipping, setShipping] = useState({
@ -151,19 +148,17 @@ export default function ShippingSettingsPage() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isSaving}
size="md" className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-blue-200 cursor-pointer"
startIcon={Save}
isLoading={isSaving}
> >
ذخیره تغییرات ارسال {isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
</Button> <span>ذخیره تغییرات ارسال</span>
</button>
</div> </div>
</form> </form>
)} )}
</div> </div>
); );
} }

View File

@ -4,11 +4,8 @@ import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { SmartAdvisorRule, Product } from '../types/admin'; import type { SmartAdvisorRule, Product } from '../types/admin';
export default function SmartAdvisorManager() { export default function SmartAdvisorManager() {
const [rules, setRules] = useState<SmartAdvisorRule[]>([]); const [rules, setRules] = useState<SmartAdvisorRule[]>([]);
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
@ -150,14 +147,13 @@ export default function SmartAdvisorManager() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">تعریف قوانین تجویز محصولات پیشنهادی بر اساس عوارض و علائم پت</p> <p className="text-gray-500 font-medium mt-1">تعریف قوانین تجویز محصولات پیشنهادی بر اساس عوارض و علائم پت</p>
</div> </div>
<Button <button
variant="primary"
size="md"
startIcon={Plus}
onClick={() => openModal()} 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>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
@ -202,20 +198,12 @@ export default function SmartAdvisorManager() {
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <button onClick={() => openModal(rule)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
variant="ghost" <Edit2 className="w-4 h-4" />
size="xs" </button>
startIcon={Edit2} <button onClick={() => setDeleteTargetId(rule.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
onClick={() => openModal(rule)} <Trash2 className="w-4 h-4" />
className="text-blue-600 hover:bg-blue-50" </button>
/>
<Button
variant="ghost"
size="xs"
startIcon={Trash2}
onClick={() => setDeleteTargetId(rule.id)}
className="text-red-600 hover:bg-red-50"
/>
</div> </div>
</td> </td>
</tr> </tr>
@ -228,100 +216,103 @@ export default function SmartAdvisorManager() {
{/* Form Modal */} {/* Form Modal */}
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isModalOpen} <div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
onClose={() => setIsModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={editingRule ? 'ویرایش قانون مشاور هوشمند' : 'تعریف قانون جدید'} <h3 className="text-lg font-bold text-gray-900">
icon={Sparkles} {editingRule ? 'ویرایش قانون مشاور هوشمند' : 'تعریف قانون جدید'}
maxWidth="lg" </h3>
> <button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<form onSubmit={handleSave} className="space-y-4 font-vazir"> <X className="w-6 h-6" />
<div> </button>
<label className="block text-sm font-bold text-gray-700 mb-1">نام عارضه یا علامت بیماری (condition) *</label>
<input
required
type="text"
value={formData.condition}
onChange={(e) => setFormData({ ...formData, condition: e.target.value })}
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>
<div> <form onSubmit={handleSave} className="p-6 space-y-4">
<label className="block text-sm font-bold text-gray-700 mb-1">گونه پت هدف (targetPetType)</label> <div>
<select <label className="block text-sm font-bold text-gray-700 mb-1">شرط / علائم عارضه (condition) *</label>
value={formData.targetPetType} <input
onChange={(e) => setFormData({ ...formData, targetPetType: 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 bg-white"
>
<option value="سگ">سگ (Dog)</option>
<option value="گربه">گربه (Cat)</option>
<option value="هر دو">هر دو (سگ و گربه)</option>
</select>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">انتخاب محصول پیشنهادی کنینا (recommendedProduct) *</label>
<div className="space-y-2">
<div className="relative">
<input
type="text"
value={productSearch}
onChange={(e) => setProductSearch(e.target.value)}
placeholder="جستجو در نام محصولات..."
className="w-full pl-4 pr-10 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs"
/>
<Search className="w-4 h-4 text-gray-400 absolute right-3 top-2.5" />
</div>
<select
required required
value={formData.recommendedProduct} type="text"
onChange={(e) => setFormData({ ...formData, recommendedProduct: e.target.value })} value={formData.condition}
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white" onChange={(e) => setFormData({ ...formData, condition: e.target.value })}
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">گونه حیوان هدف (targetPetType)</label>
<select
value={formData.targetPetType}
onChange={(e) => setFormData({ ...formData, targetPetType: 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"
> >
<option value="">-- یک محصول را انتخاب کنید --</option> <option value="سگ">سگ</option>
{filteredProducts.map((p) => ( <option value="گربه">گربه</option>
<option key={p.id} value={p.id}> <option value="هر دو">هر دو (سگ و گربه)</option>
{p.nameFa} ({p.nameEn || p.id})
</option>
))}
</select> </select>
</div> </div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">علت و توضیح علمی پیشنهاد (reason) *</label> <div>
<textarea <label className="block text-sm font-bold text-gray-700 mb-1">محصول پیشنهادی *</label>
required <div className="space-y-2">
rows={4} <div className="relative">
value={formData.reason} <Search className="w-4 h-4 absolute right-3 top-1/2 -translate-y-1/2 text-gray-400" />
onChange={(e) => setFormData({ ...formData, reason: e.target.value })} <input
placeholder="مثال: حاوی ۱۰٪ پودر صدف لب‌سبز برای بازسازی غضروف مفاصل..." type="text"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm" placeholder="جستجوی سریع محصول..."
/> value={productSearch}
</div> onChange={(e) => setProductSearch(e.target.value)}
className="w-full pl-3 pr-9 py-2 rounded-lg border border-gray-200 text-xs outline-none"
/>
</div>
<select
required
value={formData.recommendedProduct}
onChange={(e) => setFormData({ ...formData, recommendedProduct: 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"
>
{filteredProducts.map(p => (
<option key={p.id} value={p.id}>
{p.nameFa || p.name} ({p.artNo})
</option>
))}
</select>
</div>
</div>
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100"> <div>
<Button <label className="block text-sm font-bold text-gray-700 mb-1">علت و توضیح علمی پیشنهاد (reason) *</label>
type="button" <textarea
variant="secondary" required
size="md" rows={4}
onClick={() => setIsModalOpen(false)} value={formData.reason}
> onChange={(e) => setFormData({ ...formData, reason: e.target.value })}
انصراف placeholder="مثال: حاوی ۱۰٪ پودر صدف لب‌سبز برای بازسازی غضروف مفاصل..."
</Button> className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
<Button />
type="submit" </div>
variant="primary"
size="md" <div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
isLoading={isSaving} <button
> type="button"
ذخیره قانون onClick={() => setIsModalOpen(false)}
</Button> className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
</div> >
</form> انصراف
</Modal> </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>
)} )}
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
@ -337,4 +328,3 @@ export default function SmartAdvisorManager() {
</div> </div>
); );
} }

View File

@ -39,9 +39,6 @@ import {
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
interface SmsConfigState { interface SmsConfigState {
enabled: boolean; enabled: boolean;
@ -770,19 +767,17 @@ export default function SmsSettingsPage() {
{/* Submit Button */} {/* Submit Button */}
<div className="flex justify-end pt-2"> <div className="flex justify-end pt-2">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isSaving}
size="md" className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3.5 px-8 rounded-xl transition-all flex items-center gap-2 shadow-lg shadow-purple-200 disabled:opacity-50"
startIcon={Save}
isLoading={isSaving}
> >
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
ذخیره تنظیمات درگاه پیامک ذخیره تنظیمات درگاه پیامک
</Button> </button>
</div> </div>
</form> </form>
{/* Balance & Test SMS Sidebar (1 col on large) */} {/* Balance & Test SMS Sidebar (1 col on large) */}
<div className="space-y-6"> <div className="space-y-6">
{/* Realtime Credit / Balance Card */} {/* Realtime Credit / Balance Card */}

View File

@ -19,9 +19,6 @@ import {
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
interface SslStatus { interface SslStatus {
exists: boolean; exists: boolean;
@ -437,16 +434,14 @@ export default function SslSettingsPage() {
</div> </div>
<div className="flex justify-end gap-3 pt-2"> <div className="flex justify-end gap-3 pt-2">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isUpdating || !certFile || !keyFile}
size="sm" className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-xl text-xs font-bold transition-all shadow-md shadow-purple-200 flex items-center gap-2 cursor-pointer"
startIcon={ShieldCheck}
disabled={!certFile || !keyFile}
isLoading={isUpdating}
> >
نصب و فعال‌سازی گواهی جدید {isUpdating ? <Spinner size="sm" /> : <ShieldCheck className="w-4 h-4" />}
</Button> <span>نصب و فعال‌سازی گواهی جدید</span>
</button>
</div> </div>
</form> </form>
) : ( ) : (
@ -484,16 +479,14 @@ export default function SslSettingsPage() {
</div> </div>
<div className="flex justify-end gap-3 pt-2"> <div className="flex justify-end gap-3 pt-2">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isUpdating || !certText.trim() || !keyText.trim()}
size="sm" className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-xl text-xs font-bold transition-all shadow-md shadow-purple-200 flex items-center gap-2 cursor-pointer"
startIcon={ShieldCheck}
disabled={!certText.trim() || !keyText.trim()}
isLoading={isUpdating}
> >
ذخیره و اعمال تغییرات {isUpdating ? <Spinner size="sm" /> : <ShieldCheck className="w-4 h-4" />}
</Button> <span>ذخیره و اعمال تغییرات</span>
</button>
</div> </div>
</form> </form>
)} )}
@ -516,20 +509,18 @@ export default function SslSettingsPage() {
className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono outline-none focus:border-blue-500" className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono outline-none focus:border-blue-500"
dir="ltr" dir="ltr"
/> />
<Button <button
type="button" type="button"
variant="outline"
size="sm"
startIcon={ExternalLink}
onClick={handleTestOnline} onClick={handleTestOnline}
isLoading={isTestingOnline} disabled={isTestingOnline}
className="px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-bold rounded-xl transition-all shadow-xs flex items-center justify-center gap-2 cursor-pointer"
> >
تست زنده پورت ۴۴۳ دامنه {isTestingOnline ? <Spinner size="sm" /> : <ExternalLink className="w-4 h-4" />}
</Button> <span>تست زنده پورت ۴۴۳ دامنه</span>
</button>
</div> </div>
{onlineResult && ( {onlineResult && (
<div className="mt-3 p-4 bg-gray-50 rounded-xl border border-gray-200 text-xs space-y-1.5 animate-in fade-in duration-150"> <div className="mt-3 p-4 bg-gray-50 rounded-xl border border-gray-200 text-xs space-y-1.5 animate-in fade-in duration-150">
{onlineResult.onlineCheck ? ( {onlineResult.onlineCheck ? (
<div className="space-y-1"> <div className="space-y-1">

View File

@ -14,11 +14,8 @@ import {
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import type { SystemSettings } from '../types/admin'; import type { SystemSettings } from '../types/admin';
export default function SystemSettingsPage() { export default function SystemSettingsPage() {
const [system, setSystem] = useState<SystemSettings>({ const [system, setSystem] = useState<SystemSettings>({
maintenanceMode: false, maintenanceMode: false,
@ -327,19 +324,17 @@ export default function SystemSettingsPage() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<Button <button
type="submit" type="submit"
variant="primary" disabled={isSaving}
size="md" className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
startIcon={Save}
isLoading={isSaving}
> >
ذخیره تنظیمات سیستمی {isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
</Button> <span>ذخیره تنظیمات سیستمی</span>
</button>
</div> </div>
</form> </form>
)} )}
</div> </div>
); );
} }

View File

@ -5,11 +5,8 @@ import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { Testimonial } from '../types/admin'; import type { Testimonial } from '../types/admin';
export default function TestimonialsManager() { export default function TestimonialsManager() {
const [testimonials, setTestimonials] = useState<Testimonial[]>([]); const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -151,14 +148,13 @@ export default function TestimonialsManager() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">مدیریت توصیه نامه‌ها و نظرات تایید شده جهت نمایش در صفحه اصلی و گالری دامپزشکان</p> <p className="text-gray-500 font-medium mt-1">مدیریت توصیه نامه‌ها و نظرات تایید شده جهت نمایش در صفحه اصلی و گالری دامپزشکان</p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} 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>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
@ -203,51 +199,45 @@ export default function TestimonialsManager() {
{item.roleTitle && <span className="text-xs text-purple-600 font-medium">{item.roleTitle}</span>} {item.roleTitle && <span className="text-xs text-purple-600 font-medium">{item.roleTitle}</span>}
</div> </div>
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6 text-gray-600 text-sm max-w-md line-clamp-2">
<p className="text-gray-600 text-sm line-clamp-2 leading-relaxed max-w-md">{item.content}</p> {item.content}
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-1 text-amber-500"> <div className="flex items-center gap-1 text-amber-500">
{Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: item.rating || 5 }).map((_, i) => (
<Star <Star key={i} className="w-4 h-4 fill-amber-400" />
key={i}
className={`w-3.5 h-3.5 ${i < item.rating ? 'fill-amber-400 text-amber-400' : 'text-gray-200'}`}
/>
))} ))}
</div> </div>
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button {item.isFeatured && (
onClick={() => handleToggleActive(item)} <span className="px-2 py-0.5 bg-amber-50 text-amber-700 text-[11px] font-bold rounded border border-amber-200">
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold transition-colors cursor-pointer ${ ویژه (Featured)
item.isActive </span>
? 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100' )}
: 'bg-rose-50 text-rose-700 hover:bg-rose-100'
}`}
>
{item.isActive ? <CheckCircle2 className="w-3 h-3" /> : <XCircle className="w-3 h-3" />}
{item.isActive ? 'فعال' : 'غیرفعال'}
</button>
{(item as any).showOnHome && ( {(item as any).showOnHome && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs font-bold rounded-md"> <span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-[11px] font-bold rounded border border-blue-200">
خانه 🏠 صفحه اصلی
</span>
)}
{item.isActive ? (
<span className="inline-flex items-center gap-0.5 text-green-600 text-xs font-bold">
<CheckCircle2 className="w-3.5 h-3.5" /> فعال
</span>
) : (
<span className="inline-flex items-center gap-0.5 text-gray-400 text-xs font-bold">
<XCircle className="w-3.5 h-3.5" /> غیرفعال
</span> </span>
)} )}
</div> </div>
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button onClick={() => openModal(item)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
onClick={() => openModal(item)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"
>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</button> </button>
<button <button onClick={() => setDeleteTargetId(item.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
onClick={() => setDeleteTargetId(item.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
>
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
@ -260,117 +250,88 @@ export default function TestimonialsManager() {
</div> </div>
</div> </div>
{/* Modal */} {/* Form Modal */}
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isModalOpen} <div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
onClose={() => setIsModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'} <h3 className="text-lg font-bold text-gray-900">
icon={MessageSquareQuote} {editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'}
maxWidth="2xl" </h3>
footer={ <button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<div className="flex justify-end gap-2.5 w-full"> <X className="w-6 h-6" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="testimonialForm"
isLoading={isSaving}
>
ذخیره تغییرات
</Button>
</div> </div>
}
>
<div className="space-y-6 text-right font-vazir">
<form id="testimonialForm" onSubmit={handleSave} className="space-y-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>
<input
type="text"
required
value={formData.authorName}
onChange={(e) => setFormData({ ...formData, authorName: e.target.value })}
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> <form onSubmit={handleSave} className="p-6 space-y-4">
<label className="block text-sm font-bold text-gray-700 mb-1">سمت / تخصص</label> <div>
<input <label className="block text-sm font-bold text-gray-700 mb-1">نام و نام خانوادگی نویسنده *</label>
type="text" <input
value={formData.roleTitle} required
onChange={(e) => setFormData({ ...formData, roleTitle: e.target.value })} type="text"
placeholder="مثال: متخصص جراحی دامپزشکی" value={formData.authorName}
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm" onChange={(e) => setFormData({ ...formData, authorName: e.target.value })}
/> placeholder="مثال: دکتر علیرضا محمدی"
</div> className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div> </div>
<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">سمت / عنوان تخصص (roleTitle)</label>
<input
type="text"
value={formData.roleTitle}
onChange={(e) => setFormData({ ...formData, roleTitle: e.target.value })}
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">تصویر آواتار (avatarUrl)</label>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={formData.avatarUrl} value={formData.avatarUrl}
onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })} onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })}
placeholder="آدرس تصویر یا انتخاب از رسانه‌ها..." 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" 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 <button
type="button" type="button"
variant="outline"
size="sm"
onClick={() => setIsMediaSelectorOpen(true)} 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> </button>
</div> </div>
</div> </div>
<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>
<textarea <textarea
required required
rows={4} rows={4}
value={formData.content} value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })} onChange={(e) => setFormData({ ...formData, content: 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 leading-relaxed" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/> />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-center"> <div className="grid grid-cols-2 gap-4">
<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>
<div className="flex items-center gap-2"> <select
{[1, 2, 3, 4, 5].map((star) => ( value={formData.rating}
<button onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
type="button" 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"
key={star} >
onClick={() => setFormData({ ...formData, rating: star })} {[5, 4, 3, 2, 1].map(r => (
className="p-1 cursor-pointer" <option key={r} value={r}>{r} ستاره</option>
>
<Star
className={`w-6 h-6 ${
star <= formData.rating ? 'fill-amber-400 text-amber-400' : 'text-gray-300'
}`}
/>
</button>
))} ))}
<span className="text-sm font-bold text-gray-500 mr-2">({formData.rating} از ۵)</span> </select>
</div>
</div> </div>
<div> <div>
@ -384,8 +345,18 @@ export default function TestimonialsManager() {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4 pt-2"> <div className="pt-2 grid grid-cols-3 gap-3">
<label className="flex items-center justify-between p-3 bg-purple-50 rounded-xl border border-purple-200 cursor-pointer"> <label className="flex items-center justify-between p-3 bg-gray-50 rounded-xl border border-gray-200 cursor-pointer">
<span className="text-xs font-bold text-gray-700">نمایش ویژه (Featured)</span>
<input
type="checkbox"
checked={formData.isFeatured}
onChange={(e) => setFormData({ ...formData, isFeatured: e.target.checked })}
className="rounded text-purple-600 focus:ring-purple-500"
/>
</label>
<label className="flex items-center justify-between p-3 bg-gray-50 rounded-xl border border-gray-200 cursor-pointer">
<span className="text-xs font-bold text-gray-700">وضعیت انتشار (Active)</span> <span className="text-xs font-bold text-gray-700">وضعیت انتشار (Active)</span>
<input <input
type="checkbox" type="checkbox"
@ -396,7 +367,7 @@ export default function TestimonialsManager() {
</label> </label>
<label className="flex items-center justify-between p-3 bg-blue-50 rounded-xl border border-blue-200 cursor-pointer"> <label className="flex items-center justify-between p-3 bg-blue-50 rounded-xl border border-blue-200 cursor-pointer">
<span className="text-xs font-bold text-blue-700">نمایش در خانه</span> <span className="text-xs font-bold text-blue-700">🏠 صفحه اصلی</span>
<input <input
type="checkbox" type="checkbox"
checked={formData.showOnHome} checked={formData.showOnHome}
@ -405,12 +376,29 @@ export default function TestimonialsManager() {
/> />
</label> </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-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> </form>
</div> </div>
</Modal> </div>
)} )}
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{deleteTargetId && ( {deleteTargetId && (
<ConfirmModal <ConfirmModal

View File

@ -20,8 +20,6 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import api from '../services/api'; import api from '../services/api';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface TicketMessage { interface TicketMessage {
id: string; id: string;
@ -342,63 +340,47 @@ export default function Tickets() {
{/* Ticket Details & Admin Reply Modal */} {/* Ticket Details & Admin Reply Modal */}
{selectedTicket && ( {selectedTicket && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
isOpen={!!selectedTicket} <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">
onClose={() => setSelectedTicket(null)} {/* Modal Header */}
title={`تیکت ${selectedTicket.ticketNumber}: ${selectedTicket.subject}`} <div className="flex items-start justify-between pb-4 border-b border-gray-100">
icon={MessageSquare} <div>
maxWidth="3xl" <div className="flex items-center gap-2">
footer={ <h3 className="text-lg font-black text-gray-900">{selectedTicket.subject}</h3>
<div className="flex items-center justify-end gap-2.5 w-full"> <span className="px-2.5 py-0.5 bg-canina-blue/10 text-canina-blue font-mono font-bold text-xs rounded-lg">
<Button {selectedTicket.ticketNumber}
variant="secondary" </span>
size="sm" </div>
type="button" <div className="flex items-center gap-3 text-xs text-gray-500 font-medium mt-1">
onClick={() => setSelectedTicket(null)} <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>
</Button> )}
<Button </div>
variant="primary"
size="sm"
type="submit"
form="ticketReplyForm"
startIcon={Send}
isLoading={sendingReply}
disabled={!replyMessage.trim()}
>
ارسال پاسخ به کاربر
</Button>
</div>
}
>
<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 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"> <div className="flex items-center gap-2">
<span className="text-xs font-bold text-gray-500">تغییر وضعیت:</span>
<select <select
value={selectedTicket.status} value={selectedTicket.status}
onChange={(e) => handleUpdateStatus(e.target.value)} onChange={(e) => handleUpdateStatus(e.target.value)}
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" 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"
> >
<option value="OPEN">در انتظار پاسخ</option> <option value="OPEN">در انتظار پاسخ</option>
<option value="IN_PROGRESS">در حال بررسی</option> <option value="IN_PROGRESS">در حال بررسی</option>
<option value="ANSWERED">پاسخ داده شد</option> <option value="ANSWERED">پاسخ داده شد</option>
<option value="CLOSED">بسته شده</option> <option value="CLOSED">بسته شده</option>
</select> </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>
</div> </div>
{/* Messages Chat Scroll */} {/* Messages Chat Scroll */}
<div className="overflow-y-auto space-y-3.5 p-1 max-h-[340px]"> <div className="flex-1 overflow-y-auto space-y-4 p-2 min-h-[250px]">
{(selectedTicket.messages || []).map((msg) => { {(selectedTicket.messages || []).map((msg) => {
const isAdminOrVet = msg.senderRole === 'ADMIN' || msg.senderRole === 'VET_DOCTOR'; const isAdminOrVet = msg.senderRole === 'ADMIN' || msg.senderRole === 'VET_DOCTOR';
return ( return (
@ -406,7 +388,7 @@ export default function Tickets() {
key={msg.id} key={msg.id}
className={`p-4 rounded-2xl max-w-[85%] text-xs font-medium leading-relaxed ${ className={`p-4 rounded-2xl max-w-[85%] text-xs font-medium leading-relaxed ${
isAdminOrVet isAdminOrVet
? 'bg-purple-50 border border-purple-200 text-purple-950 mr-auto' ? 'bg-emerald-50 border border-emerald-200 text-emerald-950 mr-auto'
: 'bg-blue-50 border border-blue-200 text-blue-950 ml-auto' : 'bg-blue-50 border border-blue-200 text-blue-950 ml-auto'
}`} }`}
> >
@ -414,8 +396,8 @@ export default function Tickets() {
<span className="font-black text-[11px] flex items-center gap-1.5"> <span className="font-black text-[11px] flex items-center gap-1.5">
{isAdminOrVet ? ( {isAdminOrVet ? (
<> <>
<Stethoscope className="w-3.5 h-3.5 text-purple-600" /> <Stethoscope className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-purple-700">{msg.senderName} (تیم پزشکی / ادمین)</span> <span className="text-emerald-700">{msg.senderName} (تیم پزشکی / ادمین)</span>
</> </>
) : ( ) : (
<> <>
@ -435,7 +417,7 @@ export default function Tickets() {
</div> </div>
{/* Admin Response Composer */} {/* Admin Response Composer */}
<form id="ticketReplyForm" onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3"> <form onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3">
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<label className="font-bold text-gray-700">ارسال پاسخ دامپزشک / پشتیبانی:</label> <label className="font-bold text-gray-700">ارسال پاسخ دامپزشک / پشتیبانی:</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -457,13 +439,29 @@ export default function Tickets() {
placeholder="متن پاسخ تخصصی به کاربر را وارد کنید..." placeholder="متن پاسخ تخصصی به کاربر را وارد کنید..."
value={replyMessage} value={replyMessage}
onChange={(e) => setReplyMessage(e.target.value)} 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-purple-500 focus:ring-2 focus:ring-purple-200 outline-none resize-none leading-relaxed" 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"
/> />
<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> </form>
</div> </div>
</Modal> </div>
)} )}
</div> </div>
); );
} }

View File

@ -47,8 +47,6 @@ import ImagePreviewModal from '../components/ui/ImagePreviewModal';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import RichTextEditor from '../components/ui/RichTextEditor'; import RichTextEditor from '../components/ui/RichTextEditor';
import IconPickerModal, { ICON_REGISTRY } from '../components/ui/IconPickerModal'; import IconPickerModal, { ICON_REGISTRY } from '../components/ui/IconPickerModal';
import Button from '../components/ui/Button';
// Classic v1 Groupings (Backward Compatibility) // Classic v1 Groupings (Backward Compatibility)
const V1_PAGE_TABS = [ const V1_PAGE_TABS = [
@ -921,16 +919,15 @@ export default function UITexts() {
)} )}
<div className="flex justify-end pt-1"> <div className="flex justify-end pt-1">
<Button <button
type="button" type="button"
variant="primary"
size="xs"
startIcon={Save}
onClick={() => handleSave(field.key)} onClick={() => handleSave(field.key)}
isLoading={isSaving} disabled={isSaving}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-black transition-all flex items-center gap-1.5 shadow-sm cursor-pointer"
> >
ذخیره این فیلد {isSaving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
</Button> <span>ذخیره این فیلد</span>
</button>
</div> </div>
</div> </div>
); );
@ -951,21 +948,22 @@ export default function UITexts() {
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <button
type="button" type="button"
variant="primary"
size="sm"
startIcon={Save}
onClick={handleSaveAll} onClick={handleSaveAll}
disabled={!hasUnsavedChanges} disabled={isSavingAll || !hasUnsavedChanges}
isLoading={isSavingAll} className={`px-5 py-2.5 rounded-xl text-xs font-black flex items-center gap-2 transition-all shadow-sm ${
hasUnsavedChanges
? 'bg-purple-600 hover:bg-purple-700 text-white shadow-purple-200 cursor-pointer animate-pulse'
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
}`}
> >
ذخیره تمامی تغییرات {hasUnsavedChanges ? `(${Object.keys(edits).length})` : ''} {isSavingAll ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
</Button> <span>ذخیره تمامی تغییرات {hasUnsavedChanges ? `(${Object.keys(edits).length})` : ''}</span>
</button>
</div> </div>
</div> </div>
{/* Main Version Switcher: v2 (New Structure) vs v1 (Legacy) */} {/* Main Version Switcher: v2 (New Structure) vs v1 (Legacy) */}
<div className="flex items-center justify-between bg-white p-2 rounded-2xl border border-gray-200 shadow-xs"> <div className="flex items-center justify-between bg-white p-2 rounded-2xl border border-gray-200 shadow-xs">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -265,15 +265,13 @@ export default function Users() {
</div> </div>
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto"> <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)} 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 <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" 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"

View File

@ -4,9 +4,6 @@ import { toast } from 'react-hot-toast';
import api, { BASE_DOMAIN } from '../services/api'; import api, { BASE_DOMAIN } from '../services/api';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface Video { interface Video {
id: string; id: string;
@ -182,17 +179,15 @@ export default function Videos() {
آپلود و انتخاب مستقیم ویدئو/کاور از گالری رسانه یا درج کد آیفرم آپارات/یوتیوب برای مشاوره ویدئویی دامپزشکان کنینا آپلود و انتخاب مستقیم ویدئو/کاور از گالری رسانه یا درج کد آیفرم آپارات/یوتیوب برای مشاوره ویدئویی دامپزشکان کنینا
</p> </p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => handleOpenModal()} 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" />
</Button> <span>افزودن ویدئوی جدید</span>
</button>
</div> </div>
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-xs flex items-center gap-4"> <div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-xs flex items-center gap-4">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="w-5 h-5 text-gray-400 absolute right-4 top-1/2 -translate-y-1/2" /> <Search className="w-5 h-5 text-gray-400 absolute right-4 top-1/2 -translate-y-1/2" />
@ -315,35 +310,22 @@ export default function Videos() {
)} )}
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir">
isOpen={isModalOpen} <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">
onClose={() => setIsModalOpen(false)} <div className="flex items-center justify-between pb-4 border-b border-gray-100 mb-6">
title={editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'} <h3 className="text-lg font-black text-gray-900 flex items-center gap-2">
icon={VideoIcon} <VideoIcon className="w-5 h-5 text-purple-600" />
maxWidth="2xl" {editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'}
footer={ </h3>
<div className="flex justify-end gap-2.5 w-full"> <button
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)} 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> </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"> <div className="space-y-1.5">
<label className="text-xs font-black text-gray-700 block">عنوان ویدئو *</label> <label className="text-xs font-black text-gray-700 block">عنوان ویدئو *</label>
<input <input
@ -422,15 +404,14 @@ export default function Videos() {
placeholder="آدرس تصویر یا انتخاب از گالری..." 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" 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" type="button"
variant="outline"
size="sm"
startIcon={ImageIcon}
onClick={() => setIsThumbnailMediaOpen(true)} 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" />
</Button> <span>انتخاب از گالری</span>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -456,15 +437,14 @@ export default function Videos() {
placeholder="https://... یا کد آیفرم آپارات/یوتیوب" 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" 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" type="button"
variant="outline"
size="sm"
startIcon={Film}
onClick={() => setIsVideoMediaOpen(true)} 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" />
</Button> <span>انتخاب ویدئو</span>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -491,12 +471,27 @@ export default function Videos() {
نمایش به عنوان ویدئوی ویژه در بخش «مشاوره ویدئویی» صفحه اصلی سایت نمایش به عنوان ویدئوی ویژه در بخش «مشاوره ویدئویی» صفحه اصلی سایت
</label> </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 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> </form>
</div> </div>
</Modal> </div>
)} )}
{/* Cover Thumbnail Media Selector Modal */} {/* Cover Thumbnail Media Selector Modal */}
<MediaSelector <MediaSelector
isOpen={isThumbnailMediaOpen} isOpen={isThumbnailMediaOpen}

View File

@ -2,8 +2,6 @@ import { useState, useEffect } from 'react';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
export interface WholesaleRequest { export interface WholesaleRequest {
id: string; id: string;
@ -61,10 +59,10 @@ export default function WholesaleApplications() {
if (!rejectTargetId) return; if (!rejectTargetId) return;
try { try {
await api.put(`/wholesale/reject/${rejectTargetId}`); await api.put(`/wholesale/reject/${rejectTargetId}`);
toast.success('درخواست همکاری رد شد.'); toast.success('درخواست رد شد.');
fetchRequestsData(); fetchRequestsData();
} catch { } catch {
toast.error('خطا در رد درخواست همکاری'); toast.error('خطا در رد درخواست');
} finally { } finally {
setRejectTargetId(null); setRejectTargetId(null);
} }
@ -72,24 +70,20 @@ export default function WholesaleApplications() {
return ( return (
<div className="p-6 font-vazir" dir="rtl"> <div className="p-6 font-vazir" dir="rtl">
<div className="flex justify-between items-center mb-6"> <h1 className="text-2xl font-bold mb-2 text-gray-800">بررسی درخواست‌های ثبت‌نام همکاران B2B (کلینیک‌ها و داروخانه‌ها)</h1>
<div> <p className="text-sm text-gray-500 mb-6">استعلام شماره پروانه نظام دامپزشکی و اعطای دسترسی خرید عمده با تخفیف ۳۰٪ کاتالوگ Canina</p>
<h1 className="text-2xl font-bold text-gray-900">درخواست‌های ارتقا به همکار B2B</h1>
<p className="text-gray-500 text-sm mt-1">تایید یا رد درخواست‌های کاربران عادی برای خرید عمده و دسترسی به تخفیف‌های تجاری</p>
</div>
</div>
{loading ? ( {loading ? (
<div className="text-center py-12 text-gray-500 font-bold">در حال بارگذاری درخواست‌ها...</div> <div className="text-center py-12 text-gray-500 font-bold">در حال بارگذاری درخواست‌ها...</div>
) : ( ) : (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden"> <div className="bg-white rounded-2xl border border-gray-200 overflow-hidden shadow-sm">
<table className="w-full text-right border-collapse"> <table className="w-full text-right">
<thead> <thead className="bg-gray-50 border-b border-gray-200 text-xs font-bold text-gray-500">
<tr className="bg-gray-50/50 border-b border-gray-100 text-xs text-gray-500"> <tr>
<th className="px-6 py-4 font-bold">نام و اطلاعات متقاضی</th> <th className="px-6 py-4">کاربر / ایمیل</th>
<th className="px-6 py-4 font-bold">وضعیت فعلی</th> <th className="px-6 py-4">نقش فعلی</th>
<th className="px-6 py-4 font-bold">تاریخ درخواست</th> <th className="px-6 py-4">تاریخ ثبت‌نام</th>
<th className="px-6 py-4 font-bold text-center">عملیات</th> <th className="px-6 py-4 text-center">عملیات تایید همکار</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100 text-sm"> <tbody className="divide-y divide-gray-100 text-sm">
@ -113,26 +107,21 @@ export default function WholesaleApplications() {
<td className="px-6 py-4 text-xs text-gray-500 font-mono"> <td className="px-6 py-4 text-xs text-gray-500 font-mono">
{new Date(u.createdAt).toLocaleDateString('fa-IR')} {new Date(u.createdAt).toLocaleDateString('fa-IR')}
</td> </td>
<td className="px-6 py-4 text-center"> <td className="px-6 py-4 text-center space-x-2 space-x-reverse">
<div className="flex items-center justify-center gap-2"> {u.role !== 'User_Wholesale' && (
{u.role !== 'User_Wholesale' && ( <button
<Button onClick={() => setApproveTargetId(u.id)}
variant="primary" className="bg-green-600 hover:bg-green-700 text-white font-bold text-xs px-4 py-2 rounded-xl transition-all shadow-sm"
size="xs"
className="!bg-emerald-600 hover:!bg-emerald-700"
onClick={() => setApproveTargetId(u.id)}
>
تایید و اعطای لایسنس B2B
</Button>
)}
<Button
variant="secondary"
size="xs"
onClick={() => setRejectTargetId(u.id)}
> >
رد درخواست تایید و اعطای لایسنس B2B
</Button> </button>
</div> )}
<button
onClick={() => setRejectTargetId(u.id)}
className="bg-gray-100 hover:bg-red-50 hover:text-red-600 text-gray-600 font-bold text-xs px-4 py-2 rounded-xl transition-all"
>
رد درخواست
</button>
</td> </td>
</tr> </tr>
)) ))

View File

@ -5,9 +5,6 @@ import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination'; import Pagination from '../components/ui/Pagination';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface WikiTerm { export interface WikiTerm {
key: string; key: string;
@ -141,14 +138,13 @@ export default function Wiki() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">مدیریت اصطلاحات علمی، مواد تشکیل‌دهنده و خواص درمانی</p> <p className="text-gray-500 font-medium mt-1">مدیریت اصطلاحات علمی، مواد تشکیل‌دهنده و خواص درمانی</p>
</div> </div>
<Button <button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} 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>
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4"> <div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
@ -188,8 +184,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 text-gray-500 text-sm max-w-xs truncate">{t.definition}</td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-2"> <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 cursor-pointer"><Edit2 className="w-4 h-4" /></button> <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 cursor-pointer"><Trash2 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>
</div> </div>
</td> </td>
</tr> </tr>
@ -206,109 +202,99 @@ export default function Wiki() {
</div> </div>
{isModalOpen && ( {isModalOpen && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
isOpen={isModalOpen} <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">
onClose={() => setIsModalOpen(false)} <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
title={editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'} <h3 className="text-xl font-bold text-gray-900">
icon={BookOpen} {editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
maxWidth="2xl" </h3>
footer={ <button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<div className="flex justify-end gap-2.5 w-full"> <XCircle className="w-6 h-6" />
<Button </button>
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="wikiForm"
>
ذخیره
</Button>
</div> </div>
}
>
<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">
<label className="text-sm font-bold text-gray-700">شناسه یکتا (Key) *</label>
<input required type="text" value={formData.key} disabled={!!editingTerm} dir="ltr" onChange={(e) => setFormData({ ...formData, key: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm disabled:bg-gray-100" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">نام اصطلاح (Term) *</label>
<input required type="text" value={formData.term} onChange={(e) => setFormData({ ...formData, term: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
</div>
<div className="space-y-2"> <div className="p-6 overflow-y-auto flex-1">
<label className="text-sm font-bold text-gray-700">توضیح علمی *</label> <form id="wikiForm" onSubmit={handleSave} className="space-y-6">
<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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
</div> <div className="space-y-2">
<label className="text-sm font-bold text-gray-700">شناسه یکتا (Key) *</label>
<input required type="text" value={formData.key} disabled={!!editingTerm} dir="ltr" onChange={(e) => setFormData({ ...formData, key: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm disabled:bg-gray-100" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">نام اصطلاح (Term) *</label>
<input required type="text" value={formData.term} onChange={(e) => setFormData({ ...formData, term: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
</div>
{/* Related Products Selector */} <div className="space-y-2">
<div className="space-y-2 border border-gray-200 p-4 rounded-xl bg-purple-50/40"> <label className="text-sm font-bold text-gray-700">توضیح علمی *</label>
<label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی</label> <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>
<select </div>
onChange={(e) => {
const prodId = e.target.value;
if (prodId && !formData.relatedProducts.includes(prodId)) {
setFormData({ ...formData, relatedProducts: [...formData.relatedProducts, prodId] });
}
}}
className="w-full px-3 py-2 border rounded-xl text-xs bg-white"
>
<option value="">+ انتخاب محصول جهت افزودن به لیست مرتبط</option>
{productsList.map(p => (
<option key={p.id} value={p.id}>{p.nameFa} ({p.artNo})</option>
))}
</select>
<div className="flex flex-wrap gap-2 mt-2"> {/* Related Products Selector (TASK-2.23) */}
{formData.relatedProducts.map(pId => { <div className="space-y-2 border border-gray-200 p-4 rounded-xl bg-purple-50/40">
const prod = productsList.find(p => p.id === pId); <label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی (TASK-2.23)</label>
return ( <select
<span key={pId} className="inline-flex items-center gap-1.5 px-3 py-1 bg-purple-100 text-purple-700 text-xs font-bold rounded-full"> onChange={(e) => {
<span>{prod ? prod.nameFa : pId}</span> const prodId = e.target.value;
<button if (prodId && !formData.relatedProducts.includes(prodId)) {
type="button" setFormData({ ...formData, relatedProducts: [...formData.relatedProducts, prodId] });
onClick={() => setFormData({ ...formData, relatedProducts: formData.relatedProducts.filter(id => id !== pId) })} }
className="hover:text-red-600 font-black text-sm cursor-pointer" }}
> className="w-full px-3 py-2 border rounded-xl text-xs bg-white"
× >
</button> <option value="">+ انتخاب محصول جهت افزودن به لیست مرتبط</option>
</span> {productsList.map(p => (
); <option key={p.id} value={p.id}>{p.nameFa} ({p.artNo})</option>
})} ))}
</div> </select>
</div>
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50"> <div className="flex flex-wrap gap-2 mt-2">
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4> {formData.relatedProducts.map(pId => {
<div className="space-y-2"> const prod = productsList.find(p => p.id === pId);
<label className="text-xs font-bold text-gray-700">عنوان متا</label> return (
<input type="text" value={formData.metaTitle} onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" /> <span key={pId} className="inline-flex items-center gap-1.5 px-3 py-1 bg-purple-100 text-purple-700 text-xs font-bold rounded-full">
<span>{prod ? prod.nameFa : pId}</span>
<button
type="button"
onClick={() => setFormData({ ...formData, relatedProducts: formData.relatedProducts.filter(id => id !== pId) })}
className="hover:text-red-600 font-black text-sm"
>
×
</button>
</span>
);
})}
</div>
</div> </div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">کلمات کلیدی</label> <div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
<input type="text" value={formData.keywords} onChange={(e) => setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" /> <h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">عنوان متا</label>
<input type="text" value={formData.metaTitle} onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">کلمات کلیدی</label>
<input type="text" value={formData.keywords} onChange={(e) => setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">توضیحات متا</label>
<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> </div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">توضیحات متا</label> </form>
<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>
</div> <div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
</form> <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> </div>
)} )}
<ConfirmModal <ConfirmModal
isOpen={!!deleteTargetKey} isOpen={!!deleteTargetKey}
title="حذف اصطلاح علمی" title="حذف اصطلاح علمی"

View File

@ -26,9 +26,6 @@ import {
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import api from '../services/api'; import api from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export default function ZibalPortalPage() { export default function ZibalPortalPage() {
const [activeMainTab, setActiveMainTab] = useState<'refunds' | 'checkouts' | 'queue' | 'gateway_transactions'>('refunds'); const [activeMainTab, setActiveMainTab] = useState<'refunds' | 'checkouts' | 'queue' | 'gateway_transactions'>('refunds');
@ -381,27 +378,25 @@ export default function ZibalPortalPage() {
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6"> <div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
{/* Top Bar with Filter & Search Controls */} {/* 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 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-2.5"> <div className="flex items-center gap-3">
<Button <button
variant="primary"
size="sm"
startIcon={Download}
onClick={() => downloadCSV(refunds, 'zibal-refunds')} 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" />
</Button> <span>دانلود گزارش</span>
</button>
<Button <button
variant="outline"
size="sm"
onClick={fetchRefunds} onClick={fetchRefunds}
isLoading={loadingRefunds} disabled={loadingRefunds}
startIcon={RefreshCw} className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
title="بروزرسانی" title="بروزرسانی"
/> >
<RefreshCw className={`w-4 h-4 ${loadingRefunds ? 'animate-spin' : ''}`} />
</button>
</div> </div>
{/* Filter Inputs Bar */} {/* Filter Inputs Bar */}
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{/* Date Filter */} {/* Date Filter */}
@ -639,27 +634,25 @@ export default function ZibalPortalPage() {
{activeMainTab === 'checkouts' && ( {activeMainTab === 'checkouts' && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6"> <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 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-2.5"> <div className="flex items-center gap-3">
<Button <button
variant="primary"
size="sm"
startIcon={Download}
onClick={() => downloadCSV(checkouts, 'zibal-checkouts')} 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" />
</Button> <span>دانلود گزارش</span>
</button>
<Button <button
variant="outline"
size="sm"
onClick={fetchCheckouts} onClick={fetchCheckouts}
isLoading={loadingCheckouts} disabled={loadingCheckouts}
startIcon={RefreshCw} className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
title="بروزرسانی" title="بروزرسانی"
/> >
<RefreshCw className={`w-4 h-4 ${loadingCheckouts ? 'animate-spin' : ''}`} />
</button>
</div> </div>
{/* Filter Inputs Bar */} {/* Filter Inputs Bar */}
<div className="flex flex-wrap items-center gap-3"> <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"> <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">
@ -858,27 +851,25 @@ export default function ZibalPortalPage() {
{activeMainTab === 'gateway_transactions' && ( {activeMainTab === 'gateway_transactions' && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6"> <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 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-2.5"> <div className="flex items-center gap-3">
<Button <button
variant="primary"
size="sm"
startIcon={Download}
onClick={() => downloadCSV(gatewayTxList, 'zibal-ipg-transactions')} 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" />
</Button> <span>دانلود گزارش درگاه</span>
</button>
<Button <button
variant="outline"
size="sm"
onClick={fetchGatewayTx} onClick={fetchGatewayTx}
isLoading={loadingGatewayTx} disabled={loadingGatewayTx}
startIcon={RefreshCw} className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
title="بروزرسانی" title="بروزرسانی"
/> >
<RefreshCw className={`w-4 h-4 ${loadingGatewayTx ? 'animate-spin' : ''}`} />
</button>
</div> </div>
{/* Filter Inputs */} {/* Filter Inputs */}
<div className="flex flex-wrap items-center gap-3"> <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"> <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">
@ -1022,40 +1013,25 @@ export default function ZibalPortalPage() {
)} )}
{/* ======================================================== */} {/* ======================================================== */}
{/* MODAL: NEW REFUND SUBMIT */} {/* MODAL: SUBMIT NEW REFUND / REVERSE */}
{/* ======================================================== */} {/* ======================================================== */}
{newRefundModal && ( {newRefundModal && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
isOpen={newRefundModal} <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">
onClose={() => setNewRefundModal(false)} <div className="flex items-center justify-between border-b border-gray-100 pb-4">
title="ثبت و ارسال استرداد وجه جدید (Refund / Reverse)" <h3 className="text-base font-black text-gray-900 flex items-center gap-2">
icon={RotateCcw} <RotateCcw className="w-5 h-5 text-rose-600" />
maxWidth="lg" ثبت درخواست استرداد وجه (Refund / Reverse)
footer={ </h3>
<div className="flex justify-end gap-2.5 w-full"> <button
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setNewRefundModal(false)} 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> </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> <div>
<label className="block font-bold text-gray-700 mb-1">شماره تراکنش شاپرک (Track ID) *</label> <label className="block font-bold text-gray-700 mb-1">شماره تراکنش شاپرک (Track ID) *</label>
<input <input
@ -1116,38 +1092,47 @@ export default function ZibalPortalPage() {
اولویت با Reverse باشد (برگشت آنی قبل از تسویه شاپرک) اولویت با Reverse باشد (برگشت آنی قبل از تسویه شاپرک)
</label> </label>
</div> </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> </form>
</div> </div>
</Modal> </div>
)} )}
{/* ======================================================== */} {/* ======================================================== */}
{/* MODAL: DETAIL / INQUIRY VIEWER */} {/* MODAL: DETAIL / INQUIRY VIEWER */}
{/* ======================================================== */} {/* ======================================================== */}
{detailModalItem && ( {detailModalItem && (
<Modal <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
isOpen={!!detailModalItem} <div className="bg-white rounded-3xl w-full max-w-lg border border-gray-200 shadow-2xl p-6 space-y-4">
onClose={() => setDetailModalItem(null)} <div className="flex items-center justify-between border-b border-gray-100 pb-3">
title={detailModalItem.title} <h3 className="font-black text-sm text-gray-900">{detailModalItem.title}</h3>
maxWidth="lg" <button onClick={() => setDetailModalItem(null)} className="text-gray-400 font-bold text-lg">
footer={ ✕
<div className="flex justify-end w-full"> </button>
<Button
variant="secondary"
size="sm"
onClick={() => setDetailModalItem(null)}
>
بستن
</Button>
</div> </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 className="bg-slate-900 text-emerald-400 p-4 rounded-xl text-[11px] font-mono overflow-x-auto dir-ltr"> </pre>
{JSON.stringify(detailModalItem.data, null, 2)} </div>
</pre> </div>
</Modal>
)} )}
</div> </div>
); );
} }