feat: refactor clinic management into a dedicated settings tab

- Removed MyClinicPage and redirected its functionality to a new ClinicDoctorsPage.
- Created ClinicDoctorsManager component for managing doctors and invitations within the settings layout.
- Updated backend permissions to allow clinic owners to detach doctors, alongside admins.
- Adjusted API documentation to reflect new permission structure.
- Updated tests to cover new functionality and permissions.
- Modified sidebar and settings menu to reflect the new structure and role-based visibility.
This commit is contained in:
hamed
2026-07-17 21:56:27 +03:30
parent 385b81fae0
commit 6ab7ed38b8
15 changed files with 731 additions and 318 deletions
@@ -1,6 +1,7 @@
import React, { useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { SearchHeaderP } from '../../pages/subscriptionIcons';
import { useAuthStore } from '../../stores/authStore';
/**
* Settings sub-navigation for the subscription page — item list and order copied
@@ -12,28 +13,32 @@ import { SearchHeaderP } from '../../pages/subscriptionIcons';
* This list is intentionally separate from SETTINGS_MENU (SettingsLayout) so the
* other settings pages are not affected.
*/
type NavItem = { key: string; label: string; to?: string };
/** `roles`: when set, the item is only shown to those roles (omit = every role). */
type NavItem = { key: string; label: string; to?: string; roles?: string[] };
const NAV_ITEMS: NavItem[] = [
{ key: 'subscription', label: 'خرید اشتراک', to: '/admin/subscription' },
{ key: 'payment', label: 'مدیریت پرداخت', to: '/admin/my-financial' },
{ key: 'appointment', label: 'مدیریت نوبت دهی', to: '/admin/appointment-settings' },
{ key: 'insurance', label: 'مدیریت بیمه', to: '/admin/insurance-pricing' },
{ key: 'discounts', label: 'مدیریت تخفیف‌ها', to: '/admin/discounts' },
{ key: 'tags', label: 'تگ ها', to: '/admin/tags-settings' },
{ key: 'sms', label: 'پیامک ها', to: '/admin/sms-wallet' },
{ key: 'clinic', label: 'مدیریت مطب', to: '/admin/my-clinic' },
{ key: 'secretary', label: 'مدیریت منشی', to: '/admin/my-secretaries' },
{ key: 'staff', label: 'پرسنل', to: '/admin/staff' },
{ key: 'account', label: 'حساب کاربری', to: '/admin/account-settings' },
{ key: 'security', label: 'تنظیمات' },
{ key: 'subscription', label: 'خرید اشتراک', to: '/admin/subscription' },
{ key: 'payment', label: 'مدیریت پرداخت', to: '/admin/my-financial' },
{ key: 'appointment', label: 'مدیریت نوبت دهی', to: '/admin/appointment-settings', roles: ['doctor'] },
{ key: 'insurance', label: 'مدیریت بیمه', to: '/admin/insurance-pricing' },
{ key: 'discounts', label: 'مدیریت تخفیف‌ها', to: '/admin/discounts', roles: ['doctor', 'clinic'] },
{ key: 'tags', label: 'تگ ها', to: '/admin/tags-settings' },
{ key: 'sms', label: 'پیامک ها', to: '/admin/sms-wallet' },
{ key: 'clinic-doctors', label: 'پزشکان کلینیک', to: '/admin/settings/clinic-doctors', roles: ['clinic'] },
{ key: 'secretary', label: 'مدیریت منشی', to: '/admin/my-secretaries' },
{ key: 'staff', label: 'پرسنل', to: '/admin/staff' },
{ key: 'account', label: 'حساب کاربری', to: '/admin/account-settings' },
{ key: 'security', label: 'تنظیمات' },
];
export default function PurchaseSubscriptionSidebar({ active }: { active: string }) {
const [query, setQuery] = useState('');
const primaryRole = useAuthStore((s) => s.primaryRole);
const items = useMemo(
() => NAV_ITEMS.filter((i) => i.label.includes(query.trim())),
[query],
() => NAV_ITEMS
.filter((i) => !i.roles || (primaryRole != null && i.roles.includes(primaryRole)))
.filter((i) => i.label.includes(query.trim())),
[query, primaryRole],
);
return (
@@ -34,10 +34,17 @@ describe('SettingsLayout', () => {
expect(screen.queryByText('مدیریت پزشک')).not.toBeInTheDocument();
});
it('shows the same menu to every role (ungated, mirroring tauri)', () => {
// the desktop sidebar is not role-gated: مدیریت مطب shows even for a doctor
it('role-gates the desktop sidebar: a doctor does not see the clinic-doctors tab', () => {
renderWithProviders(<SettingsLayout active="subscription"><div /></SettingsLayout>);
expect(screen.getByText('مدیریت مطب')).toBeInTheDocument();
expect(screen.queryByText('پزشکان کلینیک')).not.toBeInTheDocument();
// the removed 'مدیریت مطب' tab must be gone for everyone
expect(screen.queryByText('مدیریت مطب')).not.toBeInTheDocument();
});
it('shows the clinic-doctors tab to a clinic owner', () => {
useAuthStore.setState({ primaryRole: 'clinic' });
renderWithProviders(<SettingsLayout active="clinic-doctors"><div /></SettingsLayout>);
expect(screen.getByText('پزشکان کلینیک').closest('a')).toHaveAttribute('href', '/admin/settings/clinic-doctors');
});
it('filters the menu by the search query', () => {
@@ -49,8 +56,10 @@ describe('SettingsLayout', () => {
it('menuForRole still role-gates the mobile settings list', () => {
// SettingsMenuPage (mobile) keeps using menuForRole / SETTINGS_MENU
expect(menuForRole('clinic').map((i) => i.key)).toContain('clinic');
expect(menuForRole('clinic').map((i) => i.key)).toContain('clinic-doctors');
expect(menuForRole('clinic').map((i) => i.key)).not.toContain('doctor');
expect(menuForRole('clinic').map((i) => i.key)).not.toContain('appointment');
// a plain doctor must not get the clinic-doctors management tab
expect(menuForRole('doctor').map((i) => i.key)).not.toContain('clinic-doctors');
});
});
@@ -23,7 +23,7 @@ export const SETTINGS_MENU: SettingsMenuItem[] = [
{ key: 'subscription', label: 'خرید اشتراک', icon: CreditCardIcon, to: '/admin/subscription' },
{ key: 'doctor', label: 'مدیریت پزشک', icon: UserIcon, to: '/admin/profile', roles: ['doctor'] },
{ key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/appointment-settings', roles: ['doctor'] },
{ key: 'clinic', label: 'مدیریت مطب', icon: BuildingOffice2Icon, to: '/admin/my-clinic', roles: ['clinic'] },
{ key: 'clinic-doctors', label: 'پزشکان کلینیک', icon: BuildingOffice2Icon, to: '/admin/settings/clinic-doctors', roles: ['clinic'] },
{ key: 'payment', label: 'مدیریت پرداخت', icon: BanknotesIcon, to: '/admin/my-financial' },
{ key: 'secretary', label: 'مدیریت منشی', icon: UsersIcon, to: '/admin/my-secretaries' },
{ key: 'staff', label: 'پرسنل', icon: UserPlusIcon, to: '/admin/staff' },
+1 -1
View File
@@ -203,7 +203,7 @@ function buildSections(
if (primaryRole === "clinic") {
const clinicTo = dbUuid
? `/admin/clinics/${dbUuid}`
: "/admin/my-clinic";
: "/admin/settings/clinic-doctors";
return [
{
label: "عمومی",