Compare commits
No commits in common. "1350267ffb4feef6e9fe1856ccbed7f956c86c10" and "32f8b8b6564988ff7588bea7d74ccd498aa68437" have entirely different histories.
1350267ffb
...
32f8b8b656
@ -42,33 +42,32 @@ export default function Button({
|
|||||||
const baseClasses =
|
const baseClasses =
|
||||||
'inline-flex items-center justify-center font-bold whitespace-nowrap transition-all select-none rounded-xl cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none focus:outline-none focus:ring-2 focus:ring-offset-2';
|
'inline-flex items-center justify-center font-bold whitespace-nowrap transition-all select-none rounded-xl cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||||
|
|
||||||
// Size styling - crisp, modern and compact
|
// Size styling
|
||||||
const sizeClasses: Record<ButtonSize, string> = {
|
const sizeClasses: Record<ButtonSize, string> = {
|
||||||
xs: 'px-2.5 py-1 text-[11px] gap-1.5',
|
xs: 'px-2.5 py-1 text-[11px] gap-1.5',
|
||||||
sm: 'px-3 py-1.5 text-xs gap-1.5',
|
sm: 'px-3.5 py-1.5 text-xs gap-2',
|
||||||
md: 'px-3.5 py-2 text-xs sm:text-sm gap-2',
|
md: 'px-4 py-2.5 text-xs sm:text-sm gap-2',
|
||||||
lg: 'px-5 py-2.5 text-sm sm:text-base gap-2 shadow-sm',
|
lg: 'px-6 py-3 text-sm sm:text-base gap-2.5 shadow-md',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Variant styling
|
// Variant styling
|
||||||
const variantClasses: Record<ButtonVariant, string> = {
|
const variantClasses: Record<ButtonVariant, string> = {
|
||||||
primary:
|
primary:
|
||||||
'bg-purple-600 hover:bg-purple-700 text-white shadow-xs focus:ring-purple-500 border border-purple-600',
|
'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm shadow-indigo-200 focus:ring-indigo-500 border border-indigo-600',
|
||||||
secondary:
|
secondary:
|
||||||
'bg-slate-100 hover:bg-slate-200 text-slate-700 focus:ring-slate-400 border border-slate-200',
|
'bg-slate-100 hover:bg-slate-200 text-slate-700 focus:ring-slate-400 border border-slate-200',
|
||||||
danger:
|
danger:
|
||||||
'bg-rose-600 hover:bg-rose-700 text-white shadow-xs focus:ring-rose-500 border border-rose-600',
|
'bg-rose-600 hover:bg-rose-700 text-white shadow-sm shadow-rose-200 focus:ring-rose-500 border border-rose-600',
|
||||||
success:
|
success:
|
||||||
'bg-emerald-600 hover:bg-emerald-700 text-white shadow-xs focus:ring-emerald-500 border border-emerald-600',
|
'bg-emerald-600 hover:bg-emerald-700 text-white shadow-sm shadow-emerald-200 focus:ring-emerald-500 border border-emerald-600',
|
||||||
warning:
|
warning:
|
||||||
'bg-amber-500 hover:bg-amber-600 text-white shadow-xs focus:ring-amber-400 border border-amber-500',
|
'bg-amber-500 hover:bg-amber-600 text-white shadow-sm shadow-amber-200 focus:ring-amber-400 border border-amber-500',
|
||||||
outline:
|
outline:
|
||||||
'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-purple-500',
|
'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-indigo-500',
|
||||||
ghost:
|
ghost:
|
||||||
'bg-transparent hover:bg-gray-100 text-gray-600 focus:ring-gray-300 border border-transparent',
|
'bg-transparent hover:bg-gray-100 text-gray-600 focus:ring-gray-300 border border-transparent',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const iconSizes: Record<ButtonSize, string> = {
|
const iconSizes: Record<ButtonSize, string> = {
|
||||||
xs: 'w-3 h-3',
|
xs: 'w-3 h-3',
|
||||||
sm: 'w-3.5 h-3.5',
|
sm: 'w-3.5 h-3.5',
|
||||||
|
|||||||
@ -3,13 +3,8 @@ import { Building2, CheckCircle2, XCircle, Clock, PhoneCall, Filter, X, Plus, Sh
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
import type { B2BInquiry, PartnerAccount } from '../types/admin';
|
import type { B2BInquiry, PartnerAccount } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function B2BManager() {
|
export default function B2BManager() {
|
||||||
const [activeTab, setActiveTab] = useState<'inquiries' | 'partners'>('inquiries');
|
const [activeTab, setActiveTab] = useState<'inquiries' | 'partners'>('inquiries');
|
||||||
|
|
||||||
@ -282,13 +277,12 @@ export default function B2BManager() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => openInquiryReview(inq)}
|
onClick={() => openInquiryReview(inq)}
|
||||||
|
className="px-3 py-1.5 bg-purple-600 text-white hover:bg-purple-700 rounded-lg text-xs font-bold transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
تغییر وضعیت
|
تغییر وضعیت
|
||||||
</Button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
@ -302,14 +296,13 @@ export default function B2BManager() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="font-bold text-gray-800">فهرست حسابهای تایید شده همکار</h3>
|
<h3 className="font-bold text-gray-800">فهرست حسابهای تایید شده همکار</h3>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => setIsPartnerModalOpen(true)}
|
onClick={() => setIsPartnerModalOpen(true)}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-xl flex items-center gap-2 font-bold text-xs shadow-md shadow-purple-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
افزودن همکار B2B جدید
|
افزودن همکار B2B جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
@ -367,41 +360,21 @@ export default function B2BManager() {
|
|||||||
|
|
||||||
{/* Inquiry Review Modal */}
|
{/* Inquiry Review Modal */}
|
||||||
{selectedInquiry && (
|
{selectedInquiry && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={!!selectedInquiry}
|
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
|
||||||
onClose={() => setSelectedInquiry(null)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={`بررسی درخواست B2B (${selectedInquiry.companyName})`}
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
icon={Building2}
|
بررسی درخواست B2B ({selectedInquiry.companyName})
|
||||||
maxWidth="lg"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setSelectedInquiry(null)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<X className="w-6 h-6" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedInquiry(null)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="inquiryForm"
|
|
||||||
isLoading={isInquirySubmitting}
|
|
||||||
>
|
|
||||||
بروزرسانی وضعیت
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<form onSubmit={handleInquiryReviewSubmit} className="p-6 space-y-4">
|
||||||
<div className="space-y-4 text-right font-vazir">
|
|
||||||
<form id="inquiryForm" onSubmit={handleInquiryReviewSubmit} className="space-y-4">
|
|
||||||
<div className="p-4 bg-purple-50/50 rounded-xl border border-purple-100 space-y-1 text-xs">
|
<div className="p-4 bg-purple-50/50 rounded-xl border border-purple-100 space-y-1 text-xs">
|
||||||
<div><span className="text-gray-500">رابط: </span><span className="font-bold text-gray-800">{selectedInquiry.contactName}</span></div>
|
<div><span className="font-bold text-gray-600">شخص رابط: </span>{selectedInquiry.contactName} ({selectedInquiry.phone})</div>
|
||||||
<div><span className="text-gray-500">تلفن / ایمیل: </span><span className="font-mono text-gray-800">{selectedInquiry.phone} - {selectedInquiry.email}</span></div>
|
<div><span className="font-bold text-gray-600">پیام درخواست: </span>{selectedInquiry.message}</div>
|
||||||
<div><span className="text-gray-500">نوع کسبوکار: </span><span className="font-bold text-purple-700">{selectedInquiry.businessType}</span></div>
|
|
||||||
{selectedInquiry.message && <div className="mt-2 text-gray-700 bg-white p-2.5 rounded-lg border border-gray-200">{selectedInquiry.message}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -428,43 +401,43 @@ export default function B2BManager() {
|
|||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedInquiry(null)}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isInquirySubmitting}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isInquirySubmitting && <Spinner size="sm" />}
|
||||||
|
بروزرسانی وضعیت
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Partner Account Setup Modal */}
|
{/* Partner Account Setup Modal */}
|
||||||
{isPartnerModalOpen && (
|
{isPartnerModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isPartnerModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
|
||||||
onClose={() => setIsPartnerModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title="تعریف حساب همکار B2B جدید"
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
icon={Building2}
|
تعریف حساب همکار B2B جدید
|
||||||
maxWidth="lg"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setIsPartnerModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<X className="w-6 h-6" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsPartnerModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="partnerForm"
|
|
||||||
isLoading={isPartnerSubmitting}
|
|
||||||
>
|
|
||||||
ایجاد حساب همکار
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<form onSubmit={handlePartnerSave} className="p-6 space-y-4">
|
||||||
<div className="space-y-4 text-right font-vazir">
|
|
||||||
<form id="partnerForm" onSubmit={handlePartnerSave} className="space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه کاربر (User ID) *</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه کاربر (User ID) *</label>
|
||||||
<input
|
<input
|
||||||
@ -525,11 +498,28 @@ export default function B2BManager() {
|
|||||||
<option value="tier3">سطح ۳ (۳۵٪ تخفیف)</option>
|
<option value="tier3">سطح ۳ (۳۵٪ تخفیف)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsPartnerModalOpen(false)}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPartnerSubmitting}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isPartnerSubmitting && <Spinner size="sm" />}
|
||||||
|
ایجاد حساب همکار
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,8 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
import type { Banner } from '../types/admin';
|
import type { Banner } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
export default function BannersManager() {
|
export default function BannersManager() {
|
||||||
const [banners, setBanners] = useState<Banner[]>([]);
|
const [banners, setBanners] = useState<Banner[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@ -169,14 +166,13 @@ export default function BannersManager() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مدیریت اسلایدر صفحه اصلی و تبلیغات بنری</p>
|
<p className="text-gray-500 font-medium mt-1">مدیریت اسلایدر صفحه اصلی و تبلیغات بنری</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
افزودن بنر جدید
|
افزودن بنر جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
@ -202,7 +198,7 @@ export default function BannersManager() {
|
|||||||
</tr>
|
</tr>
|
||||||
) : banners.length === 0 ? (
|
) : banners.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="py-12 text-center text-gray-500 font-medium">هیچ بنری یافت نشد</td>
|
<td colSpan={7} className="py-12 text-center text-gray-500 font-medium">هیچ بنری ثبت نشده است</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
banners.map((banner, index) => (
|
banners.map((banner, index) => (
|
||||||
@ -211,79 +207,71 @@ export default function BannersManager() {
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
disabled={index === 0}
|
disabled={index === 0}
|
||||||
onClick={() => handleReorder(index, 'up')}
|
onClick={() => handleMoveOrder(index, 'up')}
|
||||||
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30 disabled:hover:text-gray-400 cursor-pointer"
|
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<MoveUp className="w-4 h-4" />
|
<MoveUp className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
<span className="font-mono text-sm font-bold text-gray-600 px-1">{index + 1}</span>
|
||||||
<button
|
<button
|
||||||
disabled={index === banners.length - 1}
|
disabled={index === banners.length - 1}
|
||||||
onClick={() => handleReorder(index, 'down')}
|
onClick={() => handleMoveOrder(index, 'down')}
|
||||||
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30 disabled:hover:text-gray-400 cursor-pointer"
|
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<MoveDown className="w-4 h-4" />
|
<MoveDown className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-3 px-6">
|
||||||
|
<div className="w-20 h-12 rounded-xl bg-gray-100 border border-gray-200 overflow-hidden flex items-center justify-center">
|
||||||
{banner.imageUrl ? (
|
{banner.imageUrl ? (
|
||||||
<img
|
<img src={getImageFullUrl(banner.imageUrl)} alt={banner.title} className="w-full h-full object-cover" />
|
||||||
src={getImageFullUrl(banner.imageUrl)}
|
|
||||||
alt={banner.title || 'Banner'}
|
|
||||||
className="w-20 h-10 object-cover rounded-lg border border-gray-100 shadow-xs"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="w-20 h-10 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400">
|
<ImageIcon className="w-6 h-6 text-gray-400" />
|
||||||
<ImageIcon className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="font-bold text-gray-900">{banner.title || '-'}</div>
|
<div className="flex flex-col">
|
||||||
<div className="text-xs text-gray-400 mt-0.5">{banner.subtitle || '-'}</div>
|
<span className="font-bold text-gray-900">{banner.title}</span>
|
||||||
|
{banner.subtitle && <span className="text-xs text-gray-500 mt-0.5">{banner.subtitle}</span>}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<span className="px-2.5 py-1 rounded-full text-xs font-bold bg-purple-50 text-purple-700">
|
<span className="inline-block px-3 py-1 bg-purple-50 text-purple-700 text-xs font-bold rounded-lg border border-purple-100">
|
||||||
{banner.position === 'home_hero' ? 'اسلایدر اصلی' : banner.position === 'home_promo' ? 'بنر تبلیغاتی میانی' : banner.position}
|
{banner.position === 'home_hero'
|
||||||
|
? 'اسلایدر هیرو اصلی'
|
||||||
|
: banner.position === 'home_middle'
|
||||||
|
? 'بنر میانی صفحه نخست'
|
||||||
|
: banner.position === 'home_bottom'
|
||||||
|
? 'بنر عریض پایین خانه'
|
||||||
|
: banner.position === 'shop_top'
|
||||||
|
? 'بالای فروشگاه و کاتالوگ'
|
||||||
|
: banner.position === 'category_top'
|
||||||
|
? 'بالای دستهبندی'
|
||||||
|
: 'سایدبار'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6 text-sm text-gray-500 font-mono" dir="ltr">
|
<td className="py-4 px-6 text-gray-500 font-mono text-xs dir-ltr text-right">
|
||||||
{banner.linkUrl || '-'}
|
{banner.linkUrl || '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<button
|
|
||||||
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 ? (
|
{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" />
|
<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">
|
||||||
<XCircle className="w-3.5 h-3.5" />
|
<XCircle className="w-3.5 h-3.5" /> غیرفعال
|
||||||
غیرفعال
|
</span>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button onClick={() => openModal(banner)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||||
onClick={() => openModal(banner)}
|
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => setDeleteTargetId(banner.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||||
onClick={() => setDeleteTargetId(banner.id)}
|
|
||||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -296,122 +284,96 @@ export default function BannersManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Form Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={editingBanner ? 'ویرایش بنر' : 'افزودن بنر جدید'}
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
icon={ImageIcon}
|
{editingBanner ? 'ویرایش بنر' : 'افزودن بنر جدید'}
|
||||||
maxWidth="2xl"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<X className="w-6 h-6" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="bannerForm"
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
|
||||||
ذخیره تغییرات
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<form onSubmit={handleSave} className="p-6 space-y-4">
|
||||||
<div className="space-y-6 text-right font-vazir">
|
|
||||||
<form id="bannerForm" onSubmit={handleSave} className="space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">عنوان بنر</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">عنوان بنر *</label>
|
||||||
<input
|
<input
|
||||||
|
required
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.title}
|
value={formData.title}
|
||||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
placeholder="مثال: تخفیف ویژه مکملهای غذایی"
|
placeholder="مثال: تخصص دارویی از آلمان"
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">زیرعنوان (توضیحات کوتاه)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">زیرعنوان (اختیاری)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.subtitle}
|
value={formData.subtitle}
|
||||||
onChange={(e) => setFormData({ ...formData, subtitle: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, subtitle: e.target.value })}
|
||||||
placeholder="مثال: تا ۳۰٪ تخفیف برای مدت محدود"
|
placeholder="مثال: بیش از ۴۰ سال تجربه"
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-gray-700 mb-1">آدرس تصویر (imageUrl) *</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
value={formData.imageUrl}
|
||||||
|
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
|
||||||
|
placeholder="https://... یا /uploads/..."
|
||||||
|
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsMediaSelectorOpen(true)}
|
||||||
|
className="px-3 py-2.5 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0"
|
||||||
|
>
|
||||||
|
انتخاب از رسانه
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-gray-700 mb-1">لینک هدف (linkUrl)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.linkUrl}
|
||||||
|
onChange={(e) => setFormData({ ...formData, linkUrl: e.target.value })}
|
||||||
|
placeholder="مثال: /products/joints"
|
||||||
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">موقعیت نمایش</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">موقعیت نمایش</label>
|
||||||
<select
|
<select
|
||||||
value={formData.position}
|
value={formData.position}
|
||||||
onChange={(e) => setFormData({ ...formData, position: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, position: e.target.value })}
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-bold bg-white"
|
||||||
>
|
>
|
||||||
<option value="home_hero">اسلایدر اصلی (Home Hero)</option>
|
<option value="home_hero">اسلایدر هیرو اصلی (home_hero)</option>
|
||||||
<option value="home_promo">بنر تبلیغاتی میانی (Home Promo)</option>
|
<option value="home_middle">بنر میانی صفحه نخست (home_middle)</option>
|
||||||
|
<option value="home_bottom">بنر عریض پایین خانه (home_bottom)</option>
|
||||||
|
<option value="shop_top">بالای فروشگاه و کاتالوگ (shop_top)</option>
|
||||||
|
<option value="category_top">بالای دستهبندی (category_top)</option>
|
||||||
|
<option value="product_sidebar">سایدبار صفحات محصول و مقالات (product_sidebar)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">لینک هدف (URL)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">ترتیب نمایش (order)</label>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.linkUrl}
|
|
||||||
onChange={(e) => setFormData({ ...formData, linkUrl: e.target.value })}
|
|
||||||
placeholder="/shop یا https://..."
|
|
||||||
dir="ltr"
|
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر بنر *</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.imageUrl}
|
|
||||||
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
|
|
||||||
placeholder="آدرس تصویر یا انتخاب از رسانهها..."
|
|
||||||
dir="ltr"
|
|
||||||
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
startIcon={ImageIcon}
|
|
||||||
onClick={() => setIsMediaSelectorOpen(true)}
|
|
||||||
>
|
|
||||||
انتخاب تصویر
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{formData.imageUrl && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<img
|
|
||||||
src={getImageFullUrl(formData.imageUrl)}
|
|
||||||
alt="Preview"
|
|
||||||
className="h-28 w-full object-cover rounded-xl border border-gray-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-center">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">ترتیب نمایش</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.order}
|
value={formData.order}
|
||||||
@ -433,12 +395,29 @@ export default function BannersManager() {
|
|||||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isSaving && <Spinner size="sm" />}
|
||||||
|
ذخیره تغییرات
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
{deleteTargetId && (
|
{deleteTargetId && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
|||||||
@ -6,10 +6,6 @@ import Spinner from '../components/ui/Spinner';
|
|||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface BlogPost {
|
export interface BlogPost {
|
||||||
author?: {
|
author?: {
|
||||||
@ -138,14 +134,13 @@ export default function Blogs() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مدیریت محتوای آموزشی و مقالات علمی سایت</p>
|
<p className="text-gray-500 font-medium mt-1">مدیریت محتوای آموزشی و مقالات علمی سایت</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
مقاله جدید
|
مقاله جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
||||||
@ -201,8 +196,8 @@ export default function Blogs() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={() => openModal(blog)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"><Edit2 className="w-4 h-4" /></button>
|
<button onClick={() => openModal(blog)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"><Edit2 className="w-4 h-4" /></button>
|
||||||
<button onClick={() => setDeleteTargetId(blog.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"><Trash2 className="w-4 h-4" /></button>
|
<button onClick={() => setDeleteTargetId(blog.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2 className="w-4 h-4" /></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -219,34 +214,18 @@ export default function Blogs() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[95vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
|
<h3 className="text-xl font-bold text-gray-900">
|
||||||
icon={FileText}
|
{editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
|
||||||
maxWidth="4xl"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<XCircle className="w-6 h-6" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="blogForm"
|
|
||||||
>
|
|
||||||
ذخیره مقاله
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<div className="p-6 overflow-y-auto flex-1">
|
||||||
<div className="space-y-6 text-right font-vazir">
|
|
||||||
<form id="blogForm" onSubmit={handleSave} className="space-y-6">
|
<form id="blogForm" onSubmit={handleSave} className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -275,14 +254,7 @@ export default function Blogs() {
|
|||||||
<ImageIcon className="w-6 h-6" />
|
<ImageIcon className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
<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>
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsMediaSelectorOpen(true)}
|
|
||||||
>
|
|
||||||
انتخاب از گالری
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-3 cursor-pointer p-4 border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors">
|
<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" />
|
<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" />
|
||||||
@ -306,11 +278,17 @@ export default function Blogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
<MediaSelector isOpen={isMediaSelectorOpen} onClose={() => setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({ ...formData, imageUrl: url })} />
|
<MediaSelector isOpen={isMediaSelectorOpen} onClose={() => setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({ ...formData, imageUrl: url })} />
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,6 @@ import Spinner from '../components/ui/Spinner';
|
|||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: string;
|
id: string;
|
||||||
@ -131,17 +127,15 @@ export default function Categories() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">ایجاد دستهبندی محصولات و تنظیمات سئو (SEO)</p>
|
<p className="text-gray-500 font-medium mt-1">ایجاد دستهبندی محصولات و تنظیمات سئو (SEO)</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
دستهبندی جدید
|
دستهبندی جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
@ -203,10 +197,10 @@ export default function Categories() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer">
|
<button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer">
|
<button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -226,34 +220,18 @@ export default function Categories() {
|
|||||||
|
|
||||||
{/* Modal */}
|
{/* Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={editingCategory ? 'ویرایش دستهبندی' : 'افزودن دستهبندی جدید'}
|
<h3 className="text-xl font-bold text-gray-900">
|
||||||
icon={FolderTree}
|
{editingCategory ? 'ویرایش دستهبندی' : 'افزودن دستهبندی جدید'}
|
||||||
maxWidth="2xl"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<X className="w-6 h-6" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="categoryForm"
|
|
||||||
>
|
|
||||||
ذخیره دستهبندی
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<div className="p-6 overflow-y-auto flex-1">
|
||||||
<div className="space-y-6 text-right font-vazir">
|
|
||||||
<form id="categoryForm" onSubmit={handleSave} className="space-y-6">
|
<form id="categoryForm" onSubmit={handleSave} className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -286,17 +264,16 @@ export default function Categories() {
|
|||||||
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `${BASE_DOMAIN}${formData.imageUrl}`} alt="preview" className="w-16 h-16 rounded-xl object-cover border border-gray-200" />
|
<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">
|
<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" />
|
<ImageIcon className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsMediaSelectorOpen(true)}
|
onClick={() => setIsMediaSelectorOpen(true)}
|
||||||
|
className="px-4 py-2 border-2 border-dashed border-gray-300 rounded-xl text-gray-600 font-bold hover:border-purple-500 hover:text-purple-600 transition-colors"
|
||||||
>
|
>
|
||||||
انتخاب از گالری
|
انتخاب از گالری
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -335,11 +312,28 @@ export default function Categories() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Media Selector Modal */}
|
{/* Media Selector Modal */}
|
||||||
|
|
||||||
<MediaSelector
|
<MediaSelector
|
||||||
isOpen={isMediaSelectorOpen}
|
isOpen={isMediaSelectorOpen}
|
||||||
onClose={() => setIsMediaSelectorOpen(false)}
|
onClose={() => setIsMediaSelectorOpen(false)}
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Mail, MessageSquare } from 'lucide-react';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
interface ContactSubmission {
|
interface ContactSubmission {
|
||||||
id: string;
|
id: string;
|
||||||
@ -157,17 +153,16 @@ export default function ContactSubmissions() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<Button
|
<button
|
||||||
variant="secondary"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedSub(sub);
|
setSelectedSub(sub);
|
||||||
setStatus(sub.status);
|
setStatus(sub.status);
|
||||||
setAdminNotes(sub.adminNotes || '');
|
setAdminNotes(sub.adminNotes || '');
|
||||||
}}
|
}}
|
||||||
|
className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-800 rounded-lg text-xs font-medium transition"
|
||||||
>
|
>
|
||||||
مشاهده & تغییر وضعیت
|
مشاهده & تغییر وضعیت
|
||||||
</Button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@ -225,45 +220,21 @@ export default function ContactSubmissions() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSaveContactInfo}
|
onClick={handleSaveContactInfo}
|
||||||
|
className="px-6 py-2.5 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-medium transition shadow-sm"
|
||||||
>
|
>
|
||||||
ذخیره تغییرات اطلاعات نمایندگی
|
ذخیره تغییرات اطلاعات نمایندگی
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal for Submission Detail & Status Update */}
|
{/* Modal for Submission Detail & Status Update */}
|
||||||
{selectedSub && (
|
{selectedSub && (
|
||||||
<Modal
|
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
isOpen={!!selectedSub}
|
<div className="bg-white rounded-2xl p-6 max-w-lg w-full shadow-xl">
|
||||||
onClose={() => setSelectedSub(null)}
|
<h3 className="text-lg font-bold text-gray-900 mb-4">جزئیات پیام {selectedSub.name}</h3>
|
||||||
title={`جزئیات پیام ${selectedSub.name}`}
|
|
||||||
icon={Mail}
|
|
||||||
maxWidth="lg"
|
|
||||||
footer={
|
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSelectedSub(null)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleUpdateStatus}
|
|
||||||
>
|
|
||||||
ثبت تغییرات
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="space-y-4 text-right font-vazir">
|
|
||||||
<div className="space-y-3 text-sm text-gray-700 mb-6 bg-gray-50 p-4 rounded-xl">
|
<div className="space-y-3 text-sm text-gray-700 mb-6 bg-gray-50 p-4 rounded-xl">
|
||||||
<div><span className="font-semibold text-gray-900">تلفن:</span> {selectedSub.phone}</div>
|
<div><span className="font-semibold text-gray-900">تلفن:</span> {selectedSub.phone}</div>
|
||||||
{selectedSub.email && <div><span className="font-semibold text-gray-900">ایمیل:</span> {selectedSub.email}</div>}
|
{selectedSub.email && <div><span className="font-semibold text-gray-900">ایمیل:</span> {selectedSub.email}</div>}
|
||||||
@ -297,11 +268,24 @@ export default function ContactSubmissions() {
|
|||||||
className="w-full px-3 py-2 border rounded-lg text-sm"
|
className="w-full px-3 py-2 border rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedSub(null)}
|
||||||
|
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleUpdateStatus}
|
||||||
|
className="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg text-sm font-medium"
|
||||||
|
>
|
||||||
|
ثبت تغییرات
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,17 +165,12 @@ export default function Coupons() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مدیریت کدهای تخفیف با اهداف چندگانه (کاربران، حیوانات، محصولات و ...)</p>
|
<p className="text-gray-500 font-medium mt-1">مدیریت کدهای تخفیف با اهداف چندگانه (کاربران، حیوانات، محصولات و ...)</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button onClick={() => openModal()} className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200">
|
||||||
variant="primary"
|
<Plus className="w-5 h-5" />
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
|
||||||
>
|
|
||||||
کد تخفیف جدید
|
کد تخفیف جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
|
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
|
|||||||
@ -5,9 +5,6 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
export interface Doctor {
|
export interface Doctor {
|
||||||
id: string;
|
id: string;
|
||||||
@ -168,14 +165,13 @@ export default function DoctorsManager() {
|
|||||||
ثبت اطلاعات و رزومه (CV) دامپزشکان جهت انتساب به ویدئوها، مشاورهها و معرفی کادر تخصصی
|
ثبت اطلاعات و رزومه (CV) دامپزشکان جهت انتساب به ویدئوها، مشاورهها و معرفی کادر تخصصی
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => handleOpenModal()}
|
onClick={() => handleOpenModal()}
|
||||||
|
className="flex items-center justify-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-5 py-3 rounded-2xl font-bold transition-all shadow-md shadow-purple-200 cursor-pointer text-sm"
|
||||||
>
|
>
|
||||||
افزودن پزشک جدید
|
<Plus className="w-5 h-5" />
|
||||||
</Button>
|
<span>افزودن پزشک جدید</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter / Search */}
|
{/* Filter / Search */}
|
||||||
@ -207,14 +203,13 @@ export default function DoctorsManager() {
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-bold text-gray-800 mb-1">پزشکی یافت نشد</h3>
|
<h3 className="text-lg font-bold text-gray-800 mb-1">پزشکی یافت نشد</h3>
|
||||||
<p className="text-sm text-gray-400 mb-6">اولین پزشک یا متخصص کادر علمی را به سامانه اضافه کنید.</p>
|
<p className="text-sm text-gray-400 mb-6">اولین پزشک یا متخصص کادر علمی را به سامانه اضافه کنید.</p>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => handleOpenModal()}
|
onClick={() => handleOpenModal()}
|
||||||
|
className="inline-flex items-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl font-bold text-sm shadow-sm"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
افزودن اولین پزشک
|
افزودن اولین پزشک
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
@ -233,23 +228,23 @@ export default function DoctorsManager() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleOpenModal(doc)}
|
onClick={() => handleOpenModal(doc)}
|
||||||
className="p-2 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-xl transition-colors cursor-pointer"
|
className="p-2 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-xl transition-colors cursor-pointer"
|
||||||
title="ویرایش اطلاعات"
|
title="ویرایش"
|
||||||
>
|
>
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeleteTargetId(doc.id)}
|
onClick={() => setDeleteTargetId(doc.id)}
|
||||||
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
||||||
title="حذف پزشک"
|
title="حذف"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Avatar & Main Info */}
|
{/* Profile Header */}
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-purple-50 border border-purple-100 overflow-hidden flex items-center justify-center shrink-0">
|
<div className="w-16 h-16 rounded-2xl bg-purple-50 border border-purple-100 flex items-center justify-center overflow-hidden flex-shrink-0 relative">
|
||||||
{doc.avatarUrl ? (
|
{doc.avatarUrl ? (
|
||||||
<img
|
<img
|
||||||
src={doc.avatarUrl.startsWith('http') ? doc.avatarUrl : `${BASE_DOMAIN}${doc.avatarUrl}`}
|
src={doc.avatarUrl.startsWith('http') ? doc.avatarUrl : `${BASE_DOMAIN}${doc.avatarUrl}`}
|
||||||
@ -257,228 +252,199 @@ export default function DoctorsManager() {
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserCheck className="w-8 h-8 text-purple-400" />
|
<UserCheck className="w-7 h-7 text-purple-400" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-extrabold text-gray-900 text-base">{doc.name}</h4>
|
<h3 className="font-extrabold text-gray-900 text-base leading-tight mb-1">{doc.name}</h3>
|
||||||
<p className="text-xs text-purple-600 font-bold mt-0.5">{doc.title || 'پزشک متخصص'}</p>
|
<p className="text-xs text-purple-700 font-bold">{doc.title || 'دامپزشک'}</p>
|
||||||
{doc.clinicName && (
|
{doc.clinicName && (
|
||||||
<p className="text-[11px] text-gray-400 mt-0.5 flex items-center gap-1">
|
<p className="text-xs text-gray-400 font-medium mt-0.5">{doc.clinicName}</p>
|
||||||
<ShieldCheck className="w-3 h-3 text-gray-400" />
|
|
||||||
{doc.clinicName}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bio Summary */}
|
{/* Bio snippet */}
|
||||||
{doc.bio && (
|
{doc.bio && (
|
||||||
<p className="text-xs text-gray-500 line-clamp-2 leading-relaxed mb-4 bg-gray-50/50 p-2.5 rounded-xl border border-gray-100">
|
<p className="text-xs text-gray-500 leading-relaxed line-clamp-2 mb-4 bg-gray-50/70 p-3 rounded-2xl border border-gray-100">
|
||||||
{doc.bio}
|
{doc.bio}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Contact & CV Trigger */}
|
{/* CV Button */}
|
||||||
<div className="flex items-center gap-2 pt-2 border-t border-gray-50 text-xs text-gray-400">
|
|
||||||
{doc.phone && (
|
|
||||||
<span className="flex items-center gap-1 bg-gray-50 px-2 py-1 rounded-lg">
|
|
||||||
<Phone className="w-3 h-3 text-purple-500" />
|
|
||||||
<span className="font-mono text-[11px]">{doc.phone}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{doc.instagram && (
|
|
||||||
<a
|
|
||||||
href={`https://instagram.com/${doc.instagram.replace('@', '')}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="flex items-center gap-1 bg-pink-50 text-pink-600 hover:bg-pink-100 px-2 py-1 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<Globe className="w-3 h-3" />
|
|
||||||
<span className="font-mono text-[11px]">{doc.instagram}</span>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{doc.cv && (
|
{doc.cv && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCvDetailsOpen(doc)}
|
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]"
|
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"
|
||||||
>
|
>
|
||||||
<FileText className="w-3 h-3" />
|
<FileText className="w-3.5 h-3.5" />
|
||||||
مشاهده رزومه
|
<span>مشاهده رزومه و سوابق علمی (CV)</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Footer info */}
|
||||||
|
<div className="pt-4 border-t border-gray-100 flex items-center justify-between text-xs text-gray-400 font-medium">
|
||||||
|
<span>{doc._count?.videos || 0} ویدئوی آموزشی</span>
|
||||||
|
{doc.phone && <span className="font-mono">{doc.phone}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal Add/Edit */}
|
{/* Create / Edit Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden border border-gray-100 animate-in fade-in zoom-in-95 duration-200">
|
||||||
onClose={() => setIsModalOpen(false)}
|
{/* Modal Header */}
|
||||||
title={editingDoctor ? 'ویرایش اطلاعات پزشک' : 'افزودن پزشک جدید به کادر علمی'}
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50">
|
||||||
icon={UserCheck}
|
<div className="flex items-center gap-3">
|
||||||
maxWidth="2xl"
|
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
|
||||||
footer={
|
<UserCheck className="w-5 h-5" />
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="doctorForm"
|
|
||||||
>
|
|
||||||
{editingDoctor ? 'ذخیره تغییرات' : 'افزودن پزشک'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="space-y-6 text-right font-vazir">
|
|
||||||
<form id="doctorForm" onSubmit={handleSave} className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1.5">نام و نام خانوادگی *</label>
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
|
{editingDoctor ? 'ویرایش اطلاعات پزشک' : 'افزودن پزشک جدید'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 font-medium">مشخصات هویتی، آواتار و رزومه علمی پزشک</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSave} className="p-6 overflow-y-auto flex-1 space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Name */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-700 mb-1">
|
||||||
|
نام و نام خانوادگی پزشک <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
placeholder="مثال: دکتر سارا صادقی"
|
placeholder="مثال: دکتر کلاوس هنینگ"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 font-bold"
|
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1.5">عنوان / تخصص *</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">تخصص یا عنوان علمی</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
placeholder="مثال: متخصص ارتوپدی و تغذیه دامپزشکی"
|
||||||
placeholder="مثال: متخصص تغذیه و فیزیولوژی حیوانات خانگی"
|
|
||||||
value={formData.title}
|
value={formData.title}
|
||||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 font-bold"
|
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
{/* Clinic Name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1.5">نام کلینیک / بیمارستان</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">نام بیمارستان / کلینیک یا دانشگاه</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="مثال: بیمارستان دامپزشکی تهران"
|
placeholder="مثال: کلینیک تخصصی دامپزشکی برلین"
|
||||||
value={formData.clinicName}
|
value={formData.clinicName}
|
||||||
onChange={(e) => setFormData({ ...formData, clinicName: e.target.value })}
|
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"
|
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<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 */}
|
{/* Avatar Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1.5">تصویر آواتار / عکس پرتره</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">تصویر پرتره / آواتار</label>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 rounded-xl bg-gray-100 border border-gray-200 overflow-hidden flex items-center justify-center shrink-0">
|
|
||||||
{formData.avatarUrl ? (
|
|
||||||
<img
|
|
||||||
src={formData.avatarUrl.startsWith('http') ? formData.avatarUrl : `${BASE_DOMAIN}${formData.avatarUrl}`}
|
|
||||||
alt="Avatar preview"
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ImageIcon className="w-5 h-5 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="آدرس تصویر یا انتخاب از رسانهها..."
|
placeholder="https://..."
|
||||||
value={formData.avatarUrl}
|
value={formData.avatarUrl}
|
||||||
onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })}
|
||||||
className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
|
className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
|
||||||
dir="ltr"
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsAvatarMediaOpen(true)}
|
onClick={() => setIsAvatarMediaOpen(true)}
|
||||||
|
className="px-4 py-2.5 bg-purple-100 hover:bg-purple-200 text-purple-700 rounded-xl text-xs font-bold flex items-center gap-1.5 transition-colors cursor-pointer flex-shrink-0"
|
||||||
>
|
>
|
||||||
گالری رسانه
|
<ImageIcon className="w-4 h-4" />
|
||||||
</Button>
|
<span>انتخاب از رسانهها</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{formData.avatarUrl && (
|
||||||
|
<div className="mt-2 w-16 h-16 rounded-xl border border-gray-200 overflow-hidden bg-gray-50">
|
||||||
|
<img
|
||||||
|
src={formData.avatarUrl.startsWith('http') ? formData.avatarUrl : `${BASE_DOMAIN}${formData.avatarUrl}`}
|
||||||
|
alt="Preview"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bio & CV */}
|
{/* Bio */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1.5">معرفی کوتاه (Bio)</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">بیوگرافی کوتاه (معرفی سریع)</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="خلاصهای از سوابق و حوزه کاری..."
|
placeholder="معرفی کوتاه پزشک و فعالیتها..."
|
||||||
value={formData.bio}
|
value={formData.bio}
|
||||||
onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
|
||||||
className="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 leading-relaxed"
|
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* CV */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1.5">رزومه کامل و مقالات علمی (CV)</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">
|
||||||
|
رزومه، سوابق تحصیلی و مقالات علمی (CV)
|
||||||
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="سوابق تحصیلی، مقالات چاپشده، مدارک بینالمللی و تجربیات کلینیکی..."
|
placeholder="سوابق تحصیلی، دورههای تخصصی، مقالات منتشر شده، تخصصهای درمانی و..."
|
||||||
value={formData.cv}
|
value={formData.cv}
|
||||||
onChange={(e) => setFormData({ ...formData, cv: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, cv: e.target.value })}
|
||||||
className="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 leading-relaxed font-sans"
|
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600 leading-relaxed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Social & Contact */}
|
{/* Contacts */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">تلفن تماس کلینیک</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">تلفن تماس / واتساپ</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
dir="ltr"
|
placeholder="۰۹۱۲..."
|
||||||
placeholder="02188888888"
|
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||||
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
|
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">ایمیل</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">ایمیل</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
dir="ltr"
|
placeholder="doctor@example.com"
|
||||||
placeholder="dr.sadeghi@canina.ir"
|
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
|
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">آیدی اینستاگرام</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">اینستاگرام</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
dir="ltr"
|
placeholder="@dr_..."
|
||||||
placeholder="@dr.sadeghi"
|
|
||||||
value={formData.instagram}
|
value={formData.instagram}
|
||||||
onChange={(e) => setFormData({ ...formData, instagram: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, instagram: e.target.value })}
|
||||||
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
|
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
|
||||||
@ -498,38 +464,56 @@ export default function DoctorsManager() {
|
|||||||
<span className="text-xs font-bold text-gray-700">پزشک فعال است و در لیست نمایش داده شود</span>
|
<span className="text-xs font-bold text-gray-700">پزشک فعال است و در لیست نمایش داده شود</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="px-5 py-2.5 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-xl text-xs font-bold transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-bold shadow-md shadow-purple-200 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{editingDoctor ? 'ذخیره تغییرات' : 'افزودن پزشک'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* CV Detail Modal */}
|
{/* CV Detail Modal */}
|
||||||
{isCvDetailsOpen && (
|
{isCvDetailsOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs">
|
||||||
isOpen={!!isCvDetailsOpen}
|
<div className="bg-white rounded-3xl max-w-xl w-full p-6 shadow-2xl border border-gray-100 animate-in fade-in zoom-in-95">
|
||||||
onClose={() => setIsCvDetailsOpen(null)}
|
<div className="flex items-center justify-between border-b border-gray-100 pb-4 mb-4">
|
||||||
title={`رزومه علمی — ${isCvDetailsOpen.name}`}
|
<div className="flex items-center gap-3">
|
||||||
icon={FileText}
|
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
|
||||||
maxWidth="lg"
|
<FileText className="w-5 h-5" />
|
||||||
footer={
|
|
||||||
<div className="flex justify-end w-full">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsCvDetailsOpen(null)}
|
|
||||||
>
|
|
||||||
بستن
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
<div>
|
||||||
|
<h3 className="font-extrabold text-gray-900 text-base">{isCvDetailsOpen.name}</h3>
|
||||||
|
<p className="text-xs text-purple-600 font-bold">{isCvDetailsOpen.title}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsCvDetailsOpen(null)}
|
||||||
|
className="p-2 text-gray-400 hover:text-red-500 rounded-xl transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<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">
|
<X className="w-5 h-5" />
|
||||||
|
</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}
|
{isCvDetailsOpen.cv}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Media Selector Modal for Avatar */}
|
{/* Media Selector Modal for Avatar */}
|
||||||
<MediaSelector
|
<MediaSelector
|
||||||
isOpen={isAvatarMediaOpen}
|
isOpen={isAvatarMediaOpen}
|
||||||
|
|||||||
@ -4,9 +4,6 @@ import { toast } from 'react-hot-toast';
|
|||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
interface FAQ {
|
interface FAQ {
|
||||||
id: string;
|
id: string;
|
||||||
@ -132,14 +129,13 @@ export default function FAQManager() {
|
|||||||
سوالات و پاسخهایی که در صفحه اصلی سایت نمایش داده میشوند — با Schema.org FAQPage برای SEO
|
سوالات و پاسخهایی که در صفحه اصلی سایت نمایش داده میشوند — با Schema.org FAQPage برای SEO
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-orange-500 hover:bg-orange-600 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-orange-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
افزودن سوال جدید
|
افزودن سوال جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info Box */}
|
{/* Info Box */}
|
||||||
@ -151,7 +147,7 @@ export default function FAQManager() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center py-12">
|
<div className="flex justify-center py-12">
|
||||||
<Spinner size="lg" className="text-purple-600" />
|
<Spinner size="lg" className="text-orange-500" />
|
||||||
</div>
|
</div>
|
||||||
) : faqs.length === 0 ? (
|
) : faqs.length === 0 ? (
|
||||||
<div className="bg-white rounded-2xl border border-gray-200 py-16 text-center">
|
<div className="bg-white rounded-2xl border border-gray-200 py-16 text-center">
|
||||||
@ -159,71 +155,56 @@ export default function FAQManager() {
|
|||||||
<p className="text-gray-500 font-medium">هیچ سوالی ثبت نشده است</p>
|
<p className="text-gray-500 font-medium">هیچ سوالی ثبت نشده است</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
className="mt-4 text-purple-600 font-bold hover:underline text-sm cursor-pointer"
|
className="mt-4 text-orange-500 font-bold hover:underline text-sm"
|
||||||
>
|
>
|
||||||
اولین سوال را اضافه کنید
|
اولین سوال را اضافه کنید
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
faqs.map((faq) => (
|
faqs.map((faq, idx) => (
|
||||||
<div
|
<div
|
||||||
key={faq.id}
|
key={faq.id}
|
||||||
className={`bg-white rounded-2xl border p-5 transition-all ${
|
className={`bg-white rounded-2xl border p-5 transition-all ${
|
||||||
faq.isActive ? 'border-gray-200 shadow-sm' : 'border-gray-100 opacity-60 bg-gray-50'
|
faq.isActive ? 'border-gray-200' : 'border-dashed border-gray-300 opacity-60'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="flex items-start gap-3 flex-1">
|
<div className="flex items-center gap-2 shrink-0 mt-1">
|
||||||
<div className="p-2 bg-orange-50 text-orange-600 rounded-xl mt-0.5 shrink-0">
|
<GripVertical className="w-4 h-4 text-gray-300" />
|
||||||
<HelpCircle className="w-5 h-5" />
|
<span className="text-xs font-black text-gray-400 w-5 text-center">{idx + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="text-xs font-bold px-2 py-0.5 bg-gray-100 text-gray-600 rounded-md">
|
<span className="font-black text-gray-900 text-sm">{faq.question}</span>
|
||||||
ترتیب: {faq.order}
|
{!faq.isActive && (
|
||||||
</span>
|
<span className="px-2 py-0.5 bg-gray-100 text-gray-500 text-[10px] font-bold rounded">
|
||||||
<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>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-gray-900 text-base">{faq.question}</h3>
|
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2">{faq.answer}</p>
|
||||||
<p className="text-gray-600 text-sm leading-relaxed whitespace-pre-line">
|
|
||||||
{faq.answer}
|
|
||||||
</p>
|
|
||||||
</div>
|
</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)}
|
||||||
title={faq.isActive ? 'غیرفعالسازی' : 'فعالسازی'}
|
className={`p-2 rounded-lg transition-colors ${
|
||||||
className={`p-2 rounded-xl transition-colors cursor-pointer ${
|
|
||||||
faq.isActive
|
faq.isActive
|
||||||
? 'text-green-600 hover:bg-green-50'
|
? 'text-green-600 hover:bg-green-50'
|
||||||
: 'text-gray-400 hover:bg-gray-100'
|
: 'text-gray-400 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
|
title={faq.isActive ? 'غیرفعال کردن' : 'فعال کردن'}
|
||||||
>
|
>
|
||||||
{faq.isActive ? (
|
{faq.isActive ? <ToggleRight className="w-5 h-5" /> : <ToggleLeft className="w-5 h-5" />}
|
||||||
<ToggleRight className="w-5 h-5" />
|
|
||||||
) : (
|
|
||||||
<ToggleLeft className="w-5 h-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openModal(faq)}
|
onClick={() => openModal(faq)}
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-xl transition-colors cursor-pointer"
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeleteTargetId(faq.id)}
|
onClick={() => setDeleteTargetId(faq.id)}
|
||||||
className="p-2 text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -234,49 +215,31 @@ export default function FAQManager() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Form Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'}
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
icon={HelpCircle}
|
{editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'}
|
||||||
maxWidth="2xl"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<X className="w-5 h-5 text-gray-500" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="faqForm"
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
|
||||||
ذخیره سوال
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<form onSubmit={handleSave} className="p-6 space-y-4">
|
||||||
<div className="space-y-6 text-right font-vazir">
|
|
||||||
<form id="faqForm" onSubmit={handleSave} className="space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">
|
<label className="block text-sm font-bold text-gray-700 mb-1">
|
||||||
سوال <span className="text-red-500">*</span>
|
سوال <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
required
|
|
||||||
type="text"
|
type="text"
|
||||||
|
required
|
||||||
value={formData.question}
|
value={formData.question}
|
||||||
onChange={(e) => setFormData({ ...formData, question: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, question: e.target.value })}
|
||||||
placeholder="مثال: آیا محصولات کنینا برای سگهای حساس مناسب است؟"
|
placeholder="مثال: آیا محصولات کنینا برای سگهای حساس مناسب است؟"
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -290,7 +253,7 @@ export default function FAQManager() {
|
|||||||
value={formData.answer}
|
value={formData.answer}
|
||||||
onChange={(e) => setFormData({ ...formData, answer: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, answer: e.target.value })}
|
||||||
placeholder="پاسخ کامل، واضح و مفید..."
|
placeholder="پاسخ کامل، واضح و مفید..."
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm resize-none"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -301,28 +264,44 @@ export default function FAQManager() {
|
|||||||
type="number"
|
type="number"
|
||||||
value={formData.order}
|
value={formData.order}
|
||||||
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
|
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<label className="flex items-center justify-between p-3 bg-purple-50 rounded-xl border border-purple-200 cursor-pointer w-full">
|
<label className="flex items-center justify-between p-3 bg-orange-50 rounded-xl border border-orange-200 cursor-pointer w-full">
|
||||||
<span className="text-xs font-bold text-purple-700">✅ فعال و قابل نمایش</span>
|
<span className="text-xs font-bold text-orange-700">✅ فعال و قابل نمایش</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.isActive}
|
checked={formData.isActive}
|
||||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||||||
className="rounded text-purple-600 focus:ring-purple-500"
|
className="rounded text-orange-500 focus:ring-orange-400"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-orange-500 text-white font-bold hover:bg-orange-600 transition-colors shadow-md shadow-orange-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isSaving && <Spinner size="sm" />}
|
||||||
|
ذخیره
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{deleteTargetId && (
|
{deleteTargetId && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
|||||||
@ -4,10 +4,8 @@ import { toast } from 'react-hot-toast';
|
|||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import PriceInput from '../components/ui/PriceInput';
|
import PriceInput from '../components/ui/PriceInput';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import type { FinancialSettings } from '../types/admin';
|
import type { FinancialSettings } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
export default function FinancialSettingsPage() {
|
export default function FinancialSettingsPage() {
|
||||||
const [financial, setFinancial] = useState<FinancialSettings>({
|
const [financial, setFinancial] = useState<FinancialSettings>({
|
||||||
taxPercentage: 10,
|
taxPercentage: 10,
|
||||||
@ -171,15 +169,13 @@ export default function FinancialSettingsPage() {
|
|||||||
onChange={(val) => setDonationInput(val)}
|
onChange={(val) => setDonationInput(val)}
|
||||||
className="px-4 py-2 rounded-xl border border-gray-200 text-xs"
|
className="px-4 py-2 rounded-xl border border-gray-200 text-xs"
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
|
||||||
size="xs"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={addDonationOption}
|
onClick={addDonationOption}
|
||||||
|
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-bold flex items-center gap-1 shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
افزودن
|
<Plus className="w-4 h-4" /> افزودن
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 pt-2">
|
<div className="flex flex-wrap gap-2 pt-2">
|
||||||
@ -270,19 +266,17 @@ export default function FinancialSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end pt-4 border-t border-gray-100">
|
<div className="flex justify-end pt-4 border-t border-gray-100">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isSaving}
|
||||||
size="md"
|
className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-emerald-200 cursor-pointer"
|
||||||
startIcon={Save}
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
>
|
||||||
|
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
|
||||||
ذخیره تنظیمات مالی
|
ذخیره تنظیمات مالی
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,8 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
import type { Ingredient } from '../types/admin';
|
import type { Ingredient } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
export default function IngredientsManager() {
|
export default function IngredientsManager() {
|
||||||
const [ingredients, setIngredients] = useState<Ingredient[]>([]);
|
const [ingredients, setIngredients] = useState<Ingredient[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@ -167,17 +164,15 @@ export default function IngredientsManager() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مدیریت مواد موثره، ترکیبات دارویی و خواص درمانی محصولات کنینا</p>
|
<p className="text-gray-500 font-medium mt-1">مدیریت مواد موثره، ترکیبات دارویی و خواص درمانی محصولات کنینا</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
افزودن ترکیب جدید
|
افزودن ترکیب جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-right">
|
<table className="w-full text-right">
|
||||||
@ -249,39 +244,21 @@ export default function IngredientsManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Form Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200 max-h-[90vh] flex flex-col">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={editingIngredient ? 'ویرایش ترکیب دانشنامه' : 'افزودن ترکیب جدید'}
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
icon={FlaskConical}
|
{editingIngredient ? 'ویرایش ترکیب دانشنامه' : 'افزودن ترکیب جدید'}
|
||||||
maxWidth="2xl"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<X className="w-6 h-6" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="ingredientForm"
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
|
||||||
ذخیره تغییرات
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<form onSubmit={handleSave} className="p-6 space-y-4 overflow-y-auto flex-1">
|
||||||
<div className="space-y-6 text-right font-vazir">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<form id="ingredientForm" onSubmit={handleSave} className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">نام فارسی *</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">نام فارسی *</label>
|
||||||
<input
|
<input
|
||||||
@ -308,7 +285,7 @@ export default function IngredientsManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه یکتا (slug)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه یکتا (slug)</label>
|
||||||
<input
|
<input
|
||||||
@ -335,29 +312,28 @@ export default function IngredientsManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر ترکیب</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر ترکیب (imageUrl)</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.imageUrl}
|
value={formData.imageUrl}
|
||||||
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
|
||||||
placeholder="آدرس تصویر یا انتخاب از رسانهها..."
|
placeholder="https://... یا /uploads/..."
|
||||||
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsMediaSelectorOpen(true)}
|
onClick={() => setIsMediaSelectorOpen(true)}
|
||||||
|
className="px-3 py-2.5 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0"
|
||||||
>
|
>
|
||||||
انتخاب از رسانه
|
رسانه
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">خواص و مزایا</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">خواص و مزایا (benefits array)</label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
@ -371,23 +347,22 @@ export default function IngredientsManager() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="تایپ کنید و اینتر بزنید (مثال: ترمیم غضروف)"
|
placeholder="تایپ کنید و اینتر بزنید (مثال: ترمیم غضروف)"
|
||||||
className="flex-1 px-4 py-2 rounded-xl border border-gray-200 text-xs outline-none focus:border-purple-500"
|
className="flex-1 px-4 py-2 rounded-xl border border-gray-200 text-xs outline-none"
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
|
||||||
size="xs"
|
|
||||||
onClick={addBenefitTag}
|
onClick={addBenefitTag}
|
||||||
|
className="px-3 py-2 bg-purple-600 text-white rounded-xl text-xs font-bold"
|
||||||
>
|
>
|
||||||
افزودن
|
+ افزودن
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5 p-2 bg-gray-50 rounded-xl border border-gray-100 min-h-[40px]">
|
<div className="flex flex-wrap gap-1.5 p-2 bg-gray-50 rounded-xl border border-gray-100 min-h-[40px]">
|
||||||
{formData.benefits.map((b, i) => (
|
{formData.benefits.map((b, i) => (
|
||||||
<span key={i} className="inline-flex items-center gap-1 px-2.5 py-1 bg-purple-100 text-purple-800 text-xs font-bold rounded-lg">
|
<span key={i} className="inline-flex items-center gap-1 px-2.5 py-1 bg-purple-100 text-purple-800 text-xs font-bold rounded-lg">
|
||||||
<Tag className="w-3 h-3" />
|
<Tag className="w-3 h-3" />
|
||||||
{b}
|
{b}
|
||||||
<button type="button" onClick={() => removeBenefitTag(b)} className="hover:text-red-500 cursor-pointer">
|
<button type="button" onClick={() => removeBenefitTag(b)} className="hover:text-red-500">
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@ -406,12 +381,29 @@ export default function IngredientsManager() {
|
|||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isSaving && <Spinner size="sm" />}
|
||||||
|
ذخیره تغییرات
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
{deleteTargetId && (
|
{deleteTargetId && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
|||||||
@ -6,12 +6,6 @@ import Spinner from '../components/ui/Spinner';
|
|||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import ImagePreviewModal from '../components/ui/ImagePreviewModal';
|
import ImagePreviewModal from '../components/ui/ImagePreviewModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface Media {
|
interface Media {
|
||||||
id: string;
|
id: string;
|
||||||
@ -305,20 +299,26 @@ export default function MediaManager() {
|
|||||||
multiple
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Upload}
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
isLoading={isUploading}
|
className="bg-purple-600 hover:bg-purple-700 disabled:opacity-70 text-white px-4 py-2.5 rounded-xl flex items-center gap-2 text-sm font-bold transition-all shadow-md shadow-purple-200 cursor-pointer"
|
||||||
>
|
>
|
||||||
{isUploading ? `در حال آپلود ${uploadProgress ? `(${uploadProgress}٪)` : ''}...` : 'آپلود فایل جدید'}
|
{isUploading ? (
|
||||||
</Button>
|
<>
|
||||||
|
<Spinner size="sm" />
|
||||||
|
<span>در حال آپلود {uploadProgress ? `(${uploadProgress}٪)` : ''}...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
<span>آپلود فایل جدید (عکس، ویدئو، پادکست، PDF)</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Upload Progress Bar if active */}
|
{/* Upload Progress Bar if active */}
|
||||||
{isUploading && uploadProgress !== null && (
|
{isUploading && uploadProgress !== null && (
|
||||||
<div className="w-full bg-gray-100 rounded-full h-2 overflow-hidden shadow-inner">
|
<div className="w-full bg-gray-100 rounded-full h-2 overflow-hidden shadow-inner">
|
||||||
@ -596,34 +596,17 @@ export default function MediaManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit SEO Modal */}
|
|
||||||
{editingSeoMedia && (
|
{editingSeoMedia && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
|
||||||
isOpen={!!editingSeoMedia}
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden p-6 space-y-4">
|
||||||
onClose={() => setEditingSeoMedia(null)}
|
<div className="flex items-center justify-between border-b pb-3">
|
||||||
title="تنظیمات سئوی تصویر"
|
<h3 className="text-lg font-bold text-gray-900">تنظیمات سئوی تصویر</h3>
|
||||||
icon={ImageIcon}
|
<button onClick={() => setEditingSeoMedia(null)} className="text-gray-400 hover:text-red-500">
|
||||||
maxWidth="md"
|
<X className="w-5 h-5" />
|
||||||
footer={
|
</button>
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setEditingSeoMedia(null)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSaveSeo}
|
|
||||||
>
|
|
||||||
ذخیره سئو
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<div className="space-y-3 text-right">
|
||||||
<div className="space-y-3 text-right font-vazir">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-bold text-gray-700 block mb-1">متن جایگزین (Alt Text) *</label>
|
<label className="text-xs font-bold text-gray-700 block mb-1">متن جایگزین (Alt Text) *</label>
|
||||||
<input
|
<input
|
||||||
@ -668,9 +651,26 @@ export default function MediaManager() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-2 border-t">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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
|
||||||
|
type="button"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={!!deleteTargetId}
|
isOpen={!!deleteTargetId}
|
||||||
|
|||||||
@ -7,10 +7,6 @@ import { toast } from 'react-hot-toast';
|
|||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -178,17 +174,15 @@ export default function MenuManager() {
|
|||||||
مدیریت آیتمهای هدر و فوتر سایت — تغییرات بلادرنگ اعمال میشوند
|
مدیریت آیتمهای هدر و فوتر سایت — تغییرات بلادرنگ اعمال میشوند
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-blue-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
آیتم جدید
|
آیتم جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{MENU_TABS.map((tab) => (
|
{MENU_TABS.map((tab) => (
|
||||||
@ -298,45 +292,27 @@ export default function MenuManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Form Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-xl shadow-2xl overflow-hidden">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'}
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
icon={Menu}
|
{editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'}
|
||||||
maxWidth="xl"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<X className="w-5 h-5 text-gray-500" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="menuForm"
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
|
||||||
ذخیره
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<form onSubmit={handleSave} className="p-6 space-y-4">
|
||||||
<div className="space-y-6 text-right font-vazir">
|
|
||||||
<form id="menuForm" onSubmit={handleSave} className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">موقعیت در منو</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">موقعیت در منو</label>
|
||||||
<select
|
<select
|
||||||
value={formData.menuType}
|
value={formData.menuType}
|
||||||
onChange={(e) => setFormData({ ...formData, menuType: e.target.value as MenuType, parentId: null })}
|
onChange={(e) => setFormData({ ...formData, menuType: e.target.value as MenuType, parentId: null })}
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm bg-white"
|
||||||
>
|
>
|
||||||
{MENU_TABS.map((tab) => (
|
{MENU_TABS.map((tab) => (
|
||||||
<option key={tab.key} value={tab.key}>{tab.label}</option>
|
<option key={tab.key} value={tab.key}>{tab.label}</option>
|
||||||
@ -348,7 +324,7 @@ export default function MenuManager() {
|
|||||||
<select
|
<select
|
||||||
value={formData.parentId || ''}
|
value={formData.parentId || ''}
|
||||||
onChange={(e) => setFormData({ ...formData, parentId: e.target.value || null })}
|
onChange={(e) => setFormData({ ...formData, parentId: e.target.value || null })}
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm bg-white"
|
||||||
>
|
>
|
||||||
<option value="">— آیتم اصلی —</option>
|
<option value="">— آیتم اصلی —</option>
|
||||||
{parentItems.map((p) => (
|
{parentItems.map((p) => (
|
||||||
@ -366,7 +342,7 @@ export default function MenuManager() {
|
|||||||
value={formData.label}
|
value={formData.label}
|
||||||
onChange={(e) => setFormData({ ...formData, label: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, label: e.target.value })}
|
||||||
placeholder="مثال: فروشگاه تخصصی"
|
placeholder="مثال: فروشگاه تخصصی"
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -379,7 +355,7 @@ export default function MenuManager() {
|
|||||||
onChange={(e) => setFormData({ ...formData, href: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, href: e.target.value })}
|
||||||
placeholder="مثال: /shop یا https://..."
|
placeholder="مثال: /shop یا https://..."
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -391,7 +367,7 @@ export default function MenuManager() {
|
|||||||
value={formData.icon}
|
value={formData.icon}
|
||||||
onChange={(e) => setFormData({ ...formData, icon: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, icon: e.target.value })}
|
||||||
placeholder="🐕 یا نام آیکون"
|
placeholder="🐕 یا نام آیکون"
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -401,7 +377,7 @@ export default function MenuManager() {
|
|||||||
value={formData.badge}
|
value={formData.badge}
|
||||||
onChange={(e) => setFormData({ ...formData, badge: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, badge: e.target.value })}
|
||||||
placeholder="مثال: جدید"
|
placeholder="مثال: جدید"
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -413,7 +389,7 @@ export default function MenuManager() {
|
|||||||
type="number"
|
type="number"
|
||||||
value={formData.order}
|
value={formData.order}
|
||||||
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
|
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center justify-between p-3 bg-green-50 rounded-xl border border-green-200 cursor-pointer col-span-1 mt-auto">
|
<label className="flex items-center justify-between p-3 bg-green-50 rounded-xl border border-green-200 cursor-pointer col-span-1 mt-auto">
|
||||||
@ -435,12 +411,29 @@ export default function MenuManager() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-blue-600 text-white font-bold hover:bg-blue-700 shadow-md shadow-blue-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isSaving && <Spinner size="sm" />}
|
||||||
|
ذخیره
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{deleteTargetId && (
|
{deleteTargetId && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
|||||||
@ -15,9 +15,6 @@ import {
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function PaymentGatewaysPage() {
|
export default function PaymentGatewaysPage() {
|
||||||
const [activeTab, setActiveTab] = useState<'gateways' | 'wallets' | 'ebank' | 'submerchants'>('gateways');
|
const [activeTab, setActiveTab] = useState<'gateways' | 'wallets' | 'ebank' | 'submerchants'>('gateways');
|
||||||
@ -493,20 +490,18 @@ export default function PaymentGatewaysPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isSaving}
|
||||||
size="md"
|
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-indigo-200 cursor-pointer text-sm"
|
||||||
startIcon={Save}
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
>
|
||||||
ذخیره کلیه تنظیمات درگاه زیبال
|
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
|
||||||
</Button>
|
<span>ذخیره کلیه تنظیمات درگاه زیبال</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{activeTab === 'wallets' && (
|
{activeTab === 'wallets' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Wallets Overview */}
|
{/* Wallets Overview */}
|
||||||
|
|||||||
@ -5,9 +5,6 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
export interface Pet {
|
export interface Pet {
|
||||||
id: string;
|
id: string;
|
||||||
@ -218,25 +215,28 @@ export default function Pets() {
|
|||||||
|
|
||||||
{/* Pet Medical Dossier Details Modal */}
|
{/* Pet Medical Dossier Details Modal */}
|
||||||
{selectedPetForView && (
|
{selectedPetForView && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir" dir="rtl">
|
||||||
isOpen={!!selectedPetForView}
|
<div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-gray-100 flex flex-col">
|
||||||
onClose={() => setSelectedPetForView(null)}
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between sticky top-0 bg-white z-10">
|
||||||
title={`پرونده پزشکی و پایش سلامت: ${selectedPetForView.name}`}
|
<div className="flex items-center gap-3">
|
||||||
icon={selectedPetForView.type === 'گربه' ? Cat : Dog}
|
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-700 flex items-center justify-center font-black">
|
||||||
maxWidth="2xl"
|
{selectedPetForView.type === 'گربه' ? <Cat className="w-5 h-5" /> : <Dog className="w-5 h-5" />}
|
||||||
footer={
|
|
||||||
<div className="flex justify-end w-full">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSelectedPetForView(null)}
|
|
||||||
>
|
|
||||||
بستن پرونده
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
<div>
|
||||||
|
<h3 className="text-base font-black text-gray-900">پرونده پزشکی و پایش سلامت: {selectedPetForView.name}</h3>
|
||||||
|
<p className="text-xs text-gray-400 font-mono">شناسه: {selectedPetForView.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPetForView(null)}
|
||||||
|
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="space-y-6 text-right font-vazir">
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
{/* Pet Info Banner */}
|
{/* Pet Info Banner */}
|
||||||
<div className="p-4 bg-purple-50/60 rounded-2xl border border-purple-100 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 text-xs">
|
<div className="p-4 bg-purple-50/60 rounded-2xl border border-purple-100 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 text-xs">
|
||||||
<div>
|
<div>
|
||||||
@ -338,10 +338,10 @@ export default function Pets() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={!!deleteTargetId}
|
isOpen={!!deleteTargetId}
|
||||||
title="حذف حیوان خانگی"
|
title="حذف حیوان خانگی"
|
||||||
|
|||||||
@ -5,11 +5,8 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ImagePreviewModal from '../components/ui/ImagePreviewModal';
|
import ImagePreviewModal from '../components/ui/ImagePreviewModal';
|
||||||
import Badge from '../components/ui/Badge';
|
import Badge from '../components/ui/Badge';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
import type { Prescription } from '../types/admin';
|
import type { Prescription } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
interface ProductItem {
|
interface ProductItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -246,13 +243,12 @@ export default function PrescriptionsManager() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => openReviewModal(rx)}
|
onClick={() => openReviewModal(rx)}
|
||||||
|
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-bold transition-all shadow-xs cursor-pointer"
|
||||||
>
|
>
|
||||||
بررسی و تجویز
|
بررسی و تجویز
|
||||||
</Button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@ -265,36 +261,27 @@ export default function PrescriptionsManager() {
|
|||||||
|
|
||||||
{/* Review Modal */}
|
{/* Review Modal */}
|
||||||
{selectedRx && (
|
{selectedRx && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir" dir="rtl">
|
||||||
isOpen={!!selectedRx}
|
<div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-gray-100 flex flex-col">
|
||||||
onClose={() => setSelectedRx(null)}
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50 sticky top-0 bg-white z-10">
|
||||||
title="بررسی و ثبت نظر تخصصی نسخه"
|
<div className="flex items-center gap-3">
|
||||||
icon={FileText}
|
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
|
||||||
maxWidth="2xl"
|
<FileText className="w-5 h-5" />
|
||||||
footer={
|
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedRx(null)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="prescriptionForm"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
ثبت نهایی و ارسال پیامک
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">بررسی و ثبت نظر تخصصی نسخه</h3>
|
||||||
|
<p className="text-xs text-gray-500">کد نسخه: {selectedRx.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedRx(null)}
|
||||||
|
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="space-y-5 text-right font-vazir">
|
<X className="w-5 h-5" />
|
||||||
<form id="prescriptionForm" onSubmit={handleReviewSubmit} className="space-y-5">
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleReviewSubmit} className="p-6 space-y-5">
|
||||||
{/* Pet & User Details Card */}
|
{/* Pet & User Details Card */}
|
||||||
<div className="p-4 bg-gray-50 rounded-2xl border border-gray-200 text-xs space-y-2">
|
<div className="p-4 bg-gray-50 rounded-2xl border border-gray-200 text-xs space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -308,68 +295,58 @@ export default function PrescriptionsManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedRx.notes && (
|
{selectedRx.notes && (
|
||||||
<div className="pt-2 border-t border-gray-200 text-gray-500">
|
<div className="pt-2 border-t border-gray-200 text-gray-700">
|
||||||
<span className="font-bold text-gray-700">توضیحات کاربر: </span>
|
<strong className="block mb-0.5 text-gray-900">یادداشت کاربر:</strong>
|
||||||
{selectedRx.notes}
|
{selectedRx.notes}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="pt-2 border-t border-gray-200 flex justify-between items-center">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="xs"
|
|
||||||
startIcon={Eye}
|
|
||||||
onClick={() => setPreviewImageUrl(getFileFullUrl(selectedRx.fileUrl))}
|
|
||||||
>
|
|
||||||
مشاهده تصویر نسخه بارگذاری شده
|
|
||||||
</Button>
|
|
||||||
<span className="text-[11px] text-gray-400 font-mono">
|
|
||||||
تاریخ ثبت: {selectedRx.createdAt ? new Date(selectedRx.createdAt).toLocaleDateString('fa-IR') : '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Decision */}
|
{/* Status Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-black text-gray-700 mb-2">تصمیم بالینی و وضعیت تایید *</label>
|
<label className="block text-xs font-black text-gray-700 mb-2">تغییر وضعیت نسخه *</label>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{[
|
|
||||||
{ 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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={s.id}
|
onClick={() => setReviewStatus('APPROVED')}
|
||||||
onClick={() => setReviewStatus(s.id as any)}
|
className={`py-3 px-4 rounded-xl border font-bold text-xs flex items-center justify-center gap-2 transition-all cursor-pointer ${
|
||||||
className={`p-3 rounded-2xl border text-xs font-bold text-center transition-all cursor-pointer ${
|
reviewStatus === 'APPROVED'
|
||||||
reviewStatus === s.id
|
? 'border-green-500 bg-green-50 text-green-700 shadow-xs font-black'
|
||||||
? `${s.color} shadow-xs ring-2 ring-purple-600/20`
|
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
|
||||||
: 'border-gray-200 text-gray-600 hover:border-gray-300'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{s.label}
|
<CheckCircle2 className="w-4 h-4" /> تایید و ثبت تجویز (APPROVED)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setReviewStatus('REJECTED')}
|
||||||
|
className={`py-3 px-4 rounded-xl border font-bold text-xs flex items-center justify-center gap-2 transition-all cursor-pointer ${
|
||||||
|
reviewStatus === 'REJECTED'
|
||||||
|
? 'border-red-500 bg-red-50 text-red-700 shadow-xs font-black'
|
||||||
|
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<XCircle className="w-4 h-4" /> عدم تایید / رد نسخه (REJECTED)
|
||||||
</button>
|
</button>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recommended Products Selector */}
|
{/* Prescribed Products Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-black text-gray-700 mb-1">
|
<label className="block text-xs font-black text-gray-700 mb-1 flex items-center justify-between">
|
||||||
داروها و مکملهای تجویزی (انتخاب از محصولات سایت برای خرید فوری سرپرست)
|
<span>محصولات و مکملهای تجویزی پزشک (جهت خرید آسان کاربر)</span>
|
||||||
|
<span className="text-[10px] text-gray-400 font-normal">{selectedProductIds.length} محصول انتخاب شده</span>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-[11px] text-gray-400 mb-2">محصولاتی که تیک میزنید مستقیماً در پنل کاربر به عنوان سبد پیشنهادی نسخه قرار میگیرند.</p>
|
<div className="max-h-40 overflow-y-auto border border-gray-200 rounded-xl p-2 space-y-1 bg-white">
|
||||||
|
{products.map(p => {
|
||||||
<div className="max-h-48 overflow-y-auto border border-gray-200 rounded-2xl p-2 space-y-1 bg-white">
|
|
||||||
{products.map((p) => {
|
|
||||||
const isSelected = selectedProductIds.includes(p.id);
|
const isSelected = selectedProductIds.includes(p.id);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedProductIds((prev) =>
|
setSelectedProductIds(prev =>
|
||||||
isSelected ? prev.filter((id) => id !== p.id) : [...prev, p.id]
|
isSelected ? prev.filter(id => id !== p.id) : [...prev, p.id]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className={`p-2 rounded-lg text-xs font-bold flex items-center justify-between cursor-pointer transition-colors ${
|
className={`p-2 rounded-lg text-xs font-bold flex items-center justify-between cursor-pointer transition-colors ${
|
||||||
@ -402,12 +379,29 @@ export default function PrescriptionsManager() {
|
|||||||
className="w-full px-3.5 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs leading-relaxed"
|
className="w-full px-3.5 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs leading-relaxed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedRx(null)}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 text-xs font-bold hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white text-xs font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
{isSubmitting && <Spinner size="sm" />}
|
||||||
|
ثبت نهایی و ارسال پیامک
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Fullscreen Lightbox for Prescription Image */}
|
{/* Fullscreen Lightbox for Prescription Image */}
|
||||||
<ImagePreviewModal
|
<ImagePreviewModal
|
||||||
isOpen={!!previewImageUrl}
|
isOpen={!!previewImageUrl}
|
||||||
|
|||||||
@ -8,9 +8,6 @@ import Pagination from '../components/ui/Pagination';
|
|||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import PriceInput from '../components/ui/PriceInput';
|
import PriceInput from '../components/ui/PriceInput';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: string;
|
id: string;
|
||||||
@ -438,17 +435,15 @@ export default function Products() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">افزودن و ویرایش پیشرفته محصولات با تنظیمات سئو</p>
|
<p className="text-gray-500 font-medium mt-1">افزودن و ویرایش پیشرفته محصولات با تنظیمات سئو</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
محصول جدید
|
محصول جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Filters Bar */}
|
{/* Filters Bar */}
|
||||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col md:flex-row gap-4">
|
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col md:flex-row gap-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
@ -1655,29 +1650,18 @@ export default function Products() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-2.5 rounded-b-2xl">
|
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
|
||||||
<Button
|
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
<button type="submit" form="productForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="productForm"
|
|
||||||
>
|
|
||||||
ذخیره محصول
|
ذخیره محصول
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Media Selector Modal */}
|
{/* Media Selector Modal */}
|
||||||
<MediaSelector
|
<MediaSelector
|
||||||
isOpen={isMediaSelectorOpen}
|
isOpen={isMediaSelectorOpen}
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { TrendingUp, DollarSign, ShoppingBag, Users, HeartHandshake, Download } from 'lucide-react';
|
import { TrendingUp, DollarSign, ShoppingBag, Users, HeartHandshake, Download } from 'lucide-react';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
|
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
|
||||||
BarChart, Bar, PieChart, Pie, Cell, Legend
|
BarChart, Bar, PieChart, Pie, Cell, Legend
|
||||||
@ -101,10 +97,7 @@ export default function Reports() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">آمار فروش، رفتار کاربران و عملکرد تخفیفها</p>
|
<p className="text-gray-500 font-medium mt-1">آمار فروش، رفتار کاربران و عملکرد تخفیفها</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Download}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!reportData) return;
|
if (!reportData) return;
|
||||||
const rows = [
|
const rows = [
|
||||||
@ -126,12 +119,13 @@ export default function Reports() {
|
|||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
}}
|
}}
|
||||||
|
className="bg-white border-2 border-purple-200 text-purple-700 hover:bg-purple-50 px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
|
<Download className="w-5 h-5" />
|
||||||
خروجی اکسل (Excel)
|
خروجی اکسل (Excel)
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Overview Cards */}
|
{/* Overview Cards */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<div className="bg-gradient-to-br from-purple-500 to-indigo-600 p-6 rounded-2xl text-white shadow-lg shadow-purple-200">
|
<div className="bg-gradient-to-br from-purple-500 to-indigo-600 p-6 rounded-2xl text-white shadow-lg shadow-purple-200">
|
||||||
|
|||||||
@ -18,10 +18,6 @@ import {
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface ProductReview {
|
interface ProductReview {
|
||||||
id: string;
|
id: string;
|
||||||
@ -438,34 +434,21 @@ export default function Reviews() {
|
|||||||
|
|
||||||
{/* Reply Modal */}
|
{/* Reply Modal */}
|
||||||
{replyingReview && (
|
{replyingReview && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
isOpen={!!replyingReview}
|
<div className="bg-white rounded-2xl p-6 w-full max-w-lg shadow-2xl space-y-4 font-vazir animate-in fade-in zoom-in-95 duration-150" dir="rtl">
|
||||||
onClose={() => setReplyingReview(null)}
|
<div className="flex items-center justify-between border-b border-gray-100 pb-3">
|
||||||
title={`پاسخ به دیدگاه ${replyingReview.userName}`}
|
<h3 className="text-base font-black text-gray-900 flex items-center gap-2">
|
||||||
icon={Reply}
|
<Reply className="w-5 h-5 text-purple-600" />
|
||||||
maxWidth="lg"
|
پاسخ به دیدگاه {replyingReview.userName}
|
||||||
footer={
|
</h3>
|
||||||
<div className="flex items-center justify-end gap-2.5 w-full">
|
<button
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setReplyingReview(null)}
|
onClick={() => setReplyingReview(null)}
|
||||||
|
className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
انصراف
|
✕
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
isLoading={isSavingReply}
|
|
||||||
onClick={handleSaveReply}
|
|
||||||
>
|
|
||||||
ثبت و انتشار پاسخ
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="space-y-4 font-vazir text-right">
|
|
||||||
<div className="p-3 bg-gray-50 rounded-xl border border-gray-100 text-xs text-gray-600 space-y-1">
|
<div className="p-3 bg-gray-50 rounded-xl border border-gray-100 text-xs text-gray-600 space-y-1">
|
||||||
<span className="font-bold text-gray-700 block">متن دیدگاه خریدار:</span>
|
<span className="font-bold text-gray-700 block">متن دیدگاه خریدار:</span>
|
||||||
<p className="leading-relaxed whitespace-pre-wrap">{replyingReview.comment}</p>
|
<p className="leading-relaxed whitespace-pre-wrap">{replyingReview.comment}</p>
|
||||||
@ -483,10 +466,28 @@ export default function Reviews() {
|
|||||||
className="w-full p-3 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500 resize-none leading-relaxed"
|
className="w-full p-3 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500 resize-none leading-relaxed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setReplyingReview(null)}
|
||||||
|
className="px-4 py-2.5 text-xs font-bold text-gray-600 hover:bg-gray-100 rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isSavingReply}
|
||||||
|
onClick={handleSaveReply}
|
||||||
|
className="px-5 py-2.5 text-xs font-bold bg-purple-600 hover:bg-purple-700 text-white rounded-xl shadow-lg shadow-purple-600/20 transition-all flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
{isSavingReply && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
||||||
|
<span>ثبت و انتشار پاسخ</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,8 @@ import { toast } from 'react-hot-toast';
|
|||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import type { SeoSettings } from '../types/admin';
|
import type { SeoSettings } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
export default function SeoSettingsPage() {
|
export default function SeoSettingsPage() {
|
||||||
const [seo, setSeo] = useState<SeoSettings>({
|
const [seo, setSeo] = useState<SeoSettings>({
|
||||||
brandNameFa: 'کنینا ایران',
|
brandNameFa: 'کنینا ایران',
|
||||||
@ -273,34 +271,31 @@ export default function SeoSettingsPage() {
|
|||||||
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsMediaSelectorOpen(true)}
|
onClick={() => setIsMediaSelectorOpen(true)}
|
||||||
|
className="px-3 py-3 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
انتخاب از رسانه
|
انتخاب از رسانه
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end pt-4 border-t border-gray-100">
|
<div className="flex justify-end pt-4 border-t border-gray-100">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isSaving}
|
||||||
size="md"
|
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
|
||||||
startIcon={Save}
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
>
|
||||||
|
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
|
||||||
ذخیره تنظیمات سئو و ساختار عناوین
|
ذخیره تنظیمات سئو و ساختار عناوین
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{isMediaSelectorOpen && (
|
{isMediaSelectorOpen && (
|
||||||
<MediaSelector
|
<MediaSelector
|
||||||
isOpen={isMediaSelectorOpen}
|
isOpen={isMediaSelectorOpen}
|
||||||
|
|||||||
@ -20,9 +20,6 @@ import {
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const [activeTab, setActiveTab] = useState<'brand' | 'contact' | 'links'>('brand');
|
const [activeTab, setActiveTab] = useState<'brand' | 'contact' | 'links'>('brand');
|
||||||
@ -361,15 +358,14 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isSaving}
|
||||||
size="md"
|
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
|
||||||
startIcon={Save}
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
>
|
||||||
ذخیره هویت برند
|
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
|
||||||
</Button>
|
<span>ذخیره هویت برند</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : activeTab === 'contact' ? (
|
) : activeTab === 'contact' ? (
|
||||||
@ -472,18 +468,16 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isSaving}
|
||||||
size="md"
|
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
|
||||||
startIcon={Save}
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
>
|
||||||
ذخیره اطلاعات تماس
|
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
|
||||||
</Button>
|
<span>ذخیره اطلاعات تماس</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
/* Tab 3: Dedicated Modules Grid */
|
/* Tab 3: Dedicated Modules Grid */
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||||
|
|||||||
@ -4,9 +4,6 @@ import { toast } from 'react-hot-toast';
|
|||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import PriceInput from '../components/ui/PriceInput';
|
import PriceInput from '../components/ui/PriceInput';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function ShippingSettingsPage() {
|
export default function ShippingSettingsPage() {
|
||||||
const [shipping, setShipping] = useState({
|
const [shipping, setShipping] = useState({
|
||||||
@ -151,19 +148,17 @@ export default function ShippingSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isSaving}
|
||||||
size="md"
|
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-blue-200 cursor-pointer"
|
||||||
startIcon={Save}
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
>
|
||||||
ذخیره تغییرات ارسال
|
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
|
||||||
</Button>
|
<span>ذخیره تغییرات ارسال</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,11 +4,8 @@ import { toast } from 'react-hot-toast';
|
|||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
import type { SmartAdvisorRule, Product } from '../types/admin';
|
import type { SmartAdvisorRule, Product } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
export default function SmartAdvisorManager() {
|
export default function SmartAdvisorManager() {
|
||||||
const [rules, setRules] = useState<SmartAdvisorRule[]>([]);
|
const [rules, setRules] = useState<SmartAdvisorRule[]>([]);
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
@ -150,14 +147,13 @@ export default function SmartAdvisorManager() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">تعریف قوانین تجویز محصولات پیشنهادی بر اساس عوارض و علائم پت</p>
|
<p className="text-gray-500 font-medium mt-1">تعریف قوانین تجویز محصولات پیشنهادی بر اساس عوارض و علائم پت</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
تعریف قانون جدید
|
تعریف قانون جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
@ -202,20 +198,12 @@ export default function SmartAdvisorManager() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<button onClick={() => openModal(rule)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||||
variant="ghost"
|
<Edit2 className="w-4 h-4" />
|
||||||
size="xs"
|
</button>
|
||||||
startIcon={Edit2}
|
<button onClick={() => setDeleteTargetId(rule.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||||
onClick={() => openModal(rule)}
|
<Trash2 className="w-4 h-4" />
|
||||||
className="text-blue-600 hover:bg-blue-50"
|
</button>
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="xs"
|
|
||||||
startIcon={Trash2}
|
|
||||||
onClick={() => setDeleteTargetId(rule.id)}
|
|
||||||
className="text-red-600 hover:bg-red-50"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -228,69 +216,72 @@ export default function SmartAdvisorManager() {
|
|||||||
|
|
||||||
{/* Form Modal */}
|
{/* Form Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={editingRule ? 'ویرایش قانون مشاور هوشمند' : 'تعریف قانون جدید'}
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
icon={Sparkles}
|
{editingRule ? 'ویرایش قانون مشاور هوشمند' : 'تعریف قانون جدید'}
|
||||||
maxWidth="lg"
|
</h3>
|
||||||
>
|
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||||
<form onSubmit={handleSave} className="space-y-4 font-vazir">
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSave} className="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">نام عارضه یا علامت بیماری (condition) *</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">شرط / علائم عارضه (condition) *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.condition}
|
value={formData.condition}
|
||||||
onChange={(e) => setFormData({ ...formData, condition: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, condition: 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">گونه پت هدف (targetPetType)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">گونه حیوان هدف (targetPetType)</label>
|
||||||
<select
|
<select
|
||||||
value={formData.targetPetType}
|
value={formData.targetPetType}
|
||||||
onChange={(e) => setFormData({ ...formData, targetPetType: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, targetPetType: e.target.value })}
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-bold bg-white"
|
||||||
>
|
>
|
||||||
<option value="سگ">سگ (Dog)</option>
|
<option value="سگ">سگ</option>
|
||||||
<option value="گربه">گربه (Cat)</option>
|
<option value="گربه">گربه</option>
|
||||||
<option value="هر دو">هر دو (سگ و گربه)</option>
|
<option value="هر دو">هر دو (سگ و گربه)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">انتخاب محصول پیشنهادی کنینا (recommendedProduct) *</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="relative">
|
<div className="relative">
|
||||||
|
<Search className="w-4 h-4 absolute right-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
placeholder="جستجوی سریع محصول..."
|
||||||
value={productSearch}
|
value={productSearch}
|
||||||
onChange={(e) => setProductSearch(e.target.value)}
|
onChange={(e) => setProductSearch(e.target.value)}
|
||||||
placeholder="جستجو در نام محصولات..."
|
className="w-full pl-3 pr-9 py-2 rounded-lg border border-gray-200 text-xs outline-none"
|
||||||
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>
|
</div>
|
||||||
<select
|
<select
|
||||||
required
|
required
|
||||||
value={formData.recommendedProduct}
|
value={formData.recommendedProduct}
|
||||||
onChange={(e) => setFormData({ ...formData, recommendedProduct: e.target.value })}
|
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"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-bold bg-white"
|
||||||
>
|
>
|
||||||
<option value="">-- یک محصول را انتخاب کنید --</option>
|
{filteredProducts.map(p => (
|
||||||
{filteredProducts.map((p) => (
|
|
||||||
<option key={p.id} value={p.id}>
|
<option key={p.id} value={p.id}>
|
||||||
{p.nameFa} ({p.nameEn || p.id})
|
{p.nameFa || p.name} ({p.artNo})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">علت و توضیح علمی پیشنهاد (reason) *</label>
|
||||||
<textarea
|
<textarea
|
||||||
required
|
required
|
||||||
@ -303,25 +294,25 @@ export default function SmartAdvisorManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
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>
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isSaving}
|
||||||
size="md"
|
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"
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
>
|
||||||
|
{isSaving && <Spinner size="sm" />}
|
||||||
ذخیره قانون
|
ذخیره قانون
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
@ -337,4 +328,3 @@ export default function SmartAdvisorManager() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,9 +39,6 @@ import {
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface SmsConfigState {
|
interface SmsConfigState {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -770,19 +767,17 @@ export default function SmsSettingsPage() {
|
|||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className="flex justify-end pt-2">
|
<div className="flex justify-end pt-2">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isSaving}
|
||||||
size="md"
|
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3.5 px-8 rounded-xl transition-all flex items-center gap-2 shadow-lg shadow-purple-200 disabled:opacity-50"
|
||||||
startIcon={Save}
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
>
|
||||||
|
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
|
||||||
ذخیره تنظیمات درگاه پیامک
|
ذخیره تنظیمات درگاه پیامک
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
{/* Balance & Test SMS Sidebar (1 col on large) */}
|
{/* Balance & Test SMS Sidebar (1 col on large) */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Realtime Credit / Balance Card */}
|
{/* Realtime Credit / Balance Card */}
|
||||||
|
|||||||
@ -19,9 +19,6 @@ import {
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface SslStatus {
|
interface SslStatus {
|
||||||
exists: boolean;
|
exists: boolean;
|
||||||
@ -437,16 +434,14 @@ export default function SslSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-2">
|
<div className="flex justify-end gap-3 pt-2">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isUpdating || !certFile || !keyFile}
|
||||||
size="sm"
|
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-xl text-xs font-bold transition-all shadow-md shadow-purple-200 flex items-center gap-2 cursor-pointer"
|
||||||
startIcon={ShieldCheck}
|
|
||||||
disabled={!certFile || !keyFile}
|
|
||||||
isLoading={isUpdating}
|
|
||||||
>
|
>
|
||||||
نصب و فعالسازی گواهی جدید
|
{isUpdating ? <Spinner size="sm" /> : <ShieldCheck className="w-4 h-4" />}
|
||||||
</Button>
|
<span>نصب و فعالسازی گواهی جدید</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
@ -484,16 +479,14 @@ export default function SslSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-2">
|
<div className="flex justify-end gap-3 pt-2">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isUpdating || !certText.trim() || !keyText.trim()}
|
||||||
size="sm"
|
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-xl text-xs font-bold transition-all shadow-md shadow-purple-200 flex items-center gap-2 cursor-pointer"
|
||||||
startIcon={ShieldCheck}
|
|
||||||
disabled={!certText.trim() || !keyText.trim()}
|
|
||||||
isLoading={isUpdating}
|
|
||||||
>
|
>
|
||||||
ذخیره و اعمال تغییرات
|
{isUpdating ? <Spinner size="sm" /> : <ShieldCheck className="w-4 h-4" />}
|
||||||
</Button>
|
<span>ذخیره و اعمال تغییرات</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
@ -516,20 +509,18 @@ export default function SslSettingsPage() {
|
|||||||
className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono outline-none focus:border-blue-500"
|
className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono outline-none focus:border-blue-500"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
startIcon={ExternalLink}
|
|
||||||
onClick={handleTestOnline}
|
onClick={handleTestOnline}
|
||||||
isLoading={isTestingOnline}
|
disabled={isTestingOnline}
|
||||||
|
className="px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-bold rounded-xl transition-all shadow-xs flex items-center justify-center gap-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
تست زنده پورت ۴۴۳ دامنه
|
{isTestingOnline ? <Spinner size="sm" /> : <ExternalLink className="w-4 h-4" />}
|
||||||
</Button>
|
<span>تست زنده پورت ۴۴۳ دامنه</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{onlineResult && (
|
{onlineResult && (
|
||||||
|
|
||||||
<div className="mt-3 p-4 bg-gray-50 rounded-xl border border-gray-200 text-xs space-y-1.5 animate-in fade-in duration-150">
|
<div className="mt-3 p-4 bg-gray-50 rounded-xl border border-gray-200 text-xs space-y-1.5 animate-in fade-in duration-150">
|
||||||
{onlineResult.onlineCheck ? (
|
{onlineResult.onlineCheck ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|||||||
@ -14,11 +14,8 @@ import {
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import type { SystemSettings } from '../types/admin';
|
import type { SystemSettings } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function SystemSettingsPage() {
|
export default function SystemSettingsPage() {
|
||||||
const [system, setSystem] = useState<SystemSettings>({
|
const [system, setSystem] = useState<SystemSettings>({
|
||||||
maintenanceMode: false,
|
maintenanceMode: false,
|
||||||
@ -327,19 +324,17 @@ export default function SystemSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
disabled={isSaving}
|
||||||
size="md"
|
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
|
||||||
startIcon={Save}
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
>
|
||||||
ذخیره تنظیمات سیستمی
|
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
|
||||||
</Button>
|
<span>ذخیره تنظیمات سیستمی</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,8 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
import type { Testimonial } from '../types/admin';
|
import type { Testimonial } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
export default function TestimonialsManager() {
|
export default function TestimonialsManager() {
|
||||||
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
|
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@ -151,14 +148,13 @@ export default function TestimonialsManager() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مدیریت توصیه نامهها و نظرات تایید شده جهت نمایش در صفحه اصلی و گالری دامپزشکان</p>
|
<p className="text-gray-500 font-medium mt-1">مدیریت توصیه نامهها و نظرات تایید شده جهت نمایش در صفحه اصلی و گالری دامپزشکان</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
افزودن نظر جدید
|
افزودن نظر جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
@ -203,51 +199,45 @@ export default function TestimonialsManager() {
|
|||||||
{item.roleTitle && <span className="text-xs text-purple-600 font-medium">{item.roleTitle}</span>}
|
{item.roleTitle && <span className="text-xs text-purple-600 font-medium">{item.roleTitle}</span>}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6 text-gray-600 text-sm max-w-md line-clamp-2">
|
||||||
<p className="text-gray-600 text-sm line-clamp-2 leading-relaxed max-w-md">{item.content}</p>
|
{item.content}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-1 text-amber-500">
|
<div className="flex items-center gap-1 text-amber-500">
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: item.rating || 5 }).map((_, i) => (
|
||||||
<Star
|
<Star key={i} className="w-4 h-4 fill-amber-400" />
|
||||||
key={i}
|
|
||||||
className={`w-3.5 h-3.5 ${i < item.rating ? 'fill-amber-400 text-amber-400' : 'text-gray-200'}`}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
{item.isFeatured && (
|
||||||
onClick={() => handleToggleActive(item)}
|
<span className="px-2 py-0.5 bg-amber-50 text-amber-700 text-[11px] font-bold rounded border border-amber-200">
|
||||||
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold transition-colors cursor-pointer ${
|
ویژه (Featured)
|
||||||
item.isActive
|
</span>
|
||||||
? 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
)}
|
||||||
: 'bg-rose-50 text-rose-700 hover:bg-rose-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.isActive ? <CheckCircle2 className="w-3 h-3" /> : <XCircle className="w-3 h-3" />}
|
|
||||||
{item.isActive ? 'فعال' : 'غیرفعال'}
|
|
||||||
</button>
|
|
||||||
{(item as any).showOnHome && (
|
{(item as any).showOnHome && (
|
||||||
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs font-bold rounded-md">
|
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-[11px] font-bold rounded border border-blue-200">
|
||||||
خانه
|
🏠 صفحه اصلی
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.isActive ? (
|
||||||
|
<span className="inline-flex items-center gap-0.5 text-green-600 text-xs font-bold">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5" /> فعال
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-0.5 text-gray-400 text-xs font-bold">
|
||||||
|
<XCircle className="w-3.5 h-3.5" /> غیرفعال
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button onClick={() => openModal(item)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||||
onClick={() => openModal(item)}
|
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => setDeleteTargetId(item.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||||
onClick={() => setDeleteTargetId(item.id)}
|
|
||||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -260,117 +250,88 @@ export default function TestimonialsManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Form Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'}
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
icon={MessageSquareQuote}
|
{editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'}
|
||||||
maxWidth="2xl"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<X className="w-6 h-6" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="testimonialForm"
|
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
|
||||||
ذخیره تغییرات
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<form onSubmit={handleSave} className="p-6 space-y-4">
|
||||||
<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>
|
<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
|
required
|
||||||
|
type="text"
|
||||||
value={formData.authorName}
|
value={formData.authorName}
|
||||||
onChange={(e) => setFormData({ ...formData, authorName: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, authorName: 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">سمت / عنوان تخصص (roleTitle)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.roleTitle}
|
value={formData.roleTitle}
|
||||||
onChange={(e) => setFormData({ ...formData, roleTitle: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, roleTitle: 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>
|
<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">تصویر آواتار (avatarUrl)</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.avatarUrl}
|
value={formData.avatarUrl}
|
||||||
onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })}
|
||||||
placeholder="آدرس تصویر یا انتخاب از رسانهها..."
|
placeholder="https://... یا /uploads/..."
|
||||||
|
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsMediaSelectorOpen(true)}
|
onClick={() => setIsMediaSelectorOpen(true)}
|
||||||
|
className="px-3 py-2.5 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0"
|
||||||
>
|
>
|
||||||
انتخاب از رسانه
|
رسانه
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">متن نظر / توصیه نامه *</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">متن نظر / رضایتنامه *</label>
|
||||||
<textarea
|
<textarea
|
||||||
required
|
required
|
||||||
rows={4}
|
rows={4}
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||||
placeholder="متن کامل نظر و تجربه همکاری با محصولات کنینا..."
|
placeholder="متن نظر یا توصیه بالینی را بنویسید..."
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm leading-relaxed"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-center">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">امتیاز (از ۵ ستاره)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">امتیاز (۱ تا ۵)</label>
|
||||||
<div className="flex items-center gap-2">
|
<select
|
||||||
{[1, 2, 3, 4, 5].map((star) => (
|
value={formData.rating}
|
||||||
<button
|
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
|
||||||
type="button"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-bold bg-white"
|
||||||
key={star}
|
|
||||||
onClick={() => setFormData({ ...formData, rating: star })}
|
|
||||||
className="p-1 cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<Star
|
{[5, 4, 3, 2, 1].map(r => (
|
||||||
className={`w-6 h-6 ${
|
<option key={r} value={r}>{r} ستاره</option>
|
||||||
star <= formData.rating ? 'fill-amber-400 text-amber-400' : 'text-gray-300'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
<span className="text-sm font-bold text-gray-500 mr-2">({formData.rating} از ۵)</span>
|
</select>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -384,8 +345,18 @@ export default function TestimonialsManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
<div className="pt-2 grid grid-cols-3 gap-3">
|
||||||
<label className="flex items-center justify-between p-3 bg-purple-50 rounded-xl border border-purple-200 cursor-pointer">
|
<label className="flex items-center justify-between p-3 bg-gray-50 rounded-xl border border-gray-200 cursor-pointer">
|
||||||
|
<span className="text-xs font-bold text-gray-700">نمایش ویژه (Featured)</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.isFeatured}
|
||||||
|
onChange={(e) => setFormData({ ...formData, isFeatured: e.target.checked })}
|
||||||
|
className="rounded text-purple-600 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center justify-between p-3 bg-gray-50 rounded-xl border border-gray-200 cursor-pointer">
|
||||||
<span className="text-xs font-bold text-gray-700">وضعیت انتشار (Active)</span>
|
<span className="text-xs font-bold text-gray-700">وضعیت انتشار (Active)</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -396,7 +367,7 @@ export default function TestimonialsManager() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="flex items-center justify-between p-3 bg-blue-50 rounded-xl border border-blue-200 cursor-pointer">
|
<label className="flex items-center justify-between p-3 bg-blue-50 rounded-xl border border-blue-200 cursor-pointer">
|
||||||
<span className="text-xs font-bold text-blue-700">نمایش در خانه</span>
|
<span className="text-xs font-bold text-blue-700">🏠 صفحه اصلی</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.showOnHome}
|
checked={formData.showOnHome}
|
||||||
@ -405,12 +376,29 @@ export default function TestimonialsManager() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isSaving && <Spinner size="sm" />}
|
||||||
|
ذخیره تغییرات
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
{deleteTargetId && (
|
{deleteTargetId && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
|||||||
@ -20,8 +20,6 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
interface TicketMessage {
|
interface TicketMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -342,63 +340,47 @@ export default function Tickets() {
|
|||||||
|
|
||||||
{/* Ticket Details & Admin Reply Modal */}
|
{/* Ticket Details & Admin Reply Modal */}
|
||||||
{selectedTicket && (
|
{selectedTicket && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||||
isOpen={!!selectedTicket}
|
<div className="bg-white w-full max-w-3xl rounded-3xl p-6 sm:p-8 shadow-2xl space-y-6 max-h-[90vh] flex flex-col" dir="rtl">
|
||||||
onClose={() => setSelectedTicket(null)}
|
{/* Modal Header */}
|
||||||
title={`تیکت ${selectedTicket.ticketNumber}: ${selectedTicket.subject}`}
|
<div className="flex items-start justify-between pb-4 border-b border-gray-100">
|
||||||
icon={MessageSquare}
|
<div>
|
||||||
maxWidth="3xl"
|
<div className="flex items-center gap-2">
|
||||||
footer={
|
<h3 className="text-lg font-black text-gray-900">{selectedTicket.subject}</h3>
|
||||||
<div className="flex items-center justify-end gap-2.5 w-full">
|
<span className="px-2.5 py-0.5 bg-canina-blue/10 text-canina-blue font-mono font-bold text-xs rounded-lg">
|
||||||
<Button
|
{selectedTicket.ticketNumber}
|
||||||
variant="secondary"
|
</span>
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedTicket(null)}
|
|
||||||
>
|
|
||||||
بستن
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="ticketReplyForm"
|
|
||||||
startIcon={Send}
|
|
||||||
isLoading={sendingReply}
|
|
||||||
disabled={!replyMessage.trim()}
|
|
||||||
>
|
|
||||||
ارسال پاسخ به کاربر
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
<div className="flex items-center gap-3 text-xs text-gray-500 font-medium mt-1">
|
||||||
>
|
|
||||||
<div className="space-y-5 text-right font-vazir">
|
|
||||||
{/* Header meta */}
|
|
||||||
<div className="p-3 bg-gray-50 rounded-2xl border border-gray-100 flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<div className="text-xs text-gray-600 font-medium">
|
|
||||||
<span>کاربر: {selectedTicket.user?.firstName} {selectedTicket.user?.lastName} ({selectedTicket.user?.mobile})</span>
|
<span>کاربر: {selectedTicket.user?.firstName} {selectedTicket.user?.lastName} ({selectedTicket.user?.mobile})</span>
|
||||||
{selectedTicket.pet && (
|
{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>
|
<span className="text-purple-600 font-bold">• 🐾 {selectedTicket.pet.name} ({selectedTicket.pet.breed || selectedTicket.pet.type})</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-bold text-gray-500">تغییر وضعیت:</span>
|
|
||||||
<select
|
<select
|
||||||
value={selectedTicket.status}
|
value={selectedTicket.status}
|
||||||
onChange={(e) => handleUpdateStatus(e.target.value)}
|
onChange={(e) => handleUpdateStatus(e.target.value)}
|
||||||
className="px-2.5 py-1.5 border border-gray-200 rounded-xl text-xs font-bold bg-white focus:ring-2 focus:ring-purple-500 outline-none"
|
className="px-2.5 py-1.5 border border-gray-200 rounded-xl text-xs font-bold bg-gray-50 focus:border-canina-blue outline-none"
|
||||||
>
|
>
|
||||||
<option value="OPEN">در انتظار پاسخ</option>
|
<option value="OPEN">در انتظار پاسخ</option>
|
||||||
<option value="IN_PROGRESS">در حال بررسی</option>
|
<option value="IN_PROGRESS">در حال بررسی</option>
|
||||||
<option value="ANSWERED">پاسخ داده شد</option>
|
<option value="ANSWERED">پاسخ داده شد</option>
|
||||||
<option value="CLOSED">بسته شده</option>
|
<option value="CLOSED">بسته شده</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedTicket(null)}
|
||||||
|
className="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-600 flex items-center justify-center font-bold text-sm cursor-pointer"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages Chat Scroll */}
|
{/* Messages Chat Scroll */}
|
||||||
<div className="overflow-y-auto space-y-3.5 p-1 max-h-[340px]">
|
<div className="flex-1 overflow-y-auto space-y-4 p-2 min-h-[250px]">
|
||||||
{(selectedTicket.messages || []).map((msg) => {
|
{(selectedTicket.messages || []).map((msg) => {
|
||||||
const isAdminOrVet = msg.senderRole === 'ADMIN' || msg.senderRole === 'VET_DOCTOR';
|
const isAdminOrVet = msg.senderRole === 'ADMIN' || msg.senderRole === 'VET_DOCTOR';
|
||||||
return (
|
return (
|
||||||
@ -406,7 +388,7 @@ export default function Tickets() {
|
|||||||
key={msg.id}
|
key={msg.id}
|
||||||
className={`p-4 rounded-2xl max-w-[85%] text-xs font-medium leading-relaxed ${
|
className={`p-4 rounded-2xl max-w-[85%] text-xs font-medium leading-relaxed ${
|
||||||
isAdminOrVet
|
isAdminOrVet
|
||||||
? 'bg-purple-50 border border-purple-200 text-purple-950 mr-auto'
|
? 'bg-emerald-50 border border-emerald-200 text-emerald-950 mr-auto'
|
||||||
: 'bg-blue-50 border border-blue-200 text-blue-950 ml-auto'
|
: 'bg-blue-50 border border-blue-200 text-blue-950 ml-auto'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -414,8 +396,8 @@ export default function Tickets() {
|
|||||||
<span className="font-black text-[11px] flex items-center gap-1.5">
|
<span className="font-black text-[11px] flex items-center gap-1.5">
|
||||||
{isAdminOrVet ? (
|
{isAdminOrVet ? (
|
||||||
<>
|
<>
|
||||||
<Stethoscope className="w-3.5 h-3.5 text-purple-600" />
|
<Stethoscope className="w-3.5 h-3.5 text-emerald-600" />
|
||||||
<span className="text-purple-700">{msg.senderName} (تیم پزشکی / ادمین)</span>
|
<span className="text-emerald-700">{msg.senderName} (تیم پزشکی / ادمین)</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -435,7 +417,7 @@ export default function Tickets() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Admin Response Composer */}
|
{/* Admin Response Composer */}
|
||||||
<form id="ticketReplyForm" onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3">
|
<form onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3">
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<label className="font-bold text-gray-700">ارسال پاسخ دامپزشک / پشتیبانی:</label>
|
<label className="font-bold text-gray-700">ارسال پاسخ دامپزشک / پشتیبانی:</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -457,13 +439,29 @@ export default function Tickets() {
|
|||||||
placeholder="متن پاسخ تخصصی به کاربر را وارد کنید..."
|
placeholder="متن پاسخ تخصصی به کاربر را وارد کنید..."
|
||||||
value={replyMessage}
|
value={replyMessage}
|
||||||
onChange={(e) => setReplyMessage(e.target.value)}
|
onChange={(e) => setReplyMessage(e.target.value)}
|
||||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-medium focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none resize-none leading-relaxed"
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-medium focus:border-canina-blue outline-none"
|
||||||
/>
|
/>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedTicket(null)}
|
||||||
|
className="px-4 py-2 rounded-xl text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
بستن
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={sendingReply || !replyMessage.trim()}
|
||||||
|
className="px-6 py-2 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-dark disabled:opacity-50 transition-all flex items-center gap-2 cursor-pointer shadow-md shadow-canina-blue/20"
|
||||||
|
>
|
||||||
|
<Send className="w-3.5 h-3.5" />
|
||||||
|
<span>{sendingReply ? 'در حال ارسال...' : 'ارسال پاسخ به کاربر'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,8 +47,6 @@ import ImagePreviewModal from '../components/ui/ImagePreviewModal';
|
|||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import RichTextEditor from '../components/ui/RichTextEditor';
|
import RichTextEditor from '../components/ui/RichTextEditor';
|
||||||
import IconPickerModal, { ICON_REGISTRY } from '../components/ui/IconPickerModal';
|
import IconPickerModal, { ICON_REGISTRY } from '../components/ui/IconPickerModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
|
|
||||||
|
|
||||||
// Classic v1 Groupings (Backward Compatibility)
|
// Classic v1 Groupings (Backward Compatibility)
|
||||||
const V1_PAGE_TABS = [
|
const V1_PAGE_TABS = [
|
||||||
@ -921,16 +919,15 @@ export default function UITexts() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end pt-1">
|
<div className="flex justify-end pt-1">
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
|
||||||
size="xs"
|
|
||||||
startIcon={Save}
|
|
||||||
onClick={() => handleSave(field.key)}
|
onClick={() => handleSave(field.key)}
|
||||||
isLoading={isSaving}
|
disabled={isSaving}
|
||||||
|
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-black transition-all flex items-center gap-1.5 shadow-sm cursor-pointer"
|
||||||
>
|
>
|
||||||
ذخیره این فیلد
|
{isSaving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
|
||||||
</Button>
|
<span>ذخیره این فیلد</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -951,21 +948,22 @@ export default function UITexts() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Save}
|
|
||||||
onClick={handleSaveAll}
|
onClick={handleSaveAll}
|
||||||
disabled={!hasUnsavedChanges}
|
disabled={isSavingAll || !hasUnsavedChanges}
|
||||||
isLoading={isSavingAll}
|
className={`px-5 py-2.5 rounded-xl text-xs font-black flex items-center gap-2 transition-all shadow-sm ${
|
||||||
|
hasUnsavedChanges
|
||||||
|
? 'bg-purple-600 hover:bg-purple-700 text-white shadow-purple-200 cursor-pointer animate-pulse'
|
||||||
|
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
ذخیره تمامی تغییرات {hasUnsavedChanges ? `(${Object.keys(edits).length})` : ''}
|
{isSavingAll ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||||
</Button>
|
<span>ذخیره تمامی تغییرات {hasUnsavedChanges ? `(${Object.keys(edits).length})` : ''}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Main Version Switcher: v2 (New Structure) vs v1 (Legacy) */}
|
{/* Main Version Switcher: v2 (New Structure) vs v1 (Legacy) */}
|
||||||
<div className="flex items-center justify-between bg-white p-2 rounded-2xl border border-gray-200 shadow-xs">
|
<div className="flex items-center justify-between bg-white p-2 rounded-2xl border border-gray-200 shadow-xs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@ -265,15 +265,13 @@ export default function Users() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto">
|
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto">
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="w-full sm:w-auto flex items-center justify-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2.5 rounded-xl font-bold text-xs shadow-md shadow-purple-200 transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
افزودن کاربر جدید
|
افزودن کاربر جدید
|
||||||
</Button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
<select
|
<select
|
||||||
className="bg-white border border-gray-200 text-gray-700 px-4 py-2.5 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto text-xs"
|
className="bg-white border border-gray-200 text-gray-700 px-4 py-2.5 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto text-xs"
|
||||||
|
|||||||
@ -4,9 +4,6 @@ import { toast } from 'react-hot-toast';
|
|||||||
import api, { BASE_DOMAIN } from '../services/api';
|
import api, { BASE_DOMAIN } from '../services/api';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
interface Video {
|
interface Video {
|
||||||
id: string;
|
id: string;
|
||||||
@ -182,17 +179,15 @@ export default function Videos() {
|
|||||||
آپلود و انتخاب مستقیم ویدئو/کاور از گالری رسانه یا درج کد آیفرم آپارات/یوتیوب برای مشاوره ویدئویی دامپزشکان کنینا
|
آپلود و انتخاب مستقیم ویدئو/کاور از گالری رسانه یا درج کد آیفرم آپارات/یوتیوب برای مشاوره ویدئویی دامپزشکان کنینا
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => handleOpenModal()}
|
onClick={() => handleOpenModal()}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-3 rounded-xl font-black text-sm flex items-center gap-2 shadow-lg shadow-purple-600/20 transition-all hover:scale-105 shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
افزودن ویدئوی جدید
|
<Plus className="w-5 h-5" />
|
||||||
</Button>
|
<span>افزودن ویدئوی جدید</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-xs flex items-center gap-4">
|
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-xs flex items-center gap-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="w-5 h-5 text-gray-400 absolute right-4 top-1/2 -translate-y-1/2" />
|
<Search className="w-5 h-5 text-gray-400 absolute right-4 top-1/2 -translate-y-1/2" />
|
||||||
@ -315,35 +310,22 @@ export default function Videos() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-3xl max-w-2xl w-full p-6 sm:p-8 shadow-2xl border border-gray-100 max-h-[90vh] overflow-y-auto">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="flex items-center justify-between pb-4 border-b border-gray-100 mb-6">
|
||||||
title={editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'}
|
<h3 className="text-lg font-black text-gray-900 flex items-center gap-2">
|
||||||
icon={VideoIcon}
|
<VideoIcon className="w-5 h-5 text-purple-600" />
|
||||||
maxWidth="2xl"
|
{editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'}
|
||||||
footer={
|
</h3>
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<button
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
انصراف
|
<X className="w-5 h-5" />
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="videoForm"
|
|
||||||
>
|
|
||||||
ذخیره اطلاعات
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<form onSubmit={handleSave} className="space-y-5">
|
||||||
<div className="space-y-5 text-right font-vazir">
|
|
||||||
<form id="videoForm" onSubmit={handleSave} className="space-y-5">
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-xs font-black text-gray-700 block">عنوان ویدئو *</label>
|
<label className="text-xs font-black text-gray-700 block">عنوان ویدئو *</label>
|
||||||
<input
|
<input
|
||||||
@ -422,15 +404,14 @@ export default function Videos() {
|
|||||||
placeholder="آدرس تصویر یا انتخاب از گالری..."
|
placeholder="آدرس تصویر یا انتخاب از گالری..."
|
||||||
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-purple-500/20"
|
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-purple-500/20"
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
startIcon={ImageIcon}
|
|
||||||
onClick={() => setIsThumbnailMediaOpen(true)}
|
onClick={() => setIsThumbnailMediaOpen(true)}
|
||||||
|
className="px-4 py-3 bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-xl text-xs font-black flex items-center gap-1.5 border border-purple-200 shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
انتخاب کاور
|
<ImageIcon className="w-4 h-4" />
|
||||||
</Button>
|
<span>انتخاب از گالری</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -456,15 +437,14 @@ export default function Videos() {
|
|||||||
placeholder="https://... یا کد آیفرم آپارات/یوتیوب"
|
placeholder="https://... یا کد آیفرم آپارات/یوتیوب"
|
||||||
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-purple-500/20"
|
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-purple-500/20"
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Film}
|
|
||||||
onClick={() => setIsVideoMediaOpen(true)}
|
onClick={() => setIsVideoMediaOpen(true)}
|
||||||
|
className="px-4 py-3 bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-xl text-xs font-black flex items-center gap-1.5 border border-purple-200 shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
انتخاب ویدئو
|
<Film className="w-4 h-4" />
|
||||||
</Button>
|
<span>انتخاب ویدئو</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -491,12 +471,27 @@ export default function Videos() {
|
|||||||
نمایش به عنوان ویدئوی ویژه در بخش «مشاوره ویدئویی» صفحه اصلی سایت
|
نمایش به عنوان ویدئوی ویژه در بخش «مشاوره ویدئویی» صفحه اصلی سایت
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold text-sm hover:bg-gray-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold text-sm hover:bg-purple-700 shadow-md cursor-pointer"
|
||||||
|
>
|
||||||
|
ذخیره اطلاعات
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Cover Thumbnail Media Selector Modal */}
|
{/* Cover Thumbnail Media Selector Modal */}
|
||||||
<MediaSelector
|
<MediaSelector
|
||||||
isOpen={isThumbnailMediaOpen}
|
isOpen={isThumbnailMediaOpen}
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import { useState, useEffect } from 'react';
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
|
|
||||||
|
|
||||||
export interface WholesaleRequest {
|
export interface WholesaleRequest {
|
||||||
id: string;
|
id: string;
|
||||||
@ -61,10 +59,10 @@ export default function WholesaleApplications() {
|
|||||||
if (!rejectTargetId) return;
|
if (!rejectTargetId) return;
|
||||||
try {
|
try {
|
||||||
await api.put(`/wholesale/reject/${rejectTargetId}`);
|
await api.put(`/wholesale/reject/${rejectTargetId}`);
|
||||||
toast.success('درخواست همکاری رد شد.');
|
toast.success('درخواست رد شد.');
|
||||||
fetchRequestsData();
|
fetchRequestsData();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('خطا در رد درخواست همکاری');
|
toast.error('خطا در رد درخواست');
|
||||||
} finally {
|
} finally {
|
||||||
setRejectTargetId(null);
|
setRejectTargetId(null);
|
||||||
}
|
}
|
||||||
@ -72,24 +70,20 @@ export default function WholesaleApplications() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 font-vazir" dir="rtl">
|
<div className="p-6 font-vazir" dir="rtl">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<h1 className="text-2xl font-bold mb-2 text-gray-800">بررسی درخواستهای ثبتنام همکاران B2B (کلینیکها و داروخانهها)</h1>
|
||||||
<div>
|
<p className="text-sm text-gray-500 mb-6">استعلام شماره پروانه نظام دامپزشکی و اعطای دسترسی خرید عمده با تخفیف ۳۰٪ کاتالوگ Canina</p>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">درخواستهای ارتقا به همکار B2B</h1>
|
|
||||||
<p className="text-gray-500 text-sm mt-1">تایید یا رد درخواستهای کاربران عادی برای خرید عمده و دسترسی به تخفیفهای تجاری</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-12 text-gray-500 font-bold">در حال بارگذاری درخواستها...</div>
|
<div className="text-center py-12 text-gray-500 font-bold">در حال بارگذاری درخواستها...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
<div className="bg-white rounded-2xl border border-gray-200 overflow-hidden shadow-sm">
|
||||||
<table className="w-full text-right border-collapse">
|
<table className="w-full text-right">
|
||||||
<thead>
|
<thead className="bg-gray-50 border-b border-gray-200 text-xs font-bold text-gray-500">
|
||||||
<tr className="bg-gray-50/50 border-b border-gray-100 text-xs text-gray-500">
|
<tr>
|
||||||
<th className="px-6 py-4 font-bold">نام و اطلاعات متقاضی</th>
|
<th className="px-6 py-4">کاربر / ایمیل</th>
|
||||||
<th className="px-6 py-4 font-bold">وضعیت فعلی</th>
|
<th className="px-6 py-4">نقش فعلی</th>
|
||||||
<th className="px-6 py-4 font-bold">تاریخ درخواست</th>
|
<th className="px-6 py-4">تاریخ ثبتنام</th>
|
||||||
<th className="px-6 py-4 font-bold text-center">عملیات</th>
|
<th className="px-6 py-4 text-center">عملیات تایید همکار</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100 text-sm">
|
<tbody className="divide-y divide-gray-100 text-sm">
|
||||||
@ -113,26 +107,21 @@ export default function WholesaleApplications() {
|
|||||||
<td className="px-6 py-4 text-xs text-gray-500 font-mono">
|
<td className="px-6 py-4 text-xs text-gray-500 font-mono">
|
||||||
{new Date(u.createdAt).toLocaleDateString('fa-IR')}
|
{new Date(u.createdAt).toLocaleDateString('fa-IR')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-center">
|
<td className="px-6 py-4 text-center space-x-2 space-x-reverse">
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
{u.role !== 'User_Wholesale' && (
|
{u.role !== 'User_Wholesale' && (
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="xs"
|
|
||||||
className="!bg-emerald-600 hover:!bg-emerald-700"
|
|
||||||
onClick={() => setApproveTargetId(u.id)}
|
onClick={() => setApproveTargetId(u.id)}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white font-bold text-xs px-4 py-2 rounded-xl transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
تایید و اعطای لایسنس B2B
|
تایید و اعطای لایسنس B2B
|
||||||
</Button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<button
|
||||||
variant="secondary"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => setRejectTargetId(u.id)}
|
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>
|
</button>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
|
|||||||
@ -5,9 +5,6 @@ import api from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
export interface WikiTerm {
|
export interface WikiTerm {
|
||||||
key: string;
|
key: string;
|
||||||
@ -141,14 +138,13 @@ export default function Wiki() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مدیریت اصطلاحات علمی، مواد تشکیلدهنده و خواص درمانی</p>
|
<p className="text-gray-500 font-medium mt-1">مدیریت اصطلاحات علمی، مواد تشکیلدهنده و خواص درمانی</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Plus}
|
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
||||||
>
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
مفهوم جدید
|
مفهوم جدید
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
||||||
@ -188,8 +184,8 @@ export default function Wiki() {
|
|||||||
<td className="py-4 px-6 text-gray-500 text-sm max-w-xs truncate">{t.definition}</td>
|
<td className="py-4 px-6 text-gray-500 text-sm max-w-xs truncate">{t.definition}</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={() => openModal(t)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"><Edit2 className="w-4 h-4" /></button>
|
<button onClick={() => openModal(t)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"><Edit2 className="w-4 h-4" /></button>
|
||||||
<button onClick={() => setDeleteTargetKey(t.key)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"><Trash2 className="w-4 h-4" /></button>
|
<button onClick={() => setDeleteTargetKey(t.key)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2 className="w-4 h-4" /></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -206,34 +202,18 @@ export default function Wiki() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||||||
isOpen={isModalOpen}
|
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[95vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
title={editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
|
<h3 className="text-xl font-bold text-gray-900">
|
||||||
icon={BookOpen}
|
{editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
|
||||||
maxWidth="2xl"
|
</h3>
|
||||||
footer={
|
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<XCircle className="w-6 h-6" />
|
||||||
<Button
|
</button>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="wikiForm"
|
|
||||||
>
|
|
||||||
ذخیره
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<div className="p-6 overflow-y-auto flex-1">
|
||||||
<div className="space-y-6 text-right font-vazir">
|
|
||||||
<form id="wikiForm" onSubmit={handleSave} className="space-y-6">
|
<form id="wikiForm" onSubmit={handleSave} className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -251,9 +231,9 @@ export default function Wiki() {
|
|||||||
<textarea required value={formData.definition} onChange={(e) => setFormData({ ...formData, definition: e.target.value })} rows={5} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
|
<textarea required value={formData.definition} onChange={(e) => setFormData({ ...formData, definition: e.target.value })} rows={5} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Related Products Selector */}
|
{/* Related Products Selector (TASK-2.23) */}
|
||||||
<div className="space-y-2 border border-gray-200 p-4 rounded-xl bg-purple-50/40">
|
<div className="space-y-2 border border-gray-200 p-4 rounded-xl bg-purple-50/40">
|
||||||
<label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی</label>
|
<label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی (TASK-2.23)</label>
|
||||||
<select
|
<select
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const prodId = e.target.value;
|
const prodId = e.target.value;
|
||||||
@ -278,7 +258,7 @@ export default function Wiki() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFormData({ ...formData, relatedProducts: formData.relatedProducts.filter(id => id !== pId) })}
|
onClick={() => setFormData({ ...formData, relatedProducts: formData.relatedProducts.filter(id => id !== pId) })}
|
||||||
className="hover:text-red-600 font-black text-sm cursor-pointer"
|
className="hover:text-red-600 font-black text-sm"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@ -303,11 +283,17 @@ export default function Wiki() {
|
|||||||
<textarea rows={2} value={formData.metaDescription} onChange={(e) => setFormData({ ...formData, metaDescription: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm"></textarea>
|
<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>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
|
||||||
|
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">انصراف</button>
|
||||||
|
<button type="submit" form="wikiForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">ذخیره</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={!!deleteTargetKey}
|
isOpen={!!deleteTargetKey}
|
||||||
|
|||||||
@ -26,9 +26,6 @@ import {
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Button from '../components/ui/Button';
|
|
||||||
import Modal from '../components/ui/Modal';
|
|
||||||
|
|
||||||
|
|
||||||
export default function ZibalPortalPage() {
|
export default function ZibalPortalPage() {
|
||||||
const [activeMainTab, setActiveMainTab] = useState<'refunds' | 'checkouts' | 'queue' | 'gateway_transactions'>('refunds');
|
const [activeMainTab, setActiveMainTab] = useState<'refunds' | 'checkouts' | 'queue' | 'gateway_transactions'>('refunds');
|
||||||
@ -381,27 +378,25 @@ export default function ZibalPortalPage() {
|
|||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||||
{/* Top Bar with Filter & Search Controls */}
|
{/* Top Bar with Filter & Search Controls */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Download}
|
|
||||||
onClick={() => downloadCSV(refunds, 'zibal-refunds')}
|
onClick={() => downloadCSV(refunds, 'zibal-refunds')}
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-sm transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
دانلود گزارش
|
<Download className="w-4 h-4" />
|
||||||
</Button>
|
<span>دانلود گزارش</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<Button
|
<button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={fetchRefunds}
|
onClick={fetchRefunds}
|
||||||
isLoading={loadingRefunds}
|
disabled={loadingRefunds}
|
||||||
startIcon={RefreshCw}
|
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
|
||||||
title="بروزرسانی"
|
title="بروزرسانی"
|
||||||
/>
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${loadingRefunds ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Filter Inputs Bar */}
|
{/* Filter Inputs Bar */}
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{/* Date Filter */}
|
{/* Date Filter */}
|
||||||
@ -639,27 +634,25 @@ export default function ZibalPortalPage() {
|
|||||||
{activeMainTab === 'checkouts' && (
|
{activeMainTab === 'checkouts' && (
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Download}
|
|
||||||
onClick={() => downloadCSV(checkouts, 'zibal-checkouts')}
|
onClick={() => downloadCSV(checkouts, 'zibal-checkouts')}
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-sm transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
دانلود گزارش
|
<Download className="w-4 h-4" />
|
||||||
</Button>
|
<span>دانلود گزارش</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<Button
|
<button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={fetchCheckouts}
|
onClick={fetchCheckouts}
|
||||||
isLoading={loadingCheckouts}
|
disabled={loadingCheckouts}
|
||||||
startIcon={RefreshCw}
|
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
|
||||||
title="بروزرسانی"
|
title="بروزرسانی"
|
||||||
/>
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${loadingCheckouts ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Filter Inputs Bar */}
|
{/* Filter Inputs Bar */}
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
|
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
|
||||||
@ -858,27 +851,25 @@ export default function ZibalPortalPage() {
|
|||||||
{activeMainTab === 'gateway_transactions' && (
|
{activeMainTab === 'gateway_transactions' && (
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<button
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
startIcon={Download}
|
|
||||||
onClick={() => downloadCSV(gatewayTxList, 'zibal-ipg-transactions')}
|
onClick={() => downloadCSV(gatewayTxList, 'zibal-ipg-transactions')}
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-sm transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
دانلود گزارش درگاه
|
<Download className="w-4 h-4" />
|
||||||
</Button>
|
<span>دانلود گزارش درگاه</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<Button
|
<button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={fetchGatewayTx}
|
onClick={fetchGatewayTx}
|
||||||
isLoading={loadingGatewayTx}
|
disabled={loadingGatewayTx}
|
||||||
startIcon={RefreshCw}
|
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
|
||||||
title="بروزرسانی"
|
title="بروزرسانی"
|
||||||
/>
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${loadingGatewayTx ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Filter Inputs */}
|
{/* Filter Inputs */}
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
|
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
|
||||||
@ -1022,40 +1013,25 @@ export default function ZibalPortalPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ======================================================== */}
|
{/* ======================================================== */}
|
||||||
{/* MODAL: NEW REFUND SUBMIT */}
|
{/* MODAL: SUBMIT NEW REFUND / REVERSE */}
|
||||||
{/* ======================================================== */}
|
{/* ======================================================== */}
|
||||||
{newRefundModal && (
|
{newRefundModal && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||||
isOpen={newRefundModal}
|
<div className="bg-white rounded-3xl w-full max-w-lg border border-gray-200 shadow-2xl p-6 sm:p-8 space-y-6">
|
||||||
onClose={() => setNewRefundModal(false)}
|
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
||||||
title="ثبت و ارسال استرداد وجه جدید (Refund / Reverse)"
|
<h3 className="text-base font-black text-gray-900 flex items-center gap-2">
|
||||||
icon={RotateCcw}
|
<RotateCcw className="w-5 h-5 text-rose-600" />
|
||||||
maxWidth="lg"
|
ثبت درخواست استرداد وجه (Refund / Reverse)
|
||||||
footer={
|
</h3>
|
||||||
<div className="flex justify-end gap-2.5 w-full">
|
<button
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setNewRefundModal(false)}
|
onClick={() => setNewRefundModal(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-700 font-bold text-lg cursor-pointer"
|
||||||
>
|
>
|
||||||
انصراف
|
✕
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
form="zibalRefundForm"
|
|
||||||
startIcon={Send}
|
|
||||||
isLoading={isSubmittingRefund}
|
|
||||||
>
|
|
||||||
ارسال به زیبال
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
<form onSubmit={handleCreateRefundSubmit} className="space-y-4 text-xs">
|
||||||
<div className="space-y-4 text-xs font-vazir text-right">
|
|
||||||
<form id="zibalRefundForm" onSubmit={handleCreateRefundSubmit} className="space-y-4 text-xs">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block font-bold text-gray-700 mb-1">شماره تراکنش شاپرک (Track ID) *</label>
|
<label className="block font-bold text-gray-700 mb-1">شماره تراکنش شاپرک (Track ID) *</label>
|
||||||
<input
|
<input
|
||||||
@ -1116,38 +1092,47 @@ export default function ZibalPortalPage() {
|
|||||||
اولویت با Reverse باشد (برگشت آنی قبل از تسویه شاپرک)
|
اولویت با Reverse باشد (برگشت آنی قبل از تسویه شاپرک)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setNewRefundModal(false)}
|
||||||
|
className="px-5 py-2.5 rounded-xl font-bold bg-gray-100 hover:bg-gray-200 text-gray-600 cursor-pointer"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmittingRefund}
|
||||||
|
className="bg-rose-600 hover:bg-rose-700 text-white font-bold px-6 py-2.5 rounded-xl transition-colors flex items-center gap-2 cursor-pointer shadow-md shadow-rose-200"
|
||||||
|
>
|
||||||
|
{isSubmittingRefund ? <Spinner size="sm" /> : <Send className="w-4 h-4" />}
|
||||||
|
<span>ارسال به زیبال</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ======================================================== */}
|
{/* ======================================================== */}
|
||||||
{/* MODAL: DETAIL / INQUIRY VIEWER */}
|
{/* MODAL: DETAIL / INQUIRY VIEWER */}
|
||||||
{/* ======================================================== */}
|
{/* ======================================================== */}
|
||||||
{detailModalItem && (
|
{detailModalItem && (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||||
isOpen={!!detailModalItem}
|
<div className="bg-white rounded-3xl w-full max-w-lg border border-gray-200 shadow-2xl p-6 space-y-4">
|
||||||
onClose={() => setDetailModalItem(null)}
|
<div className="flex items-center justify-between border-b border-gray-100 pb-3">
|
||||||
title={detailModalItem.title}
|
<h3 className="font-black text-sm text-gray-900">{detailModalItem.title}</h3>
|
||||||
maxWidth="lg"
|
<button onClick={() => setDetailModalItem(null)} className="text-gray-400 font-bold text-lg">
|
||||||
footer={
|
✕
|
||||||
<div className="flex justify-end w-full">
|
</button>
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setDetailModalItem(null)}
|
|
||||||
>
|
|
||||||
بستن
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<pre className="bg-slate-900 text-emerald-400 p-4 rounded-xl text-[11px] font-mono overflow-x-auto dir-ltr">
|
<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)}
|
{JSON.stringify(detailModalItem.data, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</Modal>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user