feat: implement OTP login flow and enhance role-based access control

- Added OTP login functionality in driver.mjs to handle user authentication with a fixed code in dev environment.
- Enhanced RoleRoute component in App.tsx to support clinic-scoped doctor roles and permissions.
- Updated ClinicDoctorsManager component to include pagination and search functionality for better user experience.
- Refactored tests for ClinicDoctorsManager to cover new features and ensure proper API mocking.
- Adjusted permissions in settingsMenu.ts and PracticeDomainSettingsPage.tsx to align with updated backend requirements.
- Created RoleRoute.test.tsx to validate role-based access logic for different user scenarios.
This commit is contained in:
hamed
2026-08-09 12:41:11 +03:30
parent cfeb447645
commit 60ccd5cc1d
8 changed files with 515 additions and 121 deletions
+12 -13
View File
@@ -4,7 +4,7 @@ import { PencilIcon } from '@heroicons/react/24/outline';
import { useAuthStore } from '../stores/authStore';
import { usePermissions } from '../hooks/usePermissions';
import SettingsLayout from '../components/layout/SettingsLayout';
import BackButton from '../components/ui/BackButton';
import PageHeader from '../components/ui/PageHeader';
import ClinicDoctorsManager from '../components/ClinicDoctorsManager';
/**
@@ -45,18 +45,17 @@ function ClinicDoctorsContent() {
return (
<div className="fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap)' }}>
<div>
<BackButton fallback="/admin/settings-menu" />
</div>
<div className="card-title-row">
<div>
<h1 className="section-title">پزشکان کلینیک</h1>
<div className="muted">مدیریت پزشکان و دعوتنامههای کلینیک</div>
</div>
<Link className="btn ghost sm" to={`/admin/clinics/${clinicUuid}`}>
<PencilIcon style={{ width: 15, height: 15 }} /> ویرایش اطلاعات کلینیک
</Link>
</div>
{/* عنوان و بازگشت و اکشن هر سه در PageHeader — همان قاعدهٔ بقیهٔ صفحات پنل. */}
<PageHeader
title="پزشکان کلینیک"
description="مدیریت پزشکان و دعوت‌نامه‌های کلینیک"
backTo="/admin/settings-menu"
action={
<Link className="btn secondary sm" to={`/admin/clinics/${clinicUuid}`}>
<PencilIcon style={{ width: 15, height: 15 }} /> ویرایش اطلاعات کلینیک
</Link>
}
/>
<ClinicDoctorsManager
clinicUuid={clinicUuid}
@@ -5,6 +5,7 @@ import { api, ApiError, type ApiResponse } from '../lib/api';
import SettingsLayout from '../components/layout/SettingsLayout';
import SearchableSelect from '../components/ui/SearchableSelect';
import { useAuthStore } from '../stores/authStore';
import { usePermissions } from '../hooks/usePermissions';
import type { PracticeDomain } from '../types';
/**
@@ -18,6 +19,8 @@ export default function PracticeDomainSettingsPage() {
const qc = useQueryClient();
// محیط جاری با `db_uuid` شناخته می‌شود؛ همان uuid کلینیک است.
const clinicUuid = useAuthStore((s) => s.context?.db_uuid ?? null);
const { can } = usePermissions();
const canUpdate = can('clinic_info', 'update');
const { data: domainsData, isLoading } = useQuery({
queryKey: ['practice-domains'],
@@ -86,10 +89,18 @@ export default function PracticeDomainSettingsPage() {
</span>
)}
{/* پزشکِ عضو ممکن است «مشاهده» داشته باشد و «ویرایش» نه؛ دکمهٔ فعالی که
حتماً ۴۰۳ می‌گیرد، خطای کاربر نیست — خطای رابط است. */}
{!canUpdate && (
<span className="muted" style={{ fontSize: 12 }}>
برای تغییر حوزهٔ فعالیت، دسترسی «ویرایش اطلاعات کلینیک» لازم است.
</span>
)}
<button
type="button"
className="btn primary"
disabled={save.isPending || !clinicUuid}
disabled={save.isPending || !clinicUuid || !canUpdate}
onClick={() => save.mutate(selected)}
style={{ justifySelf: 'start' }}
>