feat(admin): standardize Buttons and Modals in Ingredients, Doctors, Prescriptions, Pets, B2B, and Wholesale managers
This commit is contained in:
parent
a92cd7fc85
commit
16516e7003
@ -3,8 +3,13 @@ import { Building2, CheckCircle2, XCircle, Clock, PhoneCall, Filter, X, Plus, Sh
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
import type { B2BInquiry, PartnerAccount } from '../types/admin';
|
import type { B2BInquiry, PartnerAccount } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function B2BManager() {
|
export default function B2BManager() {
|
||||||
const [activeTab, setActiveTab] = useState<'inquiries' | 'partners'>('inquiries');
|
const [activeTab, setActiveTab] = useState<'inquiries' | 'partners'>('inquiries');
|
||||||
|
|
||||||
@ -277,12 +282,13 @@ export default function B2BManager() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="xs"
|
||||||
onClick={() => openInquiryReview(inq)}
|
onClick={() => openInquiryReview(inq)}
|
||||||
className="px-3 py-1.5 bg-purple-600 text-white hover:bg-purple-700 rounded-lg text-xs font-bold transition-all shadow-sm"
|
|
||||||
>
|
>
|
||||||
تغییر وضعیت
|
تغییر وضعیت
|
||||||
</button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
@ -296,13 +302,14 @@ export default function B2BManager() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="font-bold text-gray-800">فهرست حسابهای تایید شده همکار</h3>
|
<h3 className="font-bold text-gray-800">فهرست حسابهای تایید شده همکار</h3>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
startIcon={Plus}
|
||||||
onClick={() => setIsPartnerModalOpen(true)}
|
onClick={() => setIsPartnerModalOpen(true)}
|
||||||
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-xl flex items-center gap-2 font-bold text-xs shadow-md shadow-purple-200"
|
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
افزودن همکار B2B جدید
|
افزودن همکار B2B جدید
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
@ -360,21 +367,41 @@ export default function B2BManager() {
|
|||||||
|
|
||||||
{/* Inquiry Review Modal */}
|
{/* Inquiry Review Modal */}
|
||||||
{selectedInquiry && (
|
{selectedInquiry && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
<Modal
|
||||||
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
|
isOpen={!!selectedInquiry}
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
onClose={() => setSelectedInquiry(null)}
|
||||||
<h3 className="text-lg font-bold text-gray-900">
|
title={`بررسی درخواست B2B (${selectedInquiry.companyName})`}
|
||||||
بررسی درخواست B2B ({selectedInquiry.companyName})
|
icon={Building2}
|
||||||
</h3>
|
maxWidth="lg"
|
||||||
<button onClick={() => setSelectedInquiry(null)} className="text-gray-400 hover:text-red-500 transition-colors">
|
footer={
|
||||||
<X className="w-6 h-6" />
|
<div className="flex justify-end gap-2.5 w-full">
|
||||||
</button>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedInquiry(null)}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
form="inquiryForm"
|
||||||
|
isLoading={isInquirySubmitting}
|
||||||
|
>
|
||||||
|
بروزرسانی وضعیت
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<form onSubmit={handleInquiryReviewSubmit} className="p-6 space-y-4">
|
>
|
||||||
|
<div className="space-y-4 text-right font-vazir">
|
||||||
|
<form id="inquiryForm" onSubmit={handleInquiryReviewSubmit} className="space-y-4">
|
||||||
<div className="p-4 bg-purple-50/50 rounded-xl border border-purple-100 space-y-1 text-xs">
|
<div className="p-4 bg-purple-50/50 rounded-xl border border-purple-100 space-y-1 text-xs">
|
||||||
<div><span className="font-bold text-gray-600">شخص رابط: </span>{selectedInquiry.contactName} ({selectedInquiry.phone})</div>
|
<div><span className="text-gray-500">رابط: </span><span className="font-bold text-gray-800">{selectedInquiry.contactName}</span></div>
|
||||||
<div><span className="font-bold text-gray-600">پیام درخواست: </span>{selectedInquiry.message}</div>
|
<div><span className="text-gray-500">تلفن / ایمیل: </span><span className="font-mono text-gray-800">{selectedInquiry.phone} - {selectedInquiry.email}</span></div>
|
||||||
|
<div><span className="text-gray-500">نوع کسبوکار: </span><span className="font-bold text-purple-700">{selectedInquiry.businessType}</span></div>
|
||||||
|
{selectedInquiry.message && <div className="mt-2 text-gray-700 bg-white p-2.5 rounded-lg border border-gray-200">{selectedInquiry.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -401,43 +428,43 @@ export default function B2BManager() {
|
|||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedInquiry(null)}
|
|
||||||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isInquirySubmitting}
|
|
||||||
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isInquirySubmitting && <Spinner size="sm" />}
|
|
||||||
بروزرسانی وضعیت
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Partner Account Setup Modal */}
|
{/* Partner Account Setup Modal */}
|
||||||
{isPartnerModalOpen && (
|
{isPartnerModalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
<Modal
|
||||||
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
|
isOpen={isPartnerModalOpen}
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
onClose={() => setIsPartnerModalOpen(false)}
|
||||||
<h3 className="text-lg font-bold text-gray-900">
|
title="تعریف حساب همکار B2B جدید"
|
||||||
تعریف حساب همکار B2B جدید
|
icon={Building2}
|
||||||
</h3>
|
maxWidth="lg"
|
||||||
<button onClick={() => setIsPartnerModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
footer={
|
||||||
<X className="w-6 h-6" />
|
<div className="flex justify-end gap-2.5 w-full">
|
||||||
</button>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsPartnerModalOpen(false)}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
form="partnerForm"
|
||||||
|
isLoading={isPartnerSubmitting}
|
||||||
|
>
|
||||||
|
ایجاد حساب همکار
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<form onSubmit={handlePartnerSave} className="p-6 space-y-4">
|
>
|
||||||
|
<div className="space-y-4 text-right font-vazir">
|
||||||
|
<form id="partnerForm" onSubmit={handlePartnerSave} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه کاربر (User ID) *</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه کاربر (User ID) *</label>
|
||||||
<input
|
<input
|
||||||
@ -498,28 +525,11 @@ export default function B2BManager() {
|
|||||||
<option value="tier3">سطح ۳ (۳۵٪ تخفیف)</option>
|
<option value="tier3">سطح ۳ (۳۵٪ تخفیف)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsPartnerModalOpen(false)}
|
|
||||||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isPartnerSubmitting}
|
|
||||||
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isPartnerSubmitting && <Spinner size="sm" />}
|
|
||||||
ایجاد حساب همکار
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Mail, MessageSquare } from 'lucide-react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
|
|
||||||
|
|
||||||
interface ContactSubmission {
|
interface ContactSubmission {
|
||||||
id: string;
|
id: string;
|
||||||
@ -153,16 +157,17 @@ export default function ContactSubmissions() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<button
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedSub(sub);
|
setSelectedSub(sub);
|
||||||
setStatus(sub.status);
|
setStatus(sub.status);
|
||||||
setAdminNotes(sub.adminNotes || '');
|
setAdminNotes(sub.adminNotes || '');
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-800 rounded-lg text-xs font-medium transition"
|
|
||||||
>
|
>
|
||||||
مشاهده & تغییر وضعیت
|
مشاهده & تغییر وضعیت
|
||||||
</button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@ -220,21 +225,45 @@ export default function ContactSubmissions() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
onClick={handleSaveContactInfo}
|
onClick={handleSaveContactInfo}
|
||||||
className="px-6 py-2.5 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-medium transition shadow-sm"
|
|
||||||
>
|
>
|
||||||
ذخیره تغییرات اطلاعات نمایندگی
|
ذخیره تغییرات اطلاعات نمایندگی
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal for Submission Detail & Status Update */}
|
{/* Modal for Submission Detail & Status Update */}
|
||||||
{selectedSub && (
|
{selectedSub && (
|
||||||
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
<Modal
|
||||||
<div className="bg-white rounded-2xl p-6 max-w-lg w-full shadow-xl">
|
isOpen={!!selectedSub}
|
||||||
<h3 className="text-lg font-bold text-gray-900 mb-4">جزئیات پیام {selectedSub.name}</h3>
|
onClose={() => setSelectedSub(null)}
|
||||||
|
title={`جزئیات پیام ${selectedSub.name}`}
|
||||||
|
icon={Mail}
|
||||||
|
maxWidth="lg"
|
||||||
|
footer={
|
||||||
|
<div className="flex justify-end gap-2.5 w-full">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedSub(null)}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleUpdateStatus}
|
||||||
|
>
|
||||||
|
ثبت تغییرات
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-4 text-right font-vazir">
|
||||||
<div className="space-y-3 text-sm text-gray-700 mb-6 bg-gray-50 p-4 rounded-xl">
|
<div className="space-y-3 text-sm text-gray-700 mb-6 bg-gray-50 p-4 rounded-xl">
|
||||||
<div><span className="font-semibold text-gray-900">تلفن:</span> {selectedSub.phone}</div>
|
<div><span className="font-semibold text-gray-900">تلفن:</span> {selectedSub.phone}</div>
|
||||||
{selectedSub.email && <div><span className="font-semibold text-gray-900">ایمیل:</span> {selectedSub.email}</div>}
|
{selectedSub.email && <div><span className="font-semibold text-gray-900">ایمیل:</span> {selectedSub.email}</div>}
|
||||||
@ -268,24 +297,11 @@ export default function ContactSubmissions() {
|
|||||||
className="w-full px-3 py-2 border rounded-lg text-sm"
|
className="w-full px-3 py-2 border rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedSub(null)}
|
|
||||||
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleUpdateStatus}
|
|
||||||
className="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg text-sm font-medium"
|
|
||||||
>
|
|
||||||
ثبت تغییرات
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
|
|
||||||
|
|
||||||
export interface Doctor {
|
export interface Doctor {
|
||||||
id: string;
|
id: string;
|
||||||
@ -165,13 +168,14 @@ export default function DoctorsManager() {
|
|||||||
ثبت اطلاعات و رزومه (CV) دامپزشکان جهت انتساب به ویدئوها، مشاورهها و معرفی کادر تخصصی
|
ثبت اطلاعات و رزومه (CV) دامپزشکان جهت انتساب به ویدئوها، مشاورهها و معرفی کادر تخصصی
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
startIcon={Plus}
|
||||||
onClick={() => handleOpenModal()}
|
onClick={() => handleOpenModal()}
|
||||||
className="flex items-center justify-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-5 py-3 rounded-2xl font-bold transition-all shadow-md shadow-purple-200 cursor-pointer text-sm"
|
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
افزودن پزشک جدید
|
||||||
<span>افزودن پزشک جدید</span>
|
</Button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter / Search */}
|
{/* Filter / Search */}
|
||||||
@ -203,13 +207,14 @@ export default function DoctorsManager() {
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-bold text-gray-800 mb-1">پزشکی یافت نشد</h3>
|
<h3 className="text-lg font-bold text-gray-800 mb-1">پزشکی یافت نشد</h3>
|
||||||
<p className="text-sm text-gray-400 mb-6">اولین پزشک یا متخصص کادر علمی را به سامانه اضافه کنید.</p>
|
<p className="text-sm text-gray-400 mb-6">اولین پزشک یا متخصص کادر علمی را به سامانه اضافه کنید.</p>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
startIcon={Plus}
|
||||||
onClick={() => handleOpenModal()}
|
onClick={() => handleOpenModal()}
|
||||||
className="inline-flex items-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl font-bold text-sm shadow-sm"
|
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
افزودن اولین پزشک
|
افزودن اولین پزشک
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
@ -228,23 +233,23 @@ export default function DoctorsManager() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleOpenModal(doc)}
|
onClick={() => handleOpenModal(doc)}
|
||||||
className="p-2 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-xl transition-colors cursor-pointer"
|
className="p-2 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-xl transition-colors cursor-pointer"
|
||||||
title="ویرایش"
|
title="ویرایش اطلاعات"
|
||||||
>
|
>
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeleteTargetId(doc.id)}
|
onClick={() => setDeleteTargetId(doc.id)}
|
||||||
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
||||||
title="حذف"
|
title="حذف پزشک"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile Header */}
|
{/* Avatar & Main Info */}
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-purple-50 border border-purple-100 flex items-center justify-center overflow-hidden flex-shrink-0 relative">
|
<div className="w-16 h-16 rounded-2xl bg-purple-50 border border-purple-100 overflow-hidden flex items-center justify-center shrink-0">
|
||||||
{doc.avatarUrl ? (
|
{doc.avatarUrl ? (
|
||||||
<img
|
<img
|
||||||
src={doc.avatarUrl.startsWith('http') ? doc.avatarUrl : `${BASE_DOMAIN}${doc.avatarUrl}`}
|
src={doc.avatarUrl.startsWith('http') ? doc.avatarUrl : `${BASE_DOMAIN}${doc.avatarUrl}`}
|
||||||
@ -252,199 +257,228 @@ export default function DoctorsManager() {
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserCheck className="w-7 h-7 text-purple-400" />
|
<UserCheck className="w-8 h-8 text-purple-400" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-extrabold text-gray-900 text-base leading-tight mb-1">{doc.name}</h3>
|
<h4 className="font-extrabold text-gray-900 text-base">{doc.name}</h4>
|
||||||
<p className="text-xs text-purple-700 font-bold">{doc.title || 'دامپزشک'}</p>
|
<p className="text-xs text-purple-600 font-bold mt-0.5">{doc.title || 'پزشک متخصص'}</p>
|
||||||
{doc.clinicName && (
|
{doc.clinicName && (
|
||||||
<p className="text-xs text-gray-400 font-medium mt-0.5">{doc.clinicName}</p>
|
<p className="text-[11px] text-gray-400 mt-0.5 flex items-center gap-1">
|
||||||
|
<ShieldCheck className="w-3 h-3 text-gray-400" />
|
||||||
|
{doc.clinicName}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bio snippet */}
|
{/* Bio Summary */}
|
||||||
{doc.bio && (
|
{doc.bio && (
|
||||||
<p className="text-xs text-gray-500 leading-relaxed line-clamp-2 mb-4 bg-gray-50/70 p-3 rounded-2xl border border-gray-100">
|
<p className="text-xs text-gray-500 line-clamp-2 leading-relaxed mb-4 bg-gray-50/50 p-2.5 rounded-xl border border-gray-100">
|
||||||
{doc.bio}
|
{doc.bio}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* CV Button */}
|
{/* Contact & CV Trigger */}
|
||||||
|
<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="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"
|
className="mr-auto flex items-center gap-1 text-purple-600 hover:text-purple-700 bg-purple-50 hover:bg-purple-100 px-2.5 py-1 rounded-lg font-bold transition-colors cursor-pointer text-[11px]"
|
||||||
>
|
>
|
||||||
<FileText className="w-3.5 h-3.5" />
|
<FileText className="w-3 h-3" />
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Create / Edit Modal */}
|
{/* Modal Add/Edit */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs">
|
<Modal
|
||||||
<div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden border border-gray-100 animate-in fade-in zoom-in-95 duration-200">
|
isOpen={isModalOpen}
|
||||||
{/* Modal Header */}
|
onClose={() => setIsModalOpen(false)}
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50">
|
title={editingDoctor ? 'ویرایش اطلاعات پزشک' : 'افزودن پزشک جدید به کادر علمی'}
|
||||||
<div className="flex items-center gap-3">
|
icon={UserCheck}
|
||||||
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
|
maxWidth="2xl"
|
||||||
<UserCheck className="w-5 h-5" />
|
footer={
|
||||||
</div>
|
<div className="flex justify-end gap-2.5 w-full">
|
||||||
<div>
|
<Button
|
||||||
<h3 className="text-lg font-bold text-gray-900">
|
variant="secondary"
|
||||||
{editingDoctor ? 'ویرایش اطلاعات پزشک' : 'افزودن پزشک جدید'}
|
size="sm"
|
||||||
</h3>
|
type="button"
|
||||||
<p className="text-xs text-gray-500 font-medium">مشخصات هویتی، آواتار و رزومه علمی پزشک</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
onClick={() => setIsModalOpen(false)}
|
||||||
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
انصراف
|
||||||
</button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
form="doctorForm"
|
||||||
|
>
|
||||||
|
{editingDoctor ? 'ذخیره تغییرات' : 'افزودن پزشک'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
{/* Form */}
|
>
|
||||||
<form onSubmit={handleSave} className="p-6 overflow-y-auto flex-1 space-y-4">
|
<div className="space-y-6 text-right font-vazir">
|
||||||
|
<form id="doctorForm" onSubmit={handleSave} className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Name */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">
|
<label className="block text-xs font-bold text-gray-700 mb-1.5">نام و نام خانوادگی *</label>
|
||||||
نام و نام خانوادگی پزشک <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
placeholder="مثال: دکتر کلاوس هنینگ"
|
placeholder="مثال: دکتر سارا صادقی"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
|
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 font-bold"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">تخصص یا عنوان علمی</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1.5">عنوان / تخصص *</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="مثال: متخصص ارتوپدی و تغذیه دامپزشکی"
|
required
|
||||||
|
placeholder="مثال: متخصص تغذیه و فیزیولوژی حیوانات خانگی"
|
||||||
value={formData.title}
|
value={formData.title}
|
||||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
|
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600 font-bold"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Clinic Name */}
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">نام بیمارستان / کلینیک یا دانشگاه</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1.5">نام کلینیک / بیمارستان</label>
|
||||||
<input
|
<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-sm focus:outline-none focus:border-purple-600"
|
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs focus:outline-none focus:border-purple-600"
|
||||||
/>
|
/>
|
||||||
</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 Selector */}
|
{/* Avatar Picker */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">تصویر پرتره / آواتار</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1.5">تصویر آواتار / عکس پرتره</label>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input
|
<div className="w-12 h-12 rounded-xl bg-gray-100 border border-gray-200 overflow-hidden flex items-center justify-center shrink-0">
|
||||||
type="text"
|
{formData.avatarUrl ? (
|
||||||
placeholder="https://..."
|
|
||||||
value={formData.avatarUrl}
|
|
||||||
onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })}
|
|
||||||
className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsAvatarMediaOpen(true)}
|
|
||||||
className="px-4 py-2.5 bg-purple-100 hover:bg-purple-200 text-purple-700 rounded-xl text-xs font-bold flex items-center gap-1.5 transition-colors cursor-pointer flex-shrink-0"
|
|
||||||
>
|
|
||||||
<ImageIcon className="w-4 h-4" />
|
|
||||||
<span>انتخاب از رسانهها</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{formData.avatarUrl && (
|
|
||||||
<div className="mt-2 w-16 h-16 rounded-xl border border-gray-200 overflow-hidden bg-gray-50">
|
|
||||||
<img
|
<img
|
||||||
src={formData.avatarUrl.startsWith('http') ? formData.avatarUrl : `${BASE_DOMAIN}${formData.avatarUrl}`}
|
src={formData.avatarUrl.startsWith('http') ? formData.avatarUrl : `${BASE_DOMAIN}${formData.avatarUrl}`}
|
||||||
alt="Preview"
|
alt="Avatar preview"
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
|
<ImageIcon className="w-5 h-5 text-gray-400" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bio */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">بیوگرافی کوتاه (معرفی سریع)</label>
|
|
||||||
<textarea
|
|
||||||
rows={2}
|
|
||||||
placeholder="معرفی کوتاه پزشک و فعالیتها..."
|
|
||||||
value={formData.bio}
|
|
||||||
onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
|
|
||||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CV */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">
|
|
||||||
رزومه، سوابق تحصیلی و مقالات علمی (CV)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
rows={4}
|
|
||||||
placeholder="سوابق تحصیلی، دورههای تخصصی، مقالات منتشر شده، تخصصهای درمانی و..."
|
|
||||||
value={formData.cv}
|
|
||||||
onChange={(e) => setFormData({ ...formData, cv: e.target.value })}
|
|
||||||
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-purple-600 leading-relaxed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contacts */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">تلفن تماس / واتساپ</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="۰۹۱۲..."
|
placeholder="آدرس تصویر یا انتخاب از رسانهها..."
|
||||||
|
value={formData.avatarUrl}
|
||||||
|
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"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsAvatarMediaOpen(true)}
|
||||||
|
>
|
||||||
|
گالری رسانه
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bio & CV */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-700 mb-1.5">معرفی کوتاه (Bio)</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
placeholder="خلاصهای از سوابق و حوزه کاری..."
|
||||||
|
value={formData.bio}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-700 mb-1.5">رزومه کامل و مقالات علمی (CV)</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
placeholder="سوابق تحصیلی، مقالات چاپشده، مدارک بینالمللی و تجربیات کلینیکی..."
|
||||||
|
value={formData.cv}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social & Contact */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-700 mb-1">تلفن تماس کلینیک</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
dir="ltr"
|
||||||
|
placeholder="02188888888"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||||
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs"
|
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">ایمیل</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">ایمیل</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="doctor@example.com"
|
dir="ltr"
|
||||||
|
placeholder="dr.sadeghi@canina.ir"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs"
|
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">اینستاگرام</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">آیدی اینستاگرام</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="@dr_..."
|
dir="ltr"
|
||||||
|
placeholder="@dr.sadeghi"
|
||||||
value={formData.instagram}
|
value={formData.instagram}
|
||||||
onChange={(e) => setFormData({ ...formData, instagram: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, instagram: e.target.value })}
|
||||||
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
|
className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono"
|
||||||
@ -464,56 +498,38 @@ export default function DoctorsManager() {
|
|||||||
<span className="text-xs font-bold text-gray-700">پزشک فعال است و در لیست نمایش داده شود</span>
|
<span className="text-xs font-bold text-gray-700">پزشک فعال است و در لیست نمایش داده شود</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex justify-end gap-3 pt-4 border-t border-gray-100">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
className="px-5 py-2.5 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-xl text-xs font-bold transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-bold shadow-md shadow-purple-200 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
{editingDoctor ? 'ذخیره تغییرات' : 'افزودن پزشک'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* CV Detail Modal */}
|
{/* CV Detail Modal */}
|
||||||
{isCvDetailsOpen && (
|
{isCvDetailsOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs">
|
<Modal
|
||||||
<div className="bg-white rounded-3xl max-w-xl w-full p-6 shadow-2xl border border-gray-100 animate-in fade-in zoom-in-95">
|
isOpen={!!isCvDetailsOpen}
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 pb-4 mb-4">
|
onClose={() => setIsCvDetailsOpen(null)}
|
||||||
<div className="flex items-center gap-3">
|
title={`رزومه علمی — ${isCvDetailsOpen.name}`}
|
||||||
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
|
icon={FileText}
|
||||||
<FileText className="w-5 h-5" />
|
maxWidth="lg"
|
||||||
</div>
|
footer={
|
||||||
<div>
|
<div className="flex justify-end w-full">
|
||||||
<h3 className="font-extrabold text-gray-900 text-base">{isCvDetailsOpen.name}</h3>
|
<Button
|
||||||
<p className="text-xs text-purple-600 font-bold">{isCvDetailsOpen.title}</p>
|
variant="secondary"
|
||||||
</div>
|
size="sm"
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCvDetailsOpen(null)}
|
onClick={() => setIsCvDetailsOpen(null)}
|
||||||
className="p-2 text-gray-400 hover:text-red-500 rounded-xl transition-colors cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
بستن
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
}
|
||||||
|
>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-2xl max-h-96 overflow-y-auto whitespace-pre-wrap text-xs text-gray-700 leading-relaxed font-sans text-right font-vazir">
|
||||||
{isCvDetailsOpen.cv}
|
{isCvDetailsOpen.cv}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Media Selector Modal for Avatar */}
|
{/* Media Selector Modal for Avatar */}
|
||||||
<MediaSelector
|
<MediaSelector
|
||||||
isOpen={isAvatarMediaOpen}
|
isOpen={isAvatarMediaOpen}
|
||||||
|
|||||||
@ -5,8 +5,11 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
import type { Ingredient } from '../types/admin';
|
import type { Ingredient } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
export default function IngredientsManager() {
|
export default function IngredientsManager() {
|
||||||
const [ingredients, setIngredients] = useState<Ingredient[]>([]);
|
const [ingredients, setIngredients] = useState<Ingredient[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@ -164,15 +167,17 @@ export default function IngredientsManager() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مدیریت مواد موثره، ترکیبات دارویی و خواص درمانی محصولات کنینا</p>
|
<p className="text-gray-500 font-medium mt-1">مدیریت مواد موثره، ترکیبات دارویی و خواص درمانی محصولات کنینا</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
startIcon={Plus}
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
|
||||||
افزودن ترکیب جدید
|
افزودن ترکیب جدید
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-right">
|
<table className="w-full text-right">
|
||||||
@ -244,21 +249,39 @@ export default function IngredientsManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form Modal */}
|
{/* Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
<Modal
|
||||||
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200 max-h-[90vh] flex flex-col">
|
isOpen={isModalOpen}
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
onClose={() => setIsModalOpen(false)}
|
||||||
<h3 className="text-lg font-bold text-gray-900">
|
title={editingIngredient ? 'ویرایش ترکیب دانشنامه' : 'افزودن ترکیب جدید'}
|
||||||
{editingIngredient ? 'ویرایش ترکیب دانشنامه' : 'افزودن ترکیب جدید'}
|
icon={FlaskConical}
|
||||||
</h3>
|
maxWidth="2xl"
|
||||||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
footer={
|
||||||
<X className="w-6 h-6" />
|
<div className="flex justify-end gap-2.5 w-full">
|
||||||
</button>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
form="ingredientForm"
|
||||||
|
isLoading={isSaving}
|
||||||
|
>
|
||||||
|
ذخیره تغییرات
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<form onSubmit={handleSave} className="p-6 space-y-4 overflow-y-auto flex-1">
|
>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="space-y-6 text-right font-vazir">
|
||||||
|
<form id="ingredientForm" onSubmit={handleSave} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">نام فارسی *</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">نام فارسی *</label>
|
||||||
<input
|
<input
|
||||||
@ -285,7 +308,7 @@ export default function IngredientsManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه یکتا (slug)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه یکتا (slug)</label>
|
||||||
<input
|
<input
|
||||||
@ -312,28 +335,29 @@ export default function IngredientsManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر ترکیب (imageUrl)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر ترکیب</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.imageUrl}
|
value={formData.imageUrl}
|
||||||
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
|
||||||
placeholder="https://... یا /uploads/..."
|
placeholder="آدرس تصویر یا انتخاب از رسانهها..."
|
||||||
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={() => setIsMediaSelectorOpen(true)}
|
onClick={() => setIsMediaSelectorOpen(true)}
|
||||||
className="px-3 py-2.5 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0"
|
|
||||||
>
|
>
|
||||||
رسانه
|
انتخاب از رسانه
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">خواص و مزایا (benefits array)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">خواص و مزایا</label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
@ -347,22 +371,23 @@ export default function IngredientsManager() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="تایپ کنید و اینتر بزنید (مثال: ترمیم غضروف)"
|
placeholder="تایپ کنید و اینتر بزنید (مثال: ترمیم غضروف)"
|
||||||
className="flex-1 px-4 py-2 rounded-xl border border-gray-200 text-xs outline-none"
|
className="flex-1 px-4 py-2 rounded-xl border border-gray-200 text-xs outline-none focus:border-purple-500"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
size="xs"
|
||||||
onClick={addBenefitTag}
|
onClick={addBenefitTag}
|
||||||
className="px-3 py-2 bg-purple-600 text-white rounded-xl text-xs font-bold"
|
|
||||||
>
|
>
|
||||||
+ افزودن
|
افزودن
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5 p-2 bg-gray-50 rounded-xl border border-gray-100 min-h-[40px]">
|
<div className="flex flex-wrap gap-1.5 p-2 bg-gray-50 rounded-xl border border-gray-100 min-h-[40px]">
|
||||||
{formData.benefits.map((b, i) => (
|
{formData.benefits.map((b, i) => (
|
||||||
<span key={i} className="inline-flex items-center gap-1 px-2.5 py-1 bg-purple-100 text-purple-800 text-xs font-bold rounded-lg">
|
<span key={i} className="inline-flex items-center gap-1 px-2.5 py-1 bg-purple-100 text-purple-800 text-xs font-bold rounded-lg">
|
||||||
<Tag className="w-3 h-3" />
|
<Tag className="w-3 h-3" />
|
||||||
{b}
|
{b}
|
||||||
<button type="button" onClick={() => removeBenefitTag(b)} className="hover:text-red-500">
|
<button type="button" onClick={() => removeBenefitTag(b)} className="hover:text-red-500 cursor-pointer">
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@ -381,29 +406,12 @@ export default function IngredientsManager() {
|
|||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSaving}
|
|
||||||
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isSaving && <Spinner size="sm" />}
|
|
||||||
ذخیره تغییرات
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
{deleteTargetId && (
|
{deleteTargetId && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
|
|
||||||
|
|
||||||
export interface Pet {
|
export interface Pet {
|
||||||
id: string;
|
id: string;
|
||||||
@ -215,28 +218,25 @@ export default function Pets() {
|
|||||||
|
|
||||||
{/* Pet Medical Dossier Details Modal */}
|
{/* Pet Medical Dossier Details Modal */}
|
||||||
{selectedPetForView && (
|
{selectedPetForView && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir" dir="rtl">
|
<Modal
|
||||||
<div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-gray-100 flex flex-col">
|
isOpen={!!selectedPetForView}
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between sticky top-0 bg-white z-10">
|
onClose={() => setSelectedPetForView(null)}
|
||||||
<div className="flex items-center gap-3">
|
title={`پرونده پزشکی و پایش سلامت: ${selectedPetForView.name}`}
|
||||||
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-700 flex items-center justify-center font-black">
|
icon={selectedPetForView.type === 'گربه' ? Cat : Dog}
|
||||||
{selectedPetForView.type === 'گربه' ? <Cat className="w-5 h-5" /> : <Dog className="w-5 h-5" />}
|
maxWidth="2xl"
|
||||||
</div>
|
footer={
|
||||||
<div>
|
<div className="flex justify-end w-full">
|
||||||
<h3 className="text-base font-black text-gray-900">پرونده پزشکی و پایش سلامت: {selectedPetForView.name}</h3>
|
<Button
|
||||||
<p className="text-xs text-gray-400 font-mono">شناسه: {selectedPetForView.id}</p>
|
variant="secondary"
|
||||||
</div>
|
size="sm"
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedPetForView(null)}
|
onClick={() => setSelectedPetForView(null)}
|
||||||
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
بستن پرونده
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div className="p-6 space-y-6">
|
>
|
||||||
|
<div className="space-y-6 text-right font-vazir">
|
||||||
{/* Pet Info Banner */}
|
{/* Pet Info Banner */}
|
||||||
<div className="p-4 bg-purple-50/60 rounded-2xl border border-purple-100 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 text-xs">
|
<div className="p-4 bg-purple-50/60 rounded-2xl border border-purple-100 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 text-xs">
|
||||||
<div>
|
<div>
|
||||||
@ -338,10 +338,10 @@ export default function Pets() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={!!deleteTargetId}
|
isOpen={!!deleteTargetId}
|
||||||
title="حذف حیوان خانگی"
|
title="حذف حیوان خانگی"
|
||||||
|
|||||||
@ -5,8 +5,11 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ImagePreviewModal from '../components/ui/ImagePreviewModal';
|
import ImagePreviewModal from '../components/ui/ImagePreviewModal';
|
||||||
import Badge from '../components/ui/Badge';
|
import Badge from '../components/ui/Badge';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
import type { Prescription } from '../types/admin';
|
import type { Prescription } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
interface ProductItem {
|
interface ProductItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -243,12 +246,13 @@ export default function PrescriptionsManager() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="xs"
|
||||||
onClick={() => openReviewModal(rx)}
|
onClick={() => openReviewModal(rx)}
|
||||||
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-bold transition-all shadow-xs cursor-pointer"
|
|
||||||
>
|
>
|
||||||
بررسی و تجویز
|
بررسی و تجویز
|
||||||
</button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@ -261,27 +265,36 @@ export default function PrescriptionsManager() {
|
|||||||
|
|
||||||
{/* Review Modal */}
|
{/* Review Modal */}
|
||||||
{selectedRx && (
|
{selectedRx && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir" dir="rtl">
|
<Modal
|
||||||
<div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-gray-100 flex flex-col">
|
isOpen={!!selectedRx}
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50 sticky top-0 bg-white z-10">
|
onClose={() => setSelectedRx(null)}
|
||||||
<div className="flex items-center gap-3">
|
title="بررسی و ثبت نظر تخصصی نسخه"
|
||||||
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
|
icon={FileText}
|
||||||
<FileText className="w-5 h-5" />
|
maxWidth="2xl"
|
||||||
</div>
|
footer={
|
||||||
<div>
|
<div className="flex justify-end gap-2.5 w-full">
|
||||||
<h3 className="text-lg font-bold text-gray-900">بررسی و ثبت نظر تخصصی نسخه</h3>
|
<Button
|
||||||
<p className="text-xs text-gray-500">کد نسخه: {selectedRx.id}</p>
|
variant="secondary"
|
||||||
</div>
|
size="sm"
|
||||||
</div>
|
type="button"
|
||||||
<button
|
|
||||||
onClick={() => setSelectedRx(null)}
|
onClick={() => setSelectedRx(null)}
|
||||||
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
انصراف
|
||||||
</button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
form="prescriptionForm"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
ثبت نهایی و ارسال پیامک
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<form onSubmit={handleReviewSubmit} className="p-6 space-y-5">
|
>
|
||||||
|
<div className="space-y-5 text-right font-vazir">
|
||||||
|
<form id="prescriptionForm" onSubmit={handleReviewSubmit} className="space-y-5">
|
||||||
{/* Pet & User Details Card */}
|
{/* Pet & User Details Card */}
|
||||||
<div className="p-4 bg-gray-50 rounded-2xl border border-gray-200 text-xs space-y-2">
|
<div className="p-4 bg-gray-50 rounded-2xl border border-gray-200 text-xs space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -295,58 +308,68 @@ export default function PrescriptionsManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedRx.notes && (
|
{selectedRx.notes && (
|
||||||
<div className="pt-2 border-t border-gray-200 text-gray-700">
|
<div className="pt-2 border-t border-gray-200 text-gray-500">
|
||||||
<strong className="block mb-0.5 text-gray-900">یادداشت کاربر:</strong>
|
<span className="font-bold text-gray-700">توضیحات کاربر: </span>
|
||||||
{selectedRx.notes}
|
{selectedRx.notes}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div 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>
|
||||||
|
|
||||||
{/* Status Selector */}
|
{/* Action Decision */}
|
||||||
<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-2 gap-3">
|
<div className="grid grid-cols-3 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"
|
||||||
onClick={() => setReviewStatus('APPROVED')}
|
key={s.id}
|
||||||
className={`py-3 px-4 rounded-xl border font-bold text-xs flex items-center justify-center gap-2 transition-all cursor-pointer ${
|
onClick={() => setReviewStatus(s.id as any)}
|
||||||
reviewStatus === 'APPROVED'
|
className={`p-3 rounded-2xl border text-xs font-bold text-center transition-all cursor-pointer ${
|
||||||
? 'border-green-500 bg-green-50 text-green-700 shadow-xs font-black'
|
reviewStatus === s.id
|
||||||
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
|
? `${s.color} shadow-xs ring-2 ring-purple-600/20`
|
||||||
|
: 'border-gray-200 text-gray-600 hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="w-4 h-4" /> تایید و ثبت تجویز (APPROVED)
|
{s.label}
|
||||||
</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>
|
||||||
|
|
||||||
{/* Prescribed Products Selector */}
|
{/* Recommended Products Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-black text-gray-700 mb-1 flex items-center justify-between">
|
<label className="block text-xs font-black text-gray-700 mb-1">
|
||||||
<span>محصولات و مکملهای تجویزی پزشک (جهت خرید آسان کاربر)</span>
|
داروها و مکملهای تجویزی (انتخاب از محصولات سایت برای خرید فوری سرپرست)
|
||||||
<span className="text-[10px] text-gray-400 font-normal">{selectedProductIds.length} محصول انتخاب شده</span>
|
|
||||||
</label>
|
</label>
|
||||||
<div className="max-h-40 overflow-y-auto border border-gray-200 rounded-xl p-2 space-y-1 bg-white">
|
<p className="text-[11px] text-gray-400 mb-2">محصولاتی که تیک میزنید مستقیماً در پنل کاربر به عنوان سبد پیشنهادی نسخه قرار میگیرند.</p>
|
||||||
{products.map(p => {
|
|
||||||
|
<div className="max-h-48 overflow-y-auto border border-gray-200 rounded-2xl p-2 space-y-1 bg-white">
|
||||||
|
{products.map((p) => {
|
||||||
const isSelected = selectedProductIds.includes(p.id);
|
const isSelected = selectedProductIds.includes(p.id);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedProductIds(prev =>
|
setSelectedProductIds((prev) =>
|
||||||
isSelected ? prev.filter(id => id !== p.id) : [...prev, p.id]
|
isSelected ? prev.filter((id) => id !== p.id) : [...prev, p.id]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className={`p-2 rounded-lg text-xs font-bold flex items-center justify-between cursor-pointer transition-colors ${
|
className={`p-2 rounded-lg text-xs font-bold flex items-center justify-between cursor-pointer transition-colors ${
|
||||||
@ -379,29 +402,12 @@ export default function PrescriptionsManager() {
|
|||||||
className="w-full px-3.5 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs leading-relaxed"
|
className="w-full px-3.5 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs leading-relaxed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedRx(null)}
|
|
||||||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 text-xs font-bold hover:bg-gray-50 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white text-xs font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2 cursor-pointer"
|
|
||||||
>
|
|
||||||
{isSubmitting && <Spinner size="sm" />}
|
|
||||||
ثبت نهایی و ارسال پیامک
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Fullscreen Lightbox for Prescription Image */}
|
{/* Fullscreen Lightbox for Prescription Image */}
|
||||||
<ImagePreviewModal
|
<ImagePreviewModal
|
||||||
isOpen={!!previewImageUrl}
|
isOpen={!!previewImageUrl}
|
||||||
|
|||||||
@ -5,8 +5,11 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
import type { Testimonial } from '../types/admin';
|
import type { Testimonial } from '../types/admin';
|
||||||
|
|
||||||
|
|
||||||
export default function TestimonialsManager() {
|
export default function TestimonialsManager() {
|
||||||
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
|
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@ -148,13 +151,14 @@ export default function TestimonialsManager() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مدیریت توصیه نامهها و نظرات تایید شده جهت نمایش در صفحه اصلی و گالری دامپزشکان</p>
|
<p className="text-gray-500 font-medium mt-1">مدیریت توصیه نامهها و نظرات تایید شده جهت نمایش در صفحه اصلی و گالری دامپزشکان</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
startIcon={Plus}
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
|
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
|
||||||
افزودن نظر جدید
|
افزودن نظر جدید
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
@ -199,45 +203,51 @@ export default function TestimonialsManager() {
|
|||||||
{item.roleTitle && <span className="text-xs text-purple-600 font-medium">{item.roleTitle}</span>}
|
{item.roleTitle && <span className="text-xs text-purple-600 font-medium">{item.roleTitle}</span>}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6 text-gray-600 text-sm max-w-md line-clamp-2">
|
<td className="py-4 px-6">
|
||||||
{item.content}
|
<p className="text-gray-600 text-sm line-clamp-2 leading-relaxed max-w-md">{item.content}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-1 text-amber-500">
|
<div className="flex items-center gap-1 text-amber-500">
|
||||||
{Array.from({ length: item.rating || 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<Star key={i} className="w-4 h-4 fill-amber-400" />
|
<Star
|
||||||
|
key={i}
|
||||||
|
className={`w-3.5 h-3.5 ${i < item.rating ? 'fill-amber-400 text-amber-400' : 'text-gray-200'}`}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{item.isFeatured && (
|
<button
|
||||||
<span className="px-2 py-0.5 bg-amber-50 text-amber-700 text-[11px] font-bold rounded border border-amber-200">
|
onClick={() => handleToggleActive(item)}
|
||||||
ویژه (Featured)
|
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold transition-colors cursor-pointer ${
|
||||||
</span>
|
item.isActive
|
||||||
)}
|
? 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||||
|
: 'bg-rose-50 text-rose-700 hover:bg-rose-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.isActive ? <CheckCircle2 className="w-3 h-3" /> : <XCircle className="w-3 h-3" />}
|
||||||
|
{item.isActive ? 'فعال' : 'غیرفعال'}
|
||||||
|
</button>
|
||||||
{(item as any).showOnHome && (
|
{(item as any).showOnHome && (
|
||||||
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-[11px] font-bold rounded border border-blue-200">
|
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs font-bold rounded-md">
|
||||||
🏠 صفحه اصلی
|
خانه
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{item.isActive ? (
|
|
||||||
<span className="inline-flex items-center gap-0.5 text-green-600 text-xs font-bold">
|
|
||||||
<CheckCircle2 className="w-3.5 h-3.5" /> فعال
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center gap-0.5 text-gray-400 text-xs font-bold">
|
|
||||||
<XCircle className="w-3.5 h-3.5" /> غیرفعال
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={() => openModal(item)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
<button
|
||||||
|
onClick={() => openModal(item)}
|
||||||
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setDeleteTargetId(item.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
<button
|
||||||
|
onClick={() => setDeleteTargetId(item.id)}
|
||||||
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -250,88 +260,117 @@ export default function TestimonialsManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form Modal */}
|
{/* Modal */}
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
<Modal
|
||||||
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
|
isOpen={isModalOpen}
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
onClose={() => setIsModalOpen(false)}
|
||||||
<h3 className="text-lg font-bold text-gray-900">
|
title={editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'}
|
||||||
{editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'}
|
icon={MessageSquareQuote}
|
||||||
</h3>
|
maxWidth="2xl"
|
||||||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
footer={
|
||||||
<X className="w-6 h-6" />
|
<div className="flex justify-end gap-2.5 w-full">
|
||||||
</button>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
form="testimonialForm"
|
||||||
|
isLoading={isSaving}
|
||||||
|
>
|
||||||
|
ذخیره تغییرات
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<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
|
||||||
required
|
|
||||||
type="text"
|
type="text"
|
||||||
|
required
|
||||||
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">سمت / عنوان تخصص (roleTitle)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">سمت / تخصص</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">تصویر آواتار (avatarUrl)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر نویسنده / آواتار</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.avatarUrl}
|
value={formData.avatarUrl}
|
||||||
onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, avatarUrl: e.target.value })}
|
||||||
placeholder="https://... یا /uploads/..."
|
placeholder="آدرس تصویر یا انتخاب از رسانهها..."
|
||||||
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
|
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={() => setIsMediaSelectorOpen(true)}
|
onClick={() => setIsMediaSelectorOpen(true)}
|
||||||
className="px-3 py-2.5 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0"
|
|
||||||
>
|
>
|
||||||
رسانه
|
انتخاب از رسانه
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">متن نظر / رضایتنامه *</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">متن نظر / توصیه نامه *</label>
|
||||||
<textarea
|
<textarea
|
||||||
required
|
required
|
||||||
rows={4}
|
rows={4}
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||||
placeholder="متن نظر یا توصیه بالینی را بنویسید..."
|
placeholder="متن کامل نظر و تجربه همکاری با محصولات کنینا..."
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm leading-relaxed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-center">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">امتیاز (۱ تا ۵)</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">امتیاز (از ۵ ستاره)</label>
|
||||||
<select
|
<div className="flex items-center gap-2">
|
||||||
value={formData.rating}
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
|
<button
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-bold bg-white"
|
type="button"
|
||||||
|
key={star}
|
||||||
|
onClick={() => setFormData({ ...formData, rating: star })}
|
||||||
|
className="p-1 cursor-pointer"
|
||||||
>
|
>
|
||||||
{[5, 4, 3, 2, 1].map(r => (
|
<Star
|
||||||
<option key={r} value={r}>{r} ستاره</option>
|
className={`w-6 h-6 ${
|
||||||
|
star <= formData.rating ? 'fill-amber-400 text-amber-400' : 'text-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</select>
|
<span className="text-sm font-bold text-gray-500 mr-2">({formData.rating} از ۵)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -345,18 +384,8 @@ export default function TestimonialsManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-2 grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||||
<label className="flex items-center justify-between p-3 bg-gray-50 rounded-xl border border-gray-200 cursor-pointer">
|
<label className="flex items-center justify-between p-3 bg-purple-50 rounded-xl border border-purple-200 cursor-pointer">
|
||||||
<span className="text-xs font-bold text-gray-700">نمایش ویژه (Featured)</span>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isFeatured}
|
|
||||||
onChange={(e) => setFormData({ ...formData, isFeatured: e.target.checked })}
|
|
||||||
className="rounded text-purple-600 focus:ring-purple-500"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center justify-between p-3 bg-gray-50 rounded-xl border border-gray-200 cursor-pointer">
|
|
||||||
<span className="text-xs font-bold text-gray-700">وضعیت انتشار (Active)</span>
|
<span className="text-xs font-bold text-gray-700">وضعیت انتشار (Active)</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -367,7 +396,7 @@ export default function TestimonialsManager() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="flex items-center justify-between p-3 bg-blue-50 rounded-xl border border-blue-200 cursor-pointer">
|
<label className="flex items-center justify-between p-3 bg-blue-50 rounded-xl border border-blue-200 cursor-pointer">
|
||||||
<span className="text-xs font-bold text-blue-700">🏠 صفحه اصلی</span>
|
<span className="text-xs font-bold text-blue-700">نمایش در خانه</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.showOnHome}
|
checked={formData.showOnHome}
|
||||||
@ -376,29 +405,12 @@ export default function TestimonialsManager() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSaving}
|
|
||||||
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isSaving && <Spinner size="sm" />}
|
|
||||||
ذخیره تغییرات
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
{deleteTargetId && (
|
{deleteTargetId && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
|
||||||
|
|
||||||
export interface WholesaleRequest {
|
export interface WholesaleRequest {
|
||||||
id: string;
|
id: string;
|
||||||
@ -59,10 +61,10 @@ export default function WholesaleApplications() {
|
|||||||
if (!rejectTargetId) return;
|
if (!rejectTargetId) return;
|
||||||
try {
|
try {
|
||||||
await api.put(`/wholesale/reject/${rejectTargetId}`);
|
await api.put(`/wholesale/reject/${rejectTargetId}`);
|
||||||
toast.success('درخواست رد شد.');
|
toast.success('درخواست همکاری رد شد.');
|
||||||
fetchRequestsData();
|
fetchRequestsData();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('خطا در رد درخواست');
|
toast.error('خطا در رد درخواست همکاری');
|
||||||
} finally {
|
} finally {
|
||||||
setRejectTargetId(null);
|
setRejectTargetId(null);
|
||||||
}
|
}
|
||||||
@ -70,20 +72,24 @@ export default function WholesaleApplications() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 font-vazir" dir="rtl">
|
<div className="p-6 font-vazir" dir="rtl">
|
||||||
<h1 className="text-2xl font-bold mb-2 text-gray-800">بررسی درخواستهای ثبتنام همکاران B2B (کلینیکها و داروخانهها)</h1>
|
<div className="flex justify-between items-center mb-6">
|
||||||
<p className="text-sm text-gray-500 mb-6">استعلام شماره پروانه نظام دامپزشکی و اعطای دسترسی خرید عمده با تخفیف ۳۰٪ کاتالوگ Canina</p>
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">درخواستهای ارتقا به همکار B2B</h1>
|
||||||
|
<p className="text-gray-500 text-sm mt-1">تایید یا رد درخواستهای کاربران عادی برای خرید عمده و دسترسی به تخفیفهای تجاری</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-12 text-gray-500 font-bold">در حال بارگذاری درخواستها...</div>
|
<div className="text-center py-12 text-gray-500 font-bold">در حال بارگذاری درخواستها...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white rounded-2xl border border-gray-200 overflow-hidden shadow-sm">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||||
<table className="w-full text-right">
|
<table className="w-full text-right border-collapse">
|
||||||
<thead className="bg-gray-50 border-b border-gray-200 text-xs font-bold text-gray-500">
|
<thead>
|
||||||
<tr>
|
<tr className="bg-gray-50/50 border-b border-gray-100 text-xs text-gray-500">
|
||||||
<th className="px-6 py-4">کاربر / ایمیل</th>
|
<th className="px-6 py-4 font-bold">نام و اطلاعات متقاضی</th>
|
||||||
<th className="px-6 py-4">نقش فعلی</th>
|
<th className="px-6 py-4 font-bold">وضعیت فعلی</th>
|
||||||
<th className="px-6 py-4">تاریخ ثبتنام</th>
|
<th className="px-6 py-4 font-bold">تاریخ درخواست</th>
|
||||||
<th className="px-6 py-4 text-center">عملیات تایید همکار</th>
|
<th className="px-6 py-4 font-bold text-center">عملیات</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100 text-sm">
|
<tbody className="divide-y divide-gray-100 text-sm">
|
||||||
@ -107,21 +113,26 @@ export default function WholesaleApplications() {
|
|||||||
<td className="px-6 py-4 text-xs text-gray-500 font-mono">
|
<td className="px-6 py-4 text-xs text-gray-500 font-mono">
|
||||||
{new Date(u.createdAt).toLocaleDateString('fa-IR')}
|
{new Date(u.createdAt).toLocaleDateString('fa-IR')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-center space-x-2 space-x-reverse">
|
<td className="px-6 py-4 text-center">
|
||||||
|
<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>
|
||||||
))
|
))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user