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 api from '../services/api';
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';
export default function B2BManager() {
const [activeTab, setActiveTab] = useState<'inquiries' | 'partners'>('inquiries');
@ -277,12 +282,13 @@ export default function B2BManager() {
)}
</td>
<td className="py-4 px-6">
<button
<Button
variant="primary"
size="xs"
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>
</tr>
))
@ -296,13 +302,14 @@ export default function B2BManager() {
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-bold text-gray-800">فهرست حسابهای تایید شده همکار</h3>
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
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 جدید
</button>
</Button>
</div>
<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 */}
{selectedInquiry && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h3 className="text-lg font-bold text-gray-900">
بررسی درخواست B2B ({selectedInquiry.companyName})
</h3>
<button onClick={() => setSelectedInquiry(null)} className="text-gray-400 hover:text-red-500 transition-colors">
<X className="w-6 h-6" />
</button>
<Modal
isOpen={!!selectedInquiry}
onClose={() => setSelectedInquiry(null)}
title={`بررسی درخواست B2B (${selectedInquiry.companyName})`}
icon={Building2}
maxWidth="lg"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setSelectedInquiry(null)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="inquiryForm"
isLoading={isInquirySubmitting}
>
بروزرسانی وضعیت
</Button>
</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><span className="font-bold text-gray-600">شخص رابط: </span>{selectedInquiry.contactName} ({selectedInquiry.phone})</div>
<div><span className="font-bold text-gray-600">پیام درخواست: </span>{selectedInquiry.message}</div>
<div><span className="text-gray-500">رابط: </span><span className="font-bold text-gray-800">{selectedInquiry.contactName}</span></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>
@ -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"
/>
</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>
</div>
</div>
</Modal>
)}
{/* Partner Account Setup Modal */}
{isPartnerModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h3 className="text-lg font-bold text-gray-900">
تعریف حساب همکار B2B جدید
</h3>
<button onClick={() => setIsPartnerModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<X className="w-6 h-6" />
</button>
<Modal
isOpen={isPartnerModalOpen}
onClose={() => setIsPartnerModalOpen(false)}
title="تعریف حساب همکار B2B جدید"
icon={Building2}
maxWidth="lg"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsPartnerModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="partnerForm"
isLoading={isPartnerSubmitting}
>
ایجاد حساب همکار
</Button>
</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>
<label className="block text-sm font-bold text-gray-700 mb-1">شناسه کاربر (User ID) *</label>
<input
@ -498,28 +525,11 @@ export default function B2BManager() {
<option value="tier3">سطح ۳ (۳۵٪ تخفیف)</option>
</select>
</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>
</div>
</div>
</Modal>
)}
</div>
);
}

View File

@ -1,6 +1,10 @@
import React, { useEffect, useState } from 'react';
import { Mail, MessageSquare } from 'lucide-react';
import toast from 'react-hot-toast';
import api from '../services/api';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface ContactSubmission {
id: string;
@ -153,16 +157,17 @@ export default function ContactSubmissions() {
</span>
</td>
<td className="p-4">
<button
<Button
variant="secondary"
size="xs"
onClick={() => {
setSelectedSub(sub);
setStatus(sub.status);
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>
</tr>
))}
@ -220,21 +225,45 @@ export default function ContactSubmissions() {
</div>
</div>
))}
<button
<Button
variant="primary"
size="sm"
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>
)}
{/* Modal for Submission Detail & Status Update */}
{selectedSub && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl p-6 max-w-lg w-full shadow-xl">
<h3 className="text-lg font-bold text-gray-900 mb-4">جزئیات پیام {selectedSub.name}</h3>
<Modal
isOpen={!!selectedSub}
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><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>}
@ -268,24 +297,11 @@ export default function ContactSubmissions() {
className="w-full px-3 py-2 border rounded-lg text-sm"
/>
</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>
</Modal>
)}
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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