Compare commits

...

5 Commits

34 changed files with 1911 additions and 1632 deletions

View File

@ -42,32 +42,33 @@ 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 // Size styling - crisp, modern and compact
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.5 py-1.5 text-xs gap-2', sm: 'px-3 py-1.5 text-xs gap-1.5',
md: 'px-4 py-2.5 text-xs sm:text-sm gap-2', md: 'px-3.5 py-2 text-xs sm:text-sm gap-2',
lg: 'px-6 py-3 text-sm sm:text-base gap-2.5 shadow-md', lg: 'px-5 py-2.5 text-sm sm:text-base gap-2 shadow-sm',
}; };
// Variant styling // Variant styling
const variantClasses: Record<ButtonVariant, string> = { const variantClasses: Record<ButtonVariant, string> = {
primary: primary:
'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm shadow-indigo-200 focus:ring-indigo-500 border border-indigo-600', 'bg-purple-600 hover:bg-purple-700 text-white shadow-xs focus:ring-purple-500 border border-purple-600',
secondary: 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-sm shadow-rose-200 focus:ring-rose-500 border border-rose-600', 'bg-rose-600 hover:bg-rose-700 text-white shadow-xs focus:ring-rose-500 border border-rose-600',
success: success:
'bg-emerald-600 hover:bg-emerald-700 text-white shadow-sm shadow-emerald-200 focus:ring-emerald-500 border border-emerald-600', 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-xs focus:ring-emerald-500 border border-emerald-600',
warning: warning:
'bg-amber-500 hover:bg-amber-600 text-white shadow-sm shadow-amber-200 focus:ring-amber-400 border border-amber-500', 'bg-amber-500 hover:bg-amber-600 text-white shadow-xs focus:ring-amber-400 border border-amber-500',
outline: outline:
'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-indigo-500', 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-purple-500',
ghost: 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,8 +3,13 @@ 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');
@ -277,12 +282,13 @@ 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>
)) ))
@ -296,13 +302,14 @@ 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">
@ -360,21 +367,41 @@ export default function B2BManager() {
{/* Inquiry Review Modal */} {/* Inquiry Review Modal */}
{selectedInquiry && ( {selectedInquiry && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200"> isOpen={!!selectedInquiry}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setSelectedInquiry(null)}
<h3 className="text-lg font-bold text-gray-900"> title={`بررسی درخواست B2B (${selectedInquiry.companyName})`}
بررسی درخواست B2B ({selectedInquiry.companyName}) icon={Building2}
</h3> maxWidth="lg"
<button onClick={() => setSelectedInquiry(null)} className="text-gray-400 hover:text-red-500 transition-colors"> footer={
<X className="w-6 h-6" /> <div className="flex justify-end gap-2.5 w-full">
</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="font-bold text-gray-600">شخص رابط: </span>{selectedInquiry.contactName} ({selectedInquiry.phone})</div> <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.message}</div> <div><span className="text-gray-500">تلفن / ایمیل: </span><span className="font-mono text-gray-800">{selectedInquiry.phone} - {selectedInquiry.email}</span></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>
@ -401,43 +428,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>
</div> </Modal>
)} )}
{/* Partner Account Setup Modal */} {/* Partner Account Setup Modal */}
{isPartnerModalOpen && ( {isPartnerModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200"> isOpen={isPartnerModalOpen}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setIsPartnerModalOpen(false)}
<h3 className="text-lg font-bold text-gray-900"> title="تعریف حساب همکار B2B جدید"
تعریف حساب همکار B2B جدید icon={Building2}
</h3> maxWidth="lg"
<button onClick={() => setIsPartnerModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors"> footer={
<X className="w-6 h-6" /> <div className="flex justify-end gap-2.5 w-full">
</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
@ -498,28 +525,11 @@ 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>
</div> </Modal>
)} )}
</div> </div>
); );
} }

View File

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

View File

@ -6,6 +6,10 @@ 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?: {
@ -134,13 +138,14 @@ 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">
@ -196,8 +201,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"><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 cursor-pointer"><Edit2 className="w-4 h-4" /></button>
<button onClick={() => setDeleteTargetId(blog.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"><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 cursor-pointer"><Trash2 className="w-4 h-4" /></button>
</div> </div>
</td> </td>
</tr> </tr>
@ -214,82 +219,99 @@ export default function Blogs() {
</div> </div>
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<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"> isOpen={isModalOpen}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setIsModalOpen(false)}
<h3 className="text-xl font-bold text-gray-900"> title={editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
{editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'} icon={FileText}
</h3> maxWidth="4xl"
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors"> footer={
<XCircle className="w-6 h-6" /> <div className="flex justify-end gap-2.5 w-full">
</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"> >
<form id="blogForm" onSubmit={handleSave} className="space-y-6"> <div className="space-y-6 text-right font-vazir">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <form id="blogForm" onSubmit={handleSave} className="space-y-6">
<div className="space-y-2"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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>
<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> <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">
<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="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
<div className="space-y-4"> <h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
<label className="text-sm font-bold text-gray-700">تصویر شاخص</label> <div className="space-y-2">
<div className="flex items-center gap-4"> <label className="text-xs font-bold text-gray-700">عنوان متا</label>
{formData.imageUrl ? ( <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" />
<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">
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50"> <label className="text-xs font-bold text-gray-700">کلمات کلیدی</label>
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4> <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 className="space-y-2"> </div>
<label className="text-xs font-bold text-gray-700">عنوان متا</label> <div className="space-y-2">
<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" /> <label className="text-xs font-bold text-gray-700">توضیحات متا</label>
</div> <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 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>
</div> </Modal>
)} )}
<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,6 +6,10 @@ 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;
@ -127,15 +131,17 @@ 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">
@ -197,10 +203,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"> <button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer">
<Edit2 className="w-4 h-4" /> <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"> <button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
@ -220,120 +226,120 @@ export default function Categories() {
{/* Modal */} {/* Modal */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<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"> isOpen={isModalOpen}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setIsModalOpen(false)}
<h3 className="text-xl font-bold text-gray-900"> title={editingCategory ? 'ویرایش دسته‌بندی' : 'افزودن دسته‌بندی جدید'}
{editingCategory ? 'ویرایش دسته‌بندی' : 'افزودن دسته‌بندی جدید'} icon={FolderTree}
</h3> maxWidth="2xl"
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors"> footer={
<X className="w-6 h-6" /> <div className="flex justify-end gap-2.5 w-full">
</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="space-y-6 text-right font-vazir">
<form id="categoryForm" onSubmit={handleSave} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<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="p-6 overflow-y-auto flex-1"> <div className="space-y-2">
<form id="categoryForm" onSubmit={handleSave} className="space-y-6"> <label className="text-sm font-bold text-gray-700">تصویر دسته‌بندی</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <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">نام دسته‌بندی *</label> <label className="text-sm font-bold text-gray-700">عنوان متا (Meta Title)</label>
<input <input
required
type="text" type="text"
value={formData.name} value={formData.metaTitle}
onChange={(e) => setFormData({...formData, name: e.target.value})} 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" 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">اسلاگ (Slug) *</label> <label className="text-sm font-bold text-gray-700">توضیحات متا (Meta Description)</label>
<input <textarea
required value={formData.metaDescription}
type="text" onChange={(e) => setFormData({...formData, metaDescription: e.target.value})}
value={formData.slug} rows={2}
dir="ltr" className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
onChange={(e) => setFormData({...formData, slug: e.target.value})} ></textarea>
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>
<div className="space-y-2"> </form>
<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>
</div> </Modal>
)} )}
{/* Media Selector Modal */} {/* Media Selector Modal */}
<MediaSelector <MediaSelector
isOpen={isMediaSelectorOpen} isOpen={isMediaSelectorOpen}
onClose={() => setIsMediaSelectorOpen(false)} onClose={() => setIsMediaSelectorOpen(false)}

View File

@ -1,6 +1,10 @@
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;
@ -153,16 +157,17 @@ 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>
))} ))}
@ -220,21 +225,45 @@ 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 && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4 z-50"> <Modal
<div className="bg-white rounded-2xl p-6 max-w-lg w-full shadow-xl"> isOpen={!!selectedSub}
<h3 className="text-lg font-bold text-gray-900 mb-4">جزئیات پیام {selectedSub.name}</h3> onClose={() => setSelectedSub(null)}
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>}
@ -268,24 +297,11 @@ 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>
</div> </Modal>
)} )}
</div> </div>
); );
} }

View File

@ -165,12 +165,17 @@ 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 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"> <Button
<Plus className="w-5 h-5" /> variant="primary"
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,6 +5,9 @@ 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;
@ -165,13 +168,14 @@ 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" /> افزودن پزشک جدید
<span>افزودن پزشک جدید</span> </Button>
</button>
</div> </div>
{/* Filter / Search */} {/* Filter / Search */}
@ -203,13 +207,14 @@ 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">
@ -228,23 +233,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>
{/* Profile Header */} {/* Avatar & Main Info */}
<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 flex items-center justify-center overflow-hidden flex-shrink-0 relative"> <div className="w-16 h-16 rounded-2xl bg-purple-50 border border-purple-100 overflow-hidden flex items-center justify-center shrink-0">
{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}`}
@ -252,199 +257,228 @@ export default function DoctorsManager() {
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
<UserCheck className="w-7 h-7 text-purple-400" /> <UserCheck className="w-8 h-8 text-purple-400" />
)} )}
</div> </div>
<div> <div>
<h3 className="font-extrabold text-gray-900 text-base leading-tight mb-1">{doc.name}</h3> <h4 className="font-extrabold text-gray-900 text-base">{doc.name}</h4>
<p className="text-xs text-purple-700 font-bold">{doc.title || 'دامپزشک'}</p> <p className="text-xs text-purple-600 font-bold mt-0.5">{doc.title || 'پزشک متخصص'}</p>
{doc.clinicName && ( {doc.clinicName && (
<p className="text-xs text-gray-400 font-medium mt-0.5">{doc.clinicName}</p> <p className="text-[11px] text-gray-400 mt-0.5 flex items-center gap-1">
<ShieldCheck className="w-3 h-3 text-gray-400" />
{doc.clinicName}
</p>
)} )}
</div> </div>
</div> </div>
{/* Bio snippet */} {/* Bio Summary */}
{doc.bio && ( {doc.bio && (
<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"> <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">
{doc.bio} {doc.bio}
</p> </p>
)} )}
{/* CV Button */} {/* Contact & CV Trigger */}
{doc.cv && ( <div className="flex items-center gap-2 pt-2 border-t border-gray-50 text-xs text-gray-400">
<button {doc.phone && (
onClick={() => setIsCvDetailsOpen(doc)} <span className="flex items-center gap-1 bg-gray-50 px-2 py-1 rounded-lg">
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" <Phone className="w-3 h-3 text-purple-500" />
> <span className="font-mono text-[11px]">{doc.phone}</span>
<FileText className="w-3.5 h-3.5" /> </span>
<span>مشاهده رزومه و سوابق علمی (CV)</span> )}
</button> {doc.instagram && (
)} <a
</div> href={`https://instagram.com/${doc.instagram.replace('@', '')}`}
target="_blank"
{/* Footer info */} rel="noreferrer"
<div className="pt-4 border-t border-gray-100 flex items-center justify-between text-xs text-gray-400 font-medium"> className="flex items-center gap-1 bg-pink-50 text-pink-600 hover:bg-pink-100 px-2 py-1 rounded-lg transition-colors"
<span>{doc._count?.videos || 0} ویدئوی آموزشی</span> >
{doc.phone && <span className="font-mono">{doc.phone}</span>} <Globe className="w-3 h-3" />
<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>
)} )}
{/* Create / Edit Modal */} {/* Modal Add/Edit */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"> <Modal
<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"> isOpen={isModalOpen}
{/* Modal Header */} onClose={() => setIsModalOpen(false)}
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50"> title={editingDoctor ? 'ویرایش اطلاعات پزشک' : 'افزودن پزشک جدید به کادر علمی'}
<div className="flex items-center gap-3"> icon={UserCheck}
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center"> maxWidth="2xl"
<UserCheck 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"> variant="secondary"
{editingDoctor ? 'ویرایش اطلاعات پزشک' : 'افزودن پزشک جدید'} size="sm"
</h3> type="button"
<p className="text-xs text-gray-500 font-medium">مشخصات هویتی، آواتار و رزومه علمی پزشک</p>
</div>
</div>
<button
onClick={() => setIsModalOpen(false)} 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> </Button>
<Button
variant="primary"
size="sm"
type="submit"
form="doctorForm"
>
{editingDoctor ? 'ذخیره تغییرات' : 'افزودن پزشک'}
</Button>
</div> </div>
}
{/* Form */} >
<form onSubmit={handleSave} className="p-6 overflow-y-auto flex-1 space-y-4"> <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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Name */}
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1"> <label className="block text-xs font-bold text-gray-700 mb-1.5">نام و نام خانوادگی *</label>
نام و نام خانوادگی پزشک <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-sm focus:outline-none focus:border-purple-600" 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>
{/* Title */}
<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.5">عنوان / تخصص *</label>
<input <input
type="text" type="text"
placeholder="مثال: متخصص ارتوپدی و تغذیه دامپزشکی" required
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-sm focus:outline-none focus:border-purple-600" 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>
</div> </div>
{/* Clinic Name */} <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">نام بیمارستان / کلینیک یا دانشگاه</label> <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-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">
<input <input
type="text" type="text"
placeholder="https://..." 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>
<label className="block text-xs font-bold text-gray-700 mb-1.5">تصویر آواتار / عکس پرتره</label>
<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
type="text"
placeholder="آدرس تصویر یا انتخاب از رسانه‌ها..."
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-sm focus:outline-none focus:border-purple-600" className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
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" /> گالری رسانه
<span>انتخاب از رسانه‌ها</span> </Button>
</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 */} {/* Bio & CV */}
<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.5">معرفی کوتاه (Bio)</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 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600" 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"
/> />
</div> </div>
{/* CV */}
<div> <div>
<label className="block text-xs font-bold text-gray-700 mb-1"> <label className="block text-xs font-bold text-gray-700 mb-1.5">رزومه کامل و مقالات علمی (CV)</label>
رزومه، سوابق تحصیلی و مقالات علمی (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 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" 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"
/> />
</div> </div>
{/* Contacts */} {/* Social & Contact */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<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"
placeholder="۰۹۱۲..." dir="ltr"
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" className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
/> />
</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"
placeholder="doctor@example.com" dir="ltr"
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" className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
/> />
</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"
placeholder="@dr_..." dir="ltr"
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"
@ -464,56 +498,38 @@ 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>
</div> </Modal>
)} )}
{/* CV Detail Modal */} {/* CV Detail Modal */}
{isCvDetailsOpen && ( {isCvDetailsOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"> <Modal
<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"> isOpen={!!isCvDetailsOpen}
<div className="flex items-center justify-between border-b border-gray-100 pb-4 mb-4"> onClose={() => setIsCvDetailsOpen(null)}
<div className="flex items-center gap-3"> title={`رزومه علمی — ${isCvDetailsOpen.name}`}
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center"> icon={FileText}
<FileText className="w-5 h-5" /> maxWidth="lg"
</div> footer={
<div> <div className="flex justify-end w-full">
<h3 className="font-extrabold text-gray-900 text-base">{isCvDetailsOpen.name}</h3> <Button
<p className="text-xs text-purple-600 font-bold">{isCvDetailsOpen.title}</p> variant="secondary"
</div> size="sm"
</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>
</div> </Modal>
)} )}
{/* Media Selector Modal for Avatar */} {/* Media Selector Modal for Avatar */}
<MediaSelector <MediaSelector
isOpen={isAvatarMediaOpen} isOpen={isAvatarMediaOpen}

View File

@ -4,6 +4,9 @@ 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;
@ -129,13 +132,14 @@ 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 */}
@ -147,7 +151,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-orange-500" /> <Spinner size="lg" className="text-purple-600" />
</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">
@ -155,56 +159,71 @@ 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-orange-500 font-bold hover:underline text-sm" className="mt-4 text-purple-600 font-bold hover:underline text-sm cursor-pointer"
> >
اولین سوال را اضافه کنید اولین سوال را اضافه کنید
</button> </button>
</div> </div>
) : ( ) : (
faqs.map((faq, idx) => ( faqs.map((faq) => (
<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' : 'border-dashed border-gray-300 opacity-60' faq.isActive ? 'border-gray-200 shadow-sm' : 'border-gray-100 opacity-60 bg-gray-50'
}`} }`}
> >
<div className="flex items-start gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-2 shrink-0 mt-1"> <div className="flex items-start gap-3 flex-1">
<GripVertical className="w-4 h-4 text-gray-300" /> <div className="p-2 bg-orange-50 text-orange-600 rounded-xl mt-0.5 shrink-0">
<span className="text-xs font-black text-gray-400 w-5 text-center">{idx + 1}</span> <HelpCircle className="w-5 h-5" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="space-y-2 flex-1">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 flex-wrap">
<span className="font-black text-gray-900 text-sm">{faq.question}</span> <span className="text-xs font-bold px-2 py-0.5 bg-gray-100 text-gray-600 rounded-md">
{!faq.isActive && ( ترتیب: {faq.order}
<span className="px-2 py-0.5 bg-gray-100 text-gray-500 text-[10px] font-bold rounded"> </span>
غیرفعال <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>
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2">{faq.answer}</p>
</div> </div>
<div className="flex items-center gap-1 shrink-0">
<div className="flex items-center gap-2 shrink-0">
<button <button
onClick={() => toggleActive(faq)} onClick={() => toggleActive(faq)}
className={`p-2 rounded-lg transition-colors ${ title={faq.isActive ? 'غیرفعال‌سازی' : 'فعال‌سازی'}
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-50' : 'text-gray-400 hover:bg-gray-100'
}`} }`}
title={faq.isActive ? 'غیرفعال کردن' : 'فعال کردن'}
> >
{faq.isActive ? <ToggleRight className="w-5 h-5" /> : <ToggleLeft className="w-5 h-5" />} {faq.isActive ? (
<ToggleRight className="w-5 h-5" />
) : (
<ToggleLeft className="w-5 h-5" />
)}
</button> </button>
<button <button
onClick={() => openModal(faq)} onClick={() => openModal(faq)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" className="p-2 text-blue-600 hover:bg-blue-50 rounded-xl transition-colors cursor-pointer"
> >
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => setDeleteTargetId(faq.id)} onClick={() => setDeleteTargetId(faq.id)}
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors" className="p-2 text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
@ -215,31 +234,49 @@ export default function FAQManager() {
)} )}
</div> </div>
{/* Form Modal */} {/* Modal */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<div className="bg-white rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden"> isOpen={isModalOpen}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setIsModalOpen(false)}
<h3 className="text-lg font-bold text-gray-900"> title={editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'}
{editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'} icon={HelpCircle}
</h3> maxWidth="2xl"
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg"> footer={
<X className="w-5 h-5 text-gray-500" /> <div className="flex justify-end gap-2.5 w-full">
</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
type="text"
required required
type="text"
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-orange-400 outline-none text-sm" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/> />
</div> </div>
@ -253,7 +290,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-orange-400 outline-none text-sm resize-none" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm resize-none"
/> />
</div> </div>
@ -264,44 +301,28 @@ 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-orange-400 outline-none text-sm" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/> />
</div> </div>
<div className="flex items-end"> <div className="flex items-end">
<label className="flex items-center justify-between p-3 bg-orange-50 rounded-xl border border-orange-200 cursor-pointer w-full"> <label className="flex items-center justify-between p-3 bg-purple-50 rounded-xl border border-purple-200 cursor-pointer w-full">
<span className="text-xs font-bold text-orange-700">✅ فعال و قابل نمایش</span> <span className="text-xs font-bold text-purple-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-orange-500 focus:ring-orange-400" className="rounded text-purple-600 focus:ring-purple-500"
/> />
</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>
</div> </Modal>
)} )}
{deleteTargetId && ( {deleteTargetId && (
<ConfirmModal <ConfirmModal
isOpen={true} isOpen={true}

View File

@ -4,8 +4,10 @@ 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,
@ -169,13 +171,15 @@ 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">
@ -266,17 +270,19 @@ 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"
disabled={isSaving} variant="primary"
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" size="md"
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,8 +5,11 @@ 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);
@ -164,15 +167,17 @@ 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">
@ -244,21 +249,39 @@ export default function IngredientsManager() {
</div> </div>
</div> </div>
{/* Form Modal */} {/* Modal */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<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"> isOpen={isModalOpen}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setIsModalOpen(false)}
<h3 className="text-lg font-bold text-gray-900"> title={editingIngredient ? 'ویرایش ترکیب دانشنامه' : 'افزودن ترکیب جدید'}
{editingIngredient ? 'ویرایش ترکیب دانشنامه' : 'افزودن ترکیب جدید'} icon={FlaskConical}
</h3> maxWidth="2xl"
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors"> footer={
<X className="w-6 h-6" /> <div className="flex justify-end gap-2.5 w-full">
</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="grid grid-cols-2 gap-4"> <div className="space-y-6 text-right font-vazir">
<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
@ -285,7 +308,7 @@ export default function IngredientsManager() {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-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">شناسه یکتا (slug)</label> <label className="block text-sm font-bold text-gray-700 mb-1">شناسه یکتا (slug)</label>
<input <input
@ -312,28 +335,29 @@ export default function IngredientsManager() {
</div> </div>
<div> <div>
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر ترکیب (imageUrl)</label> <label className="block text-sm font-bold text-gray-700 mb-1">تصویر ترکیب</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="https://... یا /uploads/..." placeholder="آدرس تصویر یا انتخاب از رسانه‌ها..."
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">خواص و مزایا (benefits array)</label> <label className="block text-sm font-bold text-gray-700 mb-1">خواص و مزایا</label>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
@ -347,22 +371,23 @@ export default function IngredientsManager() {
} }
}} }}
placeholder="تایپ کنید و اینتر بزنید (مثال: ترمیم غضروف)" placeholder="تایپ کنید و اینتر بزنید (مثال: ترمیم غضروف)"
className="flex-1 px-4 py-2 rounded-xl border border-gray-200 text-xs outline-none" className="flex-1 px-4 py-2 rounded-xl border border-gray-200 text-xs outline-none focus:border-purple-500"
/> />
<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"> <button type="button" onClick={() => removeBenefitTag(b)} className="hover:text-red-500 cursor-pointer">
<X className="w-3 h-3" /> <X className="w-3 h-3" />
</button> </button>
</span> </span>
@ -381,29 +406,12 @@ 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>
</div> </Modal>
)} )}
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{deleteTargetId && ( {deleteTargetId && (
<ConfirmModal <ConfirmModal

View File

@ -6,6 +6,12 @@ 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;
@ -299,26 +305,20 @@ 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}
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" isLoading={isUploading}
> >
{isUploading ? ( {isUploading ? `در حال آپلود ${uploadProgress ? `(${uploadProgress}٪)` : ''}...` : 'آپلود فایل جدید'}
<> </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 && (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs"> <Modal
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden p-6 space-y-4"> isOpen={!!editingSeoMedia}
<div className="flex items-center justify-between border-b pb-3"> onClose={() => setEditingSeoMedia(null)}
<h3 className="text-lg font-bold text-gray-900">تنظیمات سئوی تصویر</h3> title="تنظیمات سئوی تصویر"
<button onClick={() => setEditingSeoMedia(null)} className="text-gray-400 hover:text-red-500"> icon={ImageIcon}
<X className="w-5 h-5" /> maxWidth="md"
</button> footer={
</div> <div className="flex justify-end gap-2.5 w-full">
<Button
<div className="space-y-3 text-right"> variant="secondary"
<div> size="sm"
<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
type="button" variant="primary"
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>
</div> </Modal>
)} )}
<ConfirmModal <ConfirmModal
isOpen={!!deleteTargetId} isOpen={!!deleteTargetId}
title="حذف تصویر" title="حذف تصویر"

View File

@ -7,6 +7,10 @@ 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;
@ -174,15 +178,17 @@ 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) => (
@ -292,27 +298,45 @@ export default function MenuManager() {
</div> </div>
)} )}
{/* Form Modal */} {/* Modal */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<div className="bg-white rounded-2xl w-full max-w-xl shadow-2xl overflow-hidden"> isOpen={isModalOpen}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setIsModalOpen(false)}
<h3 className="text-lg font-bold text-gray-900"> title={editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'}
{editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'} icon={Menu}
</h3> maxWidth="xl"
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg"> footer={
<X className="w-5 h-5 text-gray-500" /> <div className="flex justify-end gap-2.5 w-full">
</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-blue-400 outline-none text-sm bg-white" className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
> >
{MENU_TABS.map((tab) => ( {MENU_TABS.map((tab) => (
<option key={tab.key} value={tab.key}>{tab.label}</option> <option key={tab.key} value={tab.key}>{tab.label}</option>
@ -324,7 +348,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-blue-400 outline-none text-sm bg-white" className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
> >
<option value="">— آیتم اصلی —</option> <option value="">— آیتم اصلی —</option>
{parentItems.map((p) => ( {parentItems.map((p) => (
@ -342,7 +366,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-blue-400 outline-none text-sm" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/> />
</div> </div>
@ -355,7 +379,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-blue-400 outline-none text-sm font-mono" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
/> />
</div> </div>
@ -367,7 +391,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-blue-400 outline-none text-sm" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/> />
</div> </div>
<div> <div>
@ -377,7 +401,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-blue-400 outline-none text-sm" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/> />
</div> </div>
</div> </div>
@ -389,7 +413,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-blue-400 outline-none text-sm" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/> />
</div> </div>
<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">
@ -411,29 +435,12 @@ 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>
</div> </Modal>
)} )}
{deleteTargetId && ( {deleteTargetId && (
<ConfirmModal <ConfirmModal
isOpen={true} isOpen={true}

View File

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

View File

@ -5,8 +5,11 @@ 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;
@ -243,12 +246,13 @@ 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>
); );
@ -261,27 +265,36 @@ export default function PrescriptionsManager() {
{/* Review Modal */} {/* Review Modal */}
{selectedRx && ( {selectedRx && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir" dir="rtl"> <Modal
<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"> isOpen={!!selectedRx}
<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"> onClose={() => setSelectedRx(null)}
<div className="flex items-center gap-3"> title="بررسی و ثبت نظر تخصصی نسخه"
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center"> icon={FileText}
<FileText className="w-5 h-5" /> maxWidth="2xl"
</div> footer={
<div> <div className="flex justify-end gap-2.5 w-full">
<h3 className="text-lg font-bold text-gray-900">بررسی و ثبت نظر تخصصی نسخه</h3> <Button
<p className="text-xs text-gray-500">کد نسخه: {selectedRx.id}</p> variant="secondary"
</div> size="sm"
</div> type="button"
<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">
@ -295,58 +308,68 @@ export default function PrescriptionsManager() {
</div> </div>
)} )}
{selectedRx.notes && ( {selectedRx.notes && (
<div className="pt-2 border-t border-gray-200 text-gray-700"> <div className="pt-2 border-t border-gray-200 text-gray-500">
<strong className="block mb-0.5 text-gray-900">یادداشت کاربر:</strong> <span className="font-bold text-gray-700">توضیحات کاربر: </span>
{selectedRx.notes} {selectedRx.notes}
</div> </div>
)} )}
</div> <div className="pt-2 border-t border-gray-200 flex justify-between items-center">
<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"
onClick={() => setReviewStatus('APPROVED')} variant="outline"
className={`py-3 px-4 rounded-xl border font-bold text-xs flex items-center justify-center gap-2 transition-all cursor-pointer ${ size="xs"
reviewStatus === 'APPROVED' startIcon={Eye}
? 'border-green-500 bg-green-50 text-green-700 shadow-xs font-black' onClick={() => setPreviewImageUrl(getFileFullUrl(selectedRx.fileUrl))}
: '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">
<button تاریخ ثبت: {selectedRx.createdAt ? new Date(selectedRx.createdAt).toLocaleDateString('fa-IR') : '-'}
type="button" </span>
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>
{/* Prescribed Products Selector */} {/* Action Decision */}
<div> <div>
<label className="block text-xs font-black text-gray-700 mb-1 flex items-center justify-between"> <label className="block text-xs font-black text-gray-700 mb-2">تصمیم بالینی و وضعیت تایید *</label>
<span>محصولات و مکمل‌های تجویزی پزشک (جهت خرید آسان کاربر)</span> <div className="grid grid-cols-3 gap-3">
<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>
<div className="max-h-40 overflow-y-auto border border-gray-200 rounded-xl p-2 space-y-1 bg-white"> <p className="text-[11px] text-gray-400 mb-2">محصولاتی که تیک می‌زنید مستقیماً در پنل کاربر به عنوان سبد پیشنهادی نسخه قرار می‌گیرند.</p>
{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 ${
@ -379,29 +402,12 @@ 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>
</div> </Modal>
)} )}
{/* Fullscreen Lightbox for Prescription Image */} {/* Fullscreen Lightbox for Prescription Image */}
<ImagePreviewModal <ImagePreviewModal
isOpen={!!previewImageUrl} isOpen={!!previewImageUrl}

View File

@ -8,6 +8,9 @@ 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;
@ -435,15 +438,17 @@ 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">
@ -1650,18 +1655,29 @@ export default function Products() {
</form> </form>
</div> </div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl"> <div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-2.5 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
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف انصراف
</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"> <Button
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,7 +1,11 @@
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
@ -97,7 +101,10 @@ 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 = [
@ -119,13 +126,12 @@ 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,6 +18,10 @@ 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;
@ -434,21 +438,34 @@ export default function Reviews() {
{/* Reply Modal */} {/* Reply Modal */}
{replyingReview && ( {replyingReview && (
<div className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4"> <Modal
<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"> isOpen={!!replyingReview}
<div className="flex items-center justify-between border-b border-gray-100 pb-3"> onClose={() => setReplyingReview(null)}
<h3 className="text-base font-black text-gray-900 flex items-center gap-2"> title={`پاسخ به دیدگاه ${replyingReview.userName}`}
<Reply className="w-5 h-5 text-purple-600" /> icon={Reply}
پاسخ به دیدگاه {replyingReview.userName} maxWidth="lg"
</h3> footer={
<button <div className="flex items-center justify-end gap-2.5 w-full">
<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>
@ -466,28 +483,10 @@ export default function Reviews() {
className="w-full p-3 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500 resize-none leading-relaxed" 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>
</div> </Modal>
)} )}
</div> </div>
); );
} }

View File

@ -4,8 +4,10 @@ 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: 'کنینا ایران',
@ -271,31 +273,34 @@ 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"
disabled={isSaving} variant="primary"
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" size="md"
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,6 +20,9 @@ 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');
@ -358,14 +361,15 @@ export default function Settings() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<button <Button
type="submit" type="submit"
disabled={isSaving} variant="primary"
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" size="md"
startIcon={Save}
isLoading={isSaving}
> >
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />} ذخیره هویت برند
<span>ذخیره هویت برند</span> </Button>
</button>
</div> </div>
</form> </form>
) : activeTab === 'contact' ? ( ) : activeTab === 'contact' ? (
@ -468,16 +472,18 @@ export default function Settings() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<button <Button
type="submit" type="submit"
disabled={isSaving} variant="primary"
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" size="md"
startIcon={Save}
isLoading={isSaving}
> >
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />} ذخیره اطلاعات تماس
<span>ذخیره اطلاعات تماس</span> </Button>
</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,6 +4,9 @@ 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({
@ -148,17 +151,19 @@ export default function ShippingSettingsPage() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<button <Button
type="submit" type="submit"
disabled={isSaving} variant="primary"
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" size="md"
startIcon={Save}
isLoading={isSaving}
> >
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />} ذخیره تغییرات ارسال
<span>ذخیره تغییرات ارسال</span> </Button>
</button>
</div> </div>
</form> </form>
)} )}
</div> </div>
); );
} }

View File

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

View File

@ -39,6 +39,9 @@ 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;
@ -767,17 +770,19 @@ 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"
disabled={isSaving} variant="primary"
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" size="md"
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,6 +19,9 @@ 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;
@ -434,14 +437,16 @@ 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"
disabled={isUpdating || !certFile || !keyFile} variant="primary"
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" size="sm"
startIcon={ShieldCheck}
disabled={!certFile || !keyFile}
isLoading={isUpdating}
> >
{isUpdating ? <Spinner size="sm" /> : <ShieldCheck className="w-4 h-4" />} نصب و فعال‌سازی گواهی جدید
<span>نصب و فعال‌سازی گواهی جدید</span> </Button>
</button>
</div> </div>
</form> </form>
) : ( ) : (
@ -479,14 +484,16 @@ 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"
disabled={isUpdating || !certText.trim() || !keyText.trim()} variant="primary"
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" size="sm"
startIcon={ShieldCheck}
disabled={!certText.trim() || !keyText.trim()}
isLoading={isUpdating}
> >
{isUpdating ? <Spinner size="sm" /> : <ShieldCheck className="w-4 h-4" />} ذخیره و اعمال تغییرات
<span>ذخیره و اعمال تغییرات</span> </Button>
</button>
</div> </div>
</form> </form>
)} )}
@ -509,18 +516,20 @@ 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}
disabled={isTestingOnline} isLoading={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" />} تست زنده پورت ۴۴۳ دامنه
<span>تست زنده پورت ۴۴۳ دامنه</span> </Button>
</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,8 +14,11 @@ 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,
@ -324,17 +327,19 @@ export default function SystemSettingsPage() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<button <Button
type="submit" type="submit"
disabled={isSaving} variant="primary"
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" size="md"
startIcon={Save}
isLoading={isSaving}
> >
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />} ذخیره تنظیمات سیستمی
<span>ذخیره تنظیمات سیستمی</span> </Button>
</button>
</div> </div>
</form> </form>
)} )}
</div> </div>
); );
} }

View File

@ -5,8 +5,11 @@ 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);
@ -148,13 +151,14 @@ 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">
@ -199,45 +203,51 @@ 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 text-gray-600 text-sm max-w-md line-clamp-2"> <td className="py-4 px-6">
{item.content} <p className="text-gray-600 text-sm line-clamp-2 leading-relaxed max-w-md">{item.content}</p>
</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: item.rating || 5 }).map((_, i) => ( {Array.from({ length: 5 }).map((_, i) => (
<Star key={i} className="w-4 h-4 fill-amber-400" /> <Star
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">
{item.isFeatured && ( <button
<span className="px-2 py-0.5 bg-amber-50 text-amber-700 text-[11px] font-bold rounded border border-amber-200"> onClick={() => handleToggleActive(item)}
ویژه (Featured) className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold transition-colors cursor-pointer ${
</span> item.isActive
)} ? '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-[11px] font-bold rounded border border-blue-200"> <span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs font-bold rounded-md">
🏠 صفحه اصلی خانه
</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 onClick={() => openModal(item)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"> <button
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 onClick={() => setDeleteTargetId(item.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"> <button
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>
@ -250,88 +260,117 @@ export default function TestimonialsManager() {
</div> </div>
</div> </div>
{/* Form Modal */} {/* Modal */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200"> isOpen={isModalOpen}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setIsModalOpen(false)}
<h3 className="text-lg font-bold text-gray-900"> title={editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'}
{editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'} icon={MessageSquareQuote}
</h3> maxWidth="2xl"
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors"> footer={
<X className="w-6 h-6" /> <div className="flex justify-end gap-2.5 w-full">
</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>
<form onSubmit={handleSave} className="p-6 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 type="text"
required value={formData.roleTitle}
type="text" onChange={(e) => setFormData({ ...formData, roleTitle: e.target.value })}
value={formData.authorName} placeholder="مثال: متخصص جراحی دامپزشکی"
onChange={(e) => setFormData({ ...formData, authorName: 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"
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>
<div> <div>
<label className="block text-sm font-bold text-gray-700 mb-1">سمت / عنوان تخصص (roleTitle)</label> <label className="block text-sm font-bold text-gray-700 mb-1">تصویر نویسنده / آواتار</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="https://... یا /uploads/..." placeholder="آدرس تصویر یا انتخاب از رسانه‌ها..."
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" className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm leading-relaxed"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-center">
<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 <div className="flex items-center gap-2">
value={formData.rating} {[1, 2, 3, 4, 5].map((star) => (
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })} <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" type="button"
> key={star}
{[5, 4, 3, 2, 1].map(r => ( onClick={() => setFormData({ ...formData, rating: star })}
<option key={r} value={r}>{r} ستاره</option> className="p-1 cursor-pointer"
>
<Star
className={`w-6 h-6 ${
star <= formData.rating ? 'fill-amber-400 text-amber-400' : 'text-gray-300'
}`}
/>
</button>
))} ))}
</select> <span className="text-sm font-bold text-gray-500 mr-2">({formData.rating} از ۵)</span>
</div>
</div> </div>
<div> <div>
@ -345,18 +384,8 @@ export default function TestimonialsManager() {
</div> </div>
</div> </div>
<div className="pt-2 grid grid-cols-3 gap-3"> <div className="grid grid-cols-2 gap-4 pt-2">
<label className="flex items-center justify-between p-3 bg-gray-50 rounded-xl border border-gray-200 cursor-pointer"> <label className="flex items-center justify-between p-3 bg-purple-50 rounded-xl border border-purple-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"
@ -367,7 +396,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}
@ -376,29 +405,12 @@ 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>
</div> </Modal>
)} )}
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{deleteTargetId && ( {deleteTargetId && (
<ConfirmModal <ConfirmModal

View File

@ -20,6 +20,8 @@ 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;
@ -340,47 +342,63 @@ export default function Tickets() {
{/* Ticket Details & Admin Reply Modal */} {/* Ticket Details & Admin Reply Modal */}
{selectedTicket && ( {selectedTicket && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"> <Modal
<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"> isOpen={!!selectedTicket}
{/* Modal Header */} onClose={() => setSelectedTicket(null)}
<div className="flex items-start justify-between pb-4 border-b border-gray-100"> title={`تیکت ${selectedTicket.ticketNumber}: ${selectedTicket.subject}`}
<div> icon={MessageSquare}
<div className="flex items-center gap-2"> maxWidth="3xl"
<h3 className="text-lg font-black text-gray-900">{selectedTicket.subject}</h3> footer={
<span className="px-2.5 py-0.5 bg-canina-blue/10 text-canina-blue font-mono font-bold text-xs rounded-lg"> <div className="flex items-center justify-end gap-2.5 w-full">
{selectedTicket.ticketNumber} <Button
</span> variant="secondary"
</div> size="sm"
<div className="flex items-center gap-3 text-xs text-gray-500 font-medium mt-1"> type="button"
<span>کاربر: {selectedTicket.user?.firstName} {selectedTicket.user?.lastName} ({selectedTicket.user?.mobile})</span> onClick={() => setSelectedTicket(null)}
{selectedTicket.pet && ( >
<span className="text-purple-600 font-bold">• 🐾 {selectedTicket.pet.name} ({selectedTicket.pet.breed || selectedTicket.pet.type})</span> بستن
)} </Button>
</div> <Button
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-gray-50 focus:border-canina-blue outline-none" className="px-2.5 py-1.5 border border-gray-200 rounded-xl text-xs font-bold bg-white focus:ring-2 focus:ring-purple-500 outline-none"
> >
<option value="OPEN">در انتظار پاسخ</option> <option value="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="flex-1 overflow-y-auto space-y-4 p-2 min-h-[250px]"> <div className="overflow-y-auto space-y-3.5 p-1 max-h-[340px]">
{(selectedTicket.messages || []).map((msg) => { {(selectedTicket.messages || []).map((msg) => {
const isAdminOrVet = msg.senderRole === 'ADMIN' || msg.senderRole === 'VET_DOCTOR'; const isAdminOrVet = msg.senderRole === 'ADMIN' || msg.senderRole === 'VET_DOCTOR';
return ( return (
@ -388,7 +406,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-emerald-50 border border-emerald-200 text-emerald-950 mr-auto' ? 'bg-purple-50 border border-purple-200 text-purple-950 mr-auto'
: 'bg-blue-50 border border-blue-200 text-blue-950 ml-auto' : 'bg-blue-50 border border-blue-200 text-blue-950 ml-auto'
}`} }`}
> >
@ -396,8 +414,8 @@ export default function Tickets() {
<span className="font-black text-[11px] flex items-center gap-1.5"> <span className="font-black text-[11px] flex items-center gap-1.5">
{isAdminOrVet ? ( {isAdminOrVet ? (
<> <>
<Stethoscope className="w-3.5 h-3.5 text-emerald-600" /> <Stethoscope className="w-3.5 h-3.5 text-purple-600" />
<span className="text-emerald-700">{msg.senderName} (تیم پزشکی / ادمین)</span> <span className="text-purple-700">{msg.senderName} (تیم پزشکی / ادمین)</span>
</> </>
) : ( ) : (
<> <>
@ -417,7 +435,7 @@ export default function Tickets() {
</div> </div>
{/* Admin Response Composer */} {/* Admin Response Composer */}
<form onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3"> <form id="ticketReplyForm" onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3">
<div className="flex items-center justify-between text-xs"> <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">
@ -439,29 +457,13 @@ 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-canina-blue outline-none" className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-medium focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none resize-none leading-relaxed"
/> />
<div className="flex justify-end gap-3">
<button
type="button"
onClick={() => setSelectedTicket(null)}
className="px-4 py-2 rounded-xl text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 cursor-pointer"
>
بستن
</button>
<button
type="submit"
disabled={sendingReply || !replyMessage.trim()}
className="px-6 py-2 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-dark disabled:opacity-50 transition-all flex items-center gap-2 cursor-pointer shadow-md shadow-canina-blue/20"
>
<Send className="w-3.5 h-3.5" />
<span>{sendingReply ? 'در حال ارسال...' : 'ارسال پاسخ به کاربر'}</span>
</button>
</div>
</form> </form>
</div> </div>
</div> </Modal>
)} )}
</div> </div>
); );
} }

View File

@ -47,6 +47,8 @@ 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 = [
@ -919,15 +921,16 @@ 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)}
disabled={isSaving} isLoading={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" />} ذخیره این فیلد
<span>ذخیره این فیلد</span> </Button>
</button>
</div> </div>
</div> </div>
); );
@ -948,22 +951,21 @@ 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={isSavingAll || !hasUnsavedChanges} disabled={!hasUnsavedChanges}
className={`px-5 py-2.5 rounded-xl text-xs font-black flex items-center gap-2 transition-all shadow-sm ${ isLoading={isSavingAll}
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'
}`}
> >
{isSavingAll ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />} ذخیره تمامی تغییرات {hasUnsavedChanges ? `(${Object.keys(edits).length})` : ''}
<span>ذخیره تمامی تغییرات {hasUnsavedChanges ? `(${Object.keys(edits).length})` : ''}</span> </Button>
</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,13 +265,15 @@ 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,6 +4,9 @@ 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;
@ -179,15 +182,17 @@ 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" /> افزودن ویدئوی جدید
<span>افزودن ویدئوی جدید</span> </Button>
</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" />
@ -310,22 +315,35 @@ export default function Videos() {
)} )}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir"> <Modal
<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"> isOpen={isModalOpen}
<div className="flex items-center justify-between pb-4 border-b border-gray-100 mb-6"> onClose={() => setIsModalOpen(false)}
<h3 className="text-lg font-black text-gray-900 flex items-center gap-2"> title={editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'}
<VideoIcon className="w-5 h-5 text-purple-600" /> icon={VideoIcon}
{editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'} maxWidth="2xl"
</h3> footer={
<button <div className="flex justify-end gap-2.5 w-full">
<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
@ -404,14 +422,15 @@ 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" /> انتخاب کاور
<span>انتخاب از گالری</span> </Button>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -437,14 +456,15 @@ 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" /> انتخاب ویدئو
<span>انتخاب ویدئو</span> </Button>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -471,27 +491,12 @@ 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>
</div> </Modal>
)} )}
{/* Cover Thumbnail Media Selector Modal */} {/* Cover Thumbnail Media Selector Modal */}
<MediaSelector <MediaSelector
isOpen={isThumbnailMediaOpen} isOpen={isThumbnailMediaOpen}

View File

@ -2,6 +2,8 @@ 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;
@ -59,10 +61,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);
} }
@ -70,20 +72,24 @@ export default function WholesaleApplications() {
return ( return (
<div className="p-6 font-vazir" dir="rtl"> <div className="p-6 font-vazir" dir="rtl">
<h1 className="text-2xl font-bold mb-2 text-gray-800">بررسی درخواست‌های ثبت‌نام همکاران B2B (کلینیک‌ها و داروخانه‌ها)</h1> <div className="flex justify-between items-center mb-6">
<p className="text-sm text-gray-500 mb-6">استعلام شماره پروانه نظام دامپزشکی و اعطای دسترسی خرید عمده با تخفیف ۳۰٪ کاتالوگ Canina</p> <div>
<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 border border-gray-200 overflow-hidden shadow-sm"> <div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<table className="w-full text-right"> <table className="w-full text-right border-collapse">
<thead className="bg-gray-50 border-b border-gray-200 text-xs font-bold text-gray-500"> <thead>
<tr> <tr className="bg-gray-50/50 border-b border-gray-100 text-xs text-gray-500">
<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">تاریخ درخواست</th>
<th className="px-6 py-4 text-center">عملیات تایید همکار</th> <th className="px-6 py-4 font-bold 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">
@ -107,21 +113,26 @@ 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 space-x-2 space-x-reverse"> <td className="px-6 py-4 text-center">
{u.role !== 'User_Wholesale' && ( <div className="flex items-center justify-center gap-2">
<button {u.role !== 'User_Wholesale' && (
onClick={() => setApproveTargetId(u.id)} <Button
className="bg-green-600 hover:bg-green-700 text-white font-bold text-xs px-4 py-2 rounded-xl transition-all shadow-sm" variant="primary"
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,6 +5,9 @@ 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;
@ -138,13 +141,14 @@ 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">
@ -184,8 +188,8 @@ export default function Wiki() {
<td className="py-4 px-6 text-gray-500 text-sm max-w-xs truncate">{t.definition}</td> <td className="py-4 px-6 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"><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 cursor-pointer"><Edit2 className="w-4 h-4" /></button>
<button onClick={() => setDeleteTargetKey(t.key)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"><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 cursor-pointer"><Trash2 className="w-4 h-4" /></button>
</div> </div>
</td> </td>
</tr> </tr>
@ -202,99 +206,109 @@ export default function Wiki() {
</div> </div>
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<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"> isOpen={isModalOpen}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setIsModalOpen(false)}
<h3 className="text-xl font-bold text-gray-900"> title={editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
{editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'} icon={BookOpen}
</h3> maxWidth="2xl"
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors"> footer={
<XCircle className="w-6 h-6" /> <div className="flex justify-end gap-2.5 w-full">
</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="p-6 overflow-y-auto flex-1"> >
<form id="wikiForm" onSubmit={handleSave} className="space-y-6"> <div className="space-y-6 text-right font-vazir">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <form id="wikiForm" onSubmit={handleSave} className="space-y-6">
<div className="space-y-2"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیح علمی *</label> <label className="text-sm font-bold text-gray-700">شناسه یکتا (Key) *</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> <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>
<div className="space-y-2">
{/* Related Products Selector (TASK-2.23) */} <label className="text-sm font-bold text-gray-700">نام اصطلاح (Term) *</label>
<div className="space-y-2 border border-gray-200 p-4 rounded-xl bg-purple-50/40"> <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" />
<label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی (TASK-2.23)</label>
<select
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">
{formData.relatedProducts.map(pId => {
const prod = productsList.find(p => p.id === pId);
return (
<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>
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50"> <div className="space-y-2">
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4> <label className="text-sm font-bold text-gray-700">توضیح علمی *</label>
<div className="space-y-2"> <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>
<label className="text-xs font-bold text-gray-700">عنوان متا</label> </div>
<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> {/* 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-xs font-bold text-gray-700">کلمات کلیدی</label> <label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی</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" /> <select
</div> onChange={(e) => {
<div className="space-y-2"> const prodId = e.target.value;
<label className="text-xs font-bold text-gray-700">توضیحات متا</label> if (prodId && !formData.relatedProducts.includes(prodId)) {
<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> setFormData({ ...formData, relatedProducts: [...formData.relatedProducts, prodId] });
</div> }
}}
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">
{formData.relatedProducts.map(pId => {
const prod = productsList.find(p => p.id === pId);
return (
<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 cursor-pointer"
>
×
</button>
</span>
);
})}
</div> </div>
</div>
</form> <div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
</div> <h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
<div className="space-y-2">
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl"> <label className="text-xs font-bold text-gray-700">عنوان متا</label>
<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> <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" />
<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 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>
</form>
</div> </div>
</div> </Modal>
)} )}
<ConfirmModal <ConfirmModal
isOpen={!!deleteTargetKey} isOpen={!!deleteTargetKey}
title="حذف اصطلاح علمی" title="حذف اصطلاح علمی"

View File

@ -26,6 +26,9 @@ 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');
@ -378,25 +381,27 @@ export default function ZibalPortalPage() {
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6"> <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-3"> <div className="flex items-center gap-2.5">
<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" /> دانلود گزارش
<span>دانلود گزارش</span> </Button>
</button>
<button <Button
variant="outline"
size="sm"
onClick={fetchRefunds} onClick={fetchRefunds}
disabled={loadingRefunds} isLoading={loadingRefunds}
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer" startIcon={RefreshCw}
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 */}
@ -634,25 +639,27 @@ 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-3"> <div className="flex items-center gap-2.5">
<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" /> دانلود گزارش
<span>دانلود گزارش</span> </Button>
</button>
<button <Button
variant="outline"
size="sm"
onClick={fetchCheckouts} onClick={fetchCheckouts}
disabled={loadingCheckouts} isLoading={loadingCheckouts}
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer" startIcon={RefreshCw}
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">
@ -851,25 +858,27 @@ 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-3"> <div className="flex items-center gap-2.5">
<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" /> دانلود گزارش درگاه
<span>دانلود گزارش درگاه</span> </Button>
</button>
<button <Button
variant="outline"
size="sm"
onClick={fetchGatewayTx} onClick={fetchGatewayTx}
disabled={loadingGatewayTx} isLoading={loadingGatewayTx}
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer" startIcon={RefreshCw}
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">
@ -1013,25 +1022,40 @@ export default function ZibalPortalPage() {
)} )}
{/* ======================================================== */} {/* ======================================================== */}
{/* MODAL: SUBMIT NEW REFUND / REVERSE */} {/* MODAL: NEW REFUND SUBMIT */}
{/* ======================================================== */} {/* ======================================================== */}
{newRefundModal && ( {newRefundModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"> <Modal
<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"> isOpen={newRefundModal}
<div className="flex items-center justify-between border-b border-gray-100 pb-4"> onClose={() => setNewRefundModal(false)}
<h3 className="text-base font-black text-gray-900 flex items-center gap-2"> title="ثبت و ارسال استرداد وجه جدید (Refund / Reverse)"
<RotateCcw className="w-5 h-5 text-rose-600" /> icon={RotateCcw}
ثبت درخواست استرداد وجه (Refund / Reverse) maxWidth="lg"
</h3> footer={
<button <div className="flex justify-end gap-2.5 w-full">
<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
@ -1092,47 +1116,38 @@ 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>
</div> </Modal>
)} )}
{/* ======================================================== */} {/* ======================================================== */}
{/* MODAL: DETAIL / INQUIRY VIEWER */} {/* MODAL: DETAIL / INQUIRY VIEWER */}
{/* ======================================================== */} {/* ======================================================== */}
{detailModalItem && ( {detailModalItem && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"> <Modal
<div className="bg-white rounded-3xl w-full max-w-lg border border-gray-200 shadow-2xl p-6 space-y-4"> isOpen={!!detailModalItem}
<div className="flex items-center justify-between border-b border-gray-100 pb-3"> onClose={() => setDetailModalItem(null)}
<h3 className="font-black text-sm text-gray-900">{detailModalItem.title}</h3> title={detailModalItem.title}
<button onClick={() => setDetailModalItem(null)} className="text-gray-400 font-bold text-lg"> maxWidth="lg"
✕ footer={
</button> <div className="flex justify-end w-full">
<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> <pre className="bg-slate-900 text-emerald-400 p-4 rounded-xl text-[11px] font-mono overflow-x-auto dir-ltr">
</div> {JSON.stringify(detailModalItem.data, null, 2)}
</div> </pre>
</Modal>
)} )}
</div> </div>
); );
} }