feat(admin): standardize Buttons and Modals in Ingredients, Doctors, Prescriptions, Pets, B2B, and Wholesale managers

This commit is contained in:
parsa aghaei 2026-08-23 10:12:47 +03:30
parent a92cd7fc85
commit 16516e7003
8 changed files with 705 additions and 626 deletions

View File

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

View File

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

View File

@ -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}

View File

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

View File

@ -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="حذف حیوان خانگی"

View File

@ -5,8 +5,11 @@ import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import ImagePreviewModal from '../components/ui/ImagePreviewModal'; import ImagePreviewModal from '../components/ui/ImagePreviewModal';
import Badge from '../components/ui/Badge'; import Badge from '../components/ui/Badge';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { Prescription } from '../types/admin'; import type { Prescription } from '../types/admin';
interface ProductItem { interface ProductItem {
id: string; id: string;
name: string; name: string;
@ -243,12 +246,13 @@ export default function PrescriptionsManager() {
)} )}
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<button <Button
variant="primary"
size="xs"
onClick={() => openReviewModal(rx)} onClick={() => openReviewModal(rx)}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-bold transition-all shadow-xs cursor-pointer"
> >
بررسی و تجویز بررسی و تجویز
</button> </Button>
</td> </td>
</tr> </tr>
); );
@ -261,27 +265,36 @@ export default function PrescriptionsManager() {
{/* Review Modal */} {/* Review Modal */}
{selectedRx && ( {selectedRx && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir" dir="rtl"> <Modal
<div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-gray-100 flex flex-col"> isOpen={!!selectedRx}
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50 sticky top-0 bg-white z-10"> onClose={() => setSelectedRx(null)}
<div className="flex items-center gap-3"> title="بررسی و ثبت نظر تخصصی نسخه"
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center"> icon={FileText}
<FileText className="w-5 h-5" /> maxWidth="2xl"
</div> footer={
<div> <div className="flex justify-end gap-2.5 w-full">
<h3 className="text-lg font-bold text-gray-900">بررسی و ثبت نظر تخصصی نسخه</h3> <Button
<p className="text-xs text-gray-500">کد نسخه: {selectedRx.id}</p> variant="secondary"
</div> size="sm"
</div> type="button"
<button
onClick={() => setSelectedRx(null)} onClick={() => setSelectedRx(null)}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
> >
<X className="w-5 h-5" /> انصراف
</button> </Button>
<Button
variant="primary"
size="sm"
type="submit"
form="prescriptionForm"
isLoading={isSubmitting}
>
ثبت نهایی و ارسال پیامک
</Button>
</div> </div>
}
<form onSubmit={handleReviewSubmit} className="p-6 space-y-5"> >
<div className="space-y-5 text-right font-vazir">
<form id="prescriptionForm" onSubmit={handleReviewSubmit} className="space-y-5">
{/* Pet & User Details Card */} {/* Pet & User Details Card */}
<div className="p-4 bg-gray-50 rounded-2xl border border-gray-200 text-xs space-y-2"> <div className="p-4 bg-gray-50 rounded-2xl border border-gray-200 text-xs space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -295,58 +308,68 @@ export default function PrescriptionsManager() {
</div> </div>
)} )}
{selectedRx.notes && ( {selectedRx.notes && (
<div className="pt-2 border-t border-gray-200 text-gray-700"> <div className="pt-2 border-t border-gray-200 text-gray-500">
<strong className="block mb-0.5 text-gray-900">یادداشت کاربر:</strong> <span className="font-bold text-gray-700">توضیحات کاربر: </span>
{selectedRx.notes} {selectedRx.notes}
</div> </div>
)} )}
<div 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}

View File

@ -5,8 +5,11 @@ import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner'; import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal'; import ConfirmModal from '../components/ui/ConfirmModal';
import MediaSelector from '../components/ui/MediaSelector'; import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { Testimonial } from '../types/admin'; import type { Testimonial } from '../types/admin';
export default function TestimonialsManager() { export default function TestimonialsManager() {
const [testimonials, setTestimonials] = useState<Testimonial[]>([]); const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -148,13 +151,14 @@ export default function TestimonialsManager() {
</h2> </h2>
<p className="text-gray-500 font-medium mt-1">مدیریت توصیه نامهها و نظرات تایید شده جهت نمایش در صفحه اصلی و گالری دامپزشکان</p> <p className="text-gray-500 font-medium mt-1">مدیریت توصیه نامهها و نظرات تایید شده جهت نمایش در صفحه اصلی و گالری دامپزشکان</p>
</div> </div>
<button <Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()} onClick={() => openModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
> >
<Plus className="w-5 h-5" />
افزودن نظر جدید افزودن نظر جدید
</button> </Button>
</div> </div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
@ -199,45 +203,51 @@ export default function TestimonialsManager() {
{item.roleTitle && <span className="text-xs text-purple-600 font-medium">{item.roleTitle}</span>} {item.roleTitle && <span className="text-xs text-purple-600 font-medium">{item.roleTitle}</span>}
</div> </div>
</td> </td>
<td className="py-4 px-6 text-gray-600 text-sm max-w-md line-clamp-2"> <td className="py-4 px-6">
{item.content} <p className="text-gray-600 text-sm line-clamp-2 leading-relaxed max-w-md">{item.content}</p>
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-1 text-amber-500"> <div className="flex items-center gap-1 text-amber-500">
{Array.from({ length: item.rating || 5 }).map((_, i) => ( {Array.from({ length: 5 }).map((_, i) => (
<Star key={i} className="w-4 h-4 fill-amber-400" /> <Star
key={i}
className={`w-3.5 h-3.5 ${i < item.rating ? 'fill-amber-400 text-amber-400' : 'text-gray-200'}`}
/>
))} ))}
</div> </div>
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{item.isFeatured && ( <button
<span className="px-2 py-0.5 bg-amber-50 text-amber-700 text-[11px] font-bold rounded border border-amber-200"> onClick={() => handleToggleActive(item)}
ویژه (Featured) className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold transition-colors cursor-pointer ${
</span> item.isActive
)} ? 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
: 'bg-rose-50 text-rose-700 hover:bg-rose-100'
}`}
>
{item.isActive ? <CheckCircle2 className="w-3 h-3" /> : <XCircle className="w-3 h-3" />}
{item.isActive ? 'فعال' : 'غیرفعال'}
</button>
{(item as any).showOnHome && ( {(item as any).showOnHome && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-[11px] font-bold rounded border border-blue-200"> <span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs font-bold rounded-md">
🏠 صفحه اصلی خانه
</span>
)}
{item.isActive ? (
<span className="inline-flex items-center gap-0.5 text-green-600 text-xs font-bold">
<CheckCircle2 className="w-3.5 h-3.5" /> فعال
</span>
) : (
<span className="inline-flex items-center gap-0.5 text-gray-400 text-xs font-bold">
<XCircle className="w-3.5 h-3.5" /> غیرفعال
</span> </span>
)} )}
</div> </div>
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={() => openModal(item)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"> <button
onClick={() => openModal(item)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"
>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</button> </button>
<button onClick={() => setDeleteTargetId(item.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"> <button
onClick={() => setDeleteTargetId(item.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
>
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
@ -250,88 +260,117 @@ export default function TestimonialsManager() {
</div> </div>
</div> </div>
{/* Form Modal */} {/* Modal */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm"> <Modal
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200"> isOpen={isModalOpen}
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50"> onClose={() => setIsModalOpen(false)}
<h3 className="text-lg font-bold text-gray-900"> title={editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'}
{editingTestimonial ? 'ویرایش نظر' : 'افزودن نظر جدید'} icon={MessageSquareQuote}
</h3> maxWidth="2xl"
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors"> footer={
<X className="w-6 h-6" /> <div className="flex justify-end gap-2.5 w-full">
</button> <Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="testimonialForm"
isLoading={isSaving}
>
ذخیره تغییرات
</Button>
</div> </div>
}
<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

View File

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