diff --git a/assets/admin/components/layout/SettingsLayout.test.tsx b/assets/admin/components/layout/SettingsLayout.test.tsx index 163dac94..ca639536 100644 --- a/assets/admin/components/layout/SettingsLayout.test.tsx +++ b/assets/admin/components/layout/SettingsLayout.test.tsx @@ -1,45 +1,54 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import { screen, fireEvent } from '@testing-library/react'; import { renderWithProviders } from '../../test/utils'; -import SettingsLayout, { SETTINGS_MENU } from './SettingsLayout'; +import SettingsLayout, { menuForRole } from './SettingsLayout'; +import { useAuthStore } from '../../stores/authStore'; + +beforeEach(() => { + useAuthStore.setState({ primaryRole: 'doctor' }); +}); describe('SettingsLayout', () => { - it('renders every settings menu item and the section content', () => { + it('renders every menu item available to the role, plus the content', () => { renderWithProviders(
محتوای اشتراک
, ); - - for (const item of SETTINGS_MENU) { + for (const item of menuForRole('doctor')) { expect(screen.getByText(item.label)).toBeInTheDocument(); } expect(screen.getByText('محتوای اشتراک')).toBeInTheDocument(); }); it('marks the active item with aria-current=page', () => { - renderWithProviders( -
, - ); + renderWithProviders(
); const active = screen.getByText('خرید اشتراک').closest('a'); expect(active).toHaveAttribute('aria-current', 'page'); expect(active).toHaveAttribute('href', '/admin/subscription'); }); - it('renders every menu item as a navigable link', () => { - renderWithProviders( -
, - ); - // all items are now wired to a route + it('renders every visible item as a navigable link', () => { + renderWithProviders(
); expect(screen.getByText('مدیریت نوبت دهی').closest('a')).toHaveAttribute('href', '/admin/appointment-settings'); expect(screen.getByText('برچسب‌ها').closest('a')).toHaveAttribute('href', '/admin/tags-settings'); expect(screen.queryByText('به‌زودی')).not.toBeInTheDocument(); }); + it('hides role-specific items from other roles', () => { + // doctor sees مدیریت پزشک but not مدیریت مطب (clinic-only) + renderWithProviders(
); + expect(screen.getByText('مدیریت پزشک')).toBeInTheDocument(); + expect(screen.queryByText('مدیریت مطب')).not.toBeInTheDocument(); + + // clinic sees مدیریت مطب but not مدیریت پزشک / نوبت دهی + expect(menuForRole('clinic').map((i) => i.key)).toContain('clinic'); + expect(menuForRole('clinic').map((i) => i.key)).not.toContain('doctor'); + expect(menuForRole('clinic').map((i) => i.key)).not.toContain('appointment'); + }); + it('filters the menu by the search query', () => { - renderWithProviders( -
, - ); + renderWithProviders(
); fireEvent.change(screen.getByLabelText('جستجو در تنظیمات'), { target: { value: 'اشتراک' } }); expect(screen.getByText('خرید اشتراک')).toBeInTheDocument(); expect(screen.queryByText('خدمات')).not.toBeInTheDocument(); diff --git a/assets/admin/components/layout/SettingsLayout.tsx b/assets/admin/components/layout/SettingsLayout.tsx index 8060f123..82cd3c48 100644 --- a/assets/admin/components/layout/SettingsLayout.tsx +++ b/assets/admin/components/layout/SettingsLayout.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; +import { useAuthStore } from '../../stores/authStore'; import { CreditCardIcon, UserIcon, CalendarDaysIcon, BuildingOffice2Icon, WrenchScrewdriverIcon, BanknotesIcon, UsersIcon, ShieldCheckIcon, @@ -15,13 +16,15 @@ export type SettingsMenuItem = { label: string; icon: React.ElementType; to?: string; + /** when set, the item is only shown to these roles (omit = every role) */ + roles?: string[]; }; export const SETTINGS_MENU: SettingsMenuItem[] = [ { key: 'subscription', label: 'خرید اشتراک', icon: CreditCardIcon, to: '/admin/subscription' }, - { key: 'doctor', label: 'مدیریت پزشک', icon: UserIcon, to: '/admin/profile' }, - { key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/appointment-settings' }, - { key: 'clinic', label: 'مدیریت مطب', icon: BuildingOffice2Icon, to: '/admin/my-clinic' }, + { 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: 'services', label: 'خدمات', icon: WrenchScrewdriverIcon, to: '/admin/clinic-services' }, { key: 'payment', label: 'مدیریت پرداخت', icon: BanknotesIcon, to: '/admin/my-financial' }, { key: 'secretary', label: 'مدیریت منشی', icon: UsersIcon, to: '/admin/my-secretaries' }, @@ -31,6 +34,11 @@ export const SETTINGS_MENU: SettingsMenuItem[] = [ { key: 'account', label: 'حساب کاربری', icon: UserCircleIcon, to: '/admin/account-settings' }, ]; +/** Menu items visible to the given role (items without `roles` are shown to all). */ +export function menuForRole(role: string | null | undefined): SettingsMenuItem[] { + return SETTINGS_MENU.filter((i) => !i.roles || (role != null && i.roles.includes(role))); +} + // ── Shared item styling ────────────────────────────────────────────────────── function itemStyle(active: boolean, disabled: boolean): React.CSSProperties { return { @@ -82,9 +90,10 @@ function MenuRow({ item, active }: { item: SettingsMenuItem; active: boolean }) */ export default function SettingsLayout({ active, children }: { active: string; children: React.ReactNode }) { const [query, setQuery] = useState(''); + const primaryRole = useAuthStore((s) => s.primaryRole); const items = useMemo( - () => SETTINGS_MENU.filter((i) => i.label.includes(query.trim())), - [query], + () => menuForRole(primaryRole).filter((i) => i.label.includes(query.trim())), + [query, primaryRole], ); return ( diff --git a/assets/admin/pages/SettingsMenuPage.test.tsx b/assets/admin/pages/SettingsMenuPage.test.tsx index 47750d25..42b3b835 100644 --- a/assets/admin/pages/SettingsMenuPage.test.tsx +++ b/assets/admin/pages/SettingsMenuPage.test.tsx @@ -1,18 +1,25 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import { screen } from '@testing-library/react'; import { renderWithProviders } from '../test/utils'; import SettingsMenuPage from './SettingsMenuPage'; -import { SETTINGS_MENU } from '../components/layout/SettingsLayout'; +import { menuForRole } from '../components/layout/SettingsLayout'; +import { useAuthStore } from '../stores/authStore'; + +beforeEach(() => { + useAuthStore.setState({ primaryRole: 'doctor' }); +}); describe('SettingsMenuPage', () => { - it('lists every settings section', () => { + it('lists the settings sections available to the current role', () => { renderWithProviders(); - for (const item of SETTINGS_MENU) { + for (const item of menuForRole('doctor')) { expect(screen.getByText(item.label)).toBeInTheDocument(); } + // clinic-only section is not shown to a doctor + expect(screen.queryByText('مدیریت مطب')).not.toBeInTheDocument(); }); - it('links every implemented section', () => { + it('links every section to its route', () => { renderWithProviders(); expect(screen.getByText('خرید اشتراک').closest('a')).toHaveAttribute('href', '/admin/subscription'); expect(screen.getByText('مدیریت نوبت دهی').closest('a')).toHaveAttribute('href', '/admin/appointment-settings'); diff --git a/assets/admin/pages/SettingsMenuPage.tsx b/assets/admin/pages/SettingsMenuPage.tsx index 78c59f9b..94de88f3 100644 --- a/assets/admin/pages/SettingsMenuPage.tsx +++ b/assets/admin/pages/SettingsMenuPage.tsx @@ -1,16 +1,19 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { ChevronLeftIcon } from '@heroicons/react/24/outline'; -import { SETTINGS_MENU } from '../components/layout/SettingsLayout'; +import { menuForRole } from '../components/layout/SettingsLayout'; +import { useAuthStore } from '../stores/authStore'; /** * SettingsMenuPage — the settings landing list for doctor/clinic users. - * A full-width tappable list of settings sections (mobile-first, matches the - * Figma mobile design). Implemented sections link to their route; the rest - * render as disabled rows labelled "به‌زودی". On desktop the same list is shown; - * each section itself renders the desktop shell via SettingsLayout. + * A full-width tappable list of the settings sections available to the current + * role (mobile-first, matches the Figma mobile design). Each section links to + * its route; on desktop the same sections render the shell via SettingsLayout. */ export default function SettingsMenuPage() { + const primaryRole = useAuthStore((s) => s.primaryRole); + const items = menuForRole(primaryRole); + return (

تنظیمات

@@ -19,7 +22,7 @@ export default function SettingsMenuPage() { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', overflow: 'hidden', }}> - {SETTINGS_MENU.map((item, idx) => { + {items.map((item, idx) => { const Icon = item.icon; const disabled = !item.to; const rowStyle: React.CSSProperties = {