feat: add doctor profile page with sidebar integration and routing

This commit is contained in:
hamed
2026-06-11 19:06:59 +03:30
parent 704075770b
commit 277922d4ae
7 changed files with 347 additions and 76 deletions
+2
View File
@@ -28,6 +28,7 @@ import BlogFormPage from './pages/BlogFormPage';
import SecretariesPage from './pages/SecretariesPage';
import MyClinicPage from './pages/MyClinicPage';
import SettingsPage from './pages/SettingsPage';
import DoctorProfilePage from './pages/DoctorProfilePage';
// ── Guards ──────────────────────────────────────────────────────────────────
@@ -132,6 +133,7 @@ export default function App() {
<Route path="doctors" element={<RoleRoute roles={['admin']}><DoctorsPage /></RoleRoute>} />
<Route path="doctors/new" element={<RoleRoute roles={['admin']}><DoctorFormPage /></RoleRoute>} />
<Route path="doctors/:uuid" element={<RoleRoute roles={['admin', 'doctor']}><DoctorDetailPage /></RoleRoute>} />
<Route path="profile" element={<RoleRoute roles={['doctor']}><DoctorProfilePage /></RoleRoute>} />
</Route>
<Route path="*" element={<Navigate to="/admin/dashboard" replace />} />
@@ -14,6 +14,7 @@ import {
KeyIcon,
StarIcon,
TagIcon,
UserCircleIcon,
UserGroupIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
@@ -92,6 +93,12 @@ function buildSections(primaryRole: string | null, dbUuid: string | null): Secti
{ to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' },
],
},
{
label: 'پروفایل',
items: [
{ to: '/admin/profile', icon: UserCircleIcon, label: 'پروفایل من' },
],
},
{
label: 'مدیریت',
items: [
+57 -38
View File
@@ -1776,12 +1776,14 @@ type EditForm = z.infer<typeof editSchema>;
// ── Main Page ──────────────────────────────────────────────────────────────
export default function DoctorDetailPage() {
const { uuid } = useParams<{ uuid: string }>();
export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfile?: boolean }) {
const { uuid: paramUuid } = useParams<{ uuid: string }>();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const qc = useQueryClient();
const primaryRole = useAuthStore(s => s.primaryRole);
const dbUuid = useAuthStore(s => s.dbUuid);
const uuid = isOwnProfile ? (dbUuid ?? undefined) : paramUuid;
const [editOpen, setEditOpen] = useState(searchParams.get('edit') === '1');
const [deleteOpen, setDeleteOpen] = useState(false);
@@ -1967,12 +1969,25 @@ export default function DoctorDetailPage() {
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
<button onClick={() => navigate('/admin/doctors')}
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
<ArrowRightIcon className="w-4 h-4" />پزشکان
</button>
<span>/</span>
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل پزشک</span>
{isOwnProfile ? (
<>
<button onClick={() => navigate('/admin/dashboard')}
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
<ArrowRightIcon className="w-4 h-4" />داشبورد
</button>
<span>/</span>
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل من</span>
</>
) : (
<>
<button onClick={() => navigate('/admin/doctors')}
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
<ArrowRightIcon className="w-4 h-4" />پزشکان
</button>
<span>/</span>
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل پزشک</span>
</>
)}
</div>
{/* Profile Card */}
@@ -2026,39 +2041,43 @@ export default function DoctorDetailPage() {
<PencilIcon className="w-4 h-4" />ویرایش
</button>
{/* Toggle active button — styled by current state */}
<button
onClick={() => setToggleConfirm(true)}
disabled={toggleMut.isPending}
className={`inline-flex items-center gap-1.5 text-sm font-medium px-3 h-10 rounded-xl border transition-colors disabled:opacity-50 ${
doctor.active
? 'border-orange-300 dark:border-orange-500/40 text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-500/10 hover:bg-orange-100 dark:hover:bg-orange-500/20'
: 'border-emerald-300 dark:border-emerald-500/40 text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20'
}`}
>
{toggleMut.isPending
? <span className="w-3.5 h-3.5 border-2 border-current/30 border-t-current rounded-full animate-spin" />
: doctor.active
? <XCircleIcon className="w-4 h-4" />
: <CheckCircleIcon className="w-4 h-4" />
}
{doctor.active ? 'غیرفعال کردن' : 'فعال کردن'}
</button>
{/* Toggle active / Delete — فقط ادمین */}
{!isOwnProfile && (
<>
<button
onClick={() => setToggleConfirm(true)}
disabled={toggleMut.isPending}
className={`inline-flex items-center gap-1.5 text-sm font-medium px-3 h-10 rounded-xl border transition-colors disabled:opacity-50 ${
doctor.active
? 'border-orange-300 dark:border-orange-500/40 text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-500/10 hover:bg-orange-100 dark:hover:bg-orange-500/20'
: 'border-emerald-300 dark:border-emerald-500/40 text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20'
}`}
>
{toggleMut.isPending
? <span className="w-3.5 h-3.5 border-2 border-current/30 border-t-current rounded-full animate-spin" />
: doctor.active
? <XCircleIcon className="w-4 h-4" />
: <CheckCircleIcon className="w-4 h-4" />
}
{doctor.active ? 'غیرفعال کردن' : 'فعال کردن'}
</button>
<div className="relative" data-dmenu>
<button data-dmenu onClick={() => setMenuOpen(!menuOpen)}
className="cp-btn-secondary h-10 w-10 flex items-center justify-center px-0">
<EllipsisVerticalIcon className="w-4 h-4" />
</button>
{menuOpen && (
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-slate-200 dark:border-gray-700 py-1.5 animate-scale-in" data-dmenu>
<button onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10">
<TrashIcon className="w-4 h-4 shrink-0" />حذف این پزشک
<div className="relative" data-dmenu>
<button data-dmenu onClick={() => setMenuOpen(!menuOpen)}
className="cp-btn-secondary h-10 w-10 flex items-center justify-center px-0">
<EllipsisVerticalIcon className="w-4 h-4" />
</button>
{menuOpen && (
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-slate-200 dark:border-gray-700 py-1.5 animate-scale-in" data-dmenu>
<button onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10">
<TrashIcon className="w-4 h-4 shrink-0" />حذف این پزشک
</button>
</div>
)}
</div>
)}
</div>
</>
)}
</div>
</div>
</div>
+5
View File
@@ -0,0 +1,5 @@
import DoctorDetailPage from './DoctorDetailPage';
export default function DoctorProfilePage() {
return <DoctorDetailPage isOwnProfile />;
}