From 221dce29c3a46dffb98961b28cbb93826b0e7917 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 23 Jul 2026 18:11:41 +0330 Subject: [PATCH] =?UTF-8?q?fix(secretary):=20route=20=C2=AB=D9=86=D9=88?= =?UTF-8?q?=D8=A8=D8=AA=E2=80=8C=D8=AF=D9=87=DB=8C=C2=BB=20settings=20to?= =?UTF-8?q?=20the=20scope-correct=20variant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A clinic-scoped secretary opening «مدیریت نوبت دهی» hit 404s (/api/v1/doctor/{clinicUuid}, available-locations, weekly-schedule): the permission-only secretary filter ignored each item's `roles`, so BOTH appointment-settings variants (doctor → /admin/appointment-settings, clinic → /admin/settings/appointment-settings) showed. Clicking the doctor variant landed on the personal page, which has no doctor uuid for a clinic secretary and fell back to the clinic uuid — not a doctor → 404. Make the secretary settings filter scope-aware in both navs (PurchaseSubscriptionSidebar + menuForRole): a role-variant item is kept only when its `roles` matches the secretary's context scope (clinic→'clinic', else 'doctor'). The clinic page already threads clinic_uuid through ScheduleSection, so once routed correctly the flow works end-to-end. Test: appointment variant resolves to the clinic route under clinic scope. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../PurchaseSubscriptionSidebar.test.tsx | 14 +++++++++++++ .../layout/PurchaseSubscriptionSidebar.tsx | 21 ++++++++++++------- .../components/layout/SettingsLayout.tsx | 6 +++++- assets/admin/pages/SettingsMenuPage.tsx | 3 ++- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/assets/admin/components/layout/PurchaseSubscriptionSidebar.test.tsx b/assets/admin/components/layout/PurchaseSubscriptionSidebar.test.tsx index 46e47a38..885bdb5b 100644 --- a/assets/admin/components/layout/PurchaseSubscriptionSidebar.test.tsx +++ b/assets/admin/components/layout/PurchaseSubscriptionSidebar.test.tsx @@ -38,6 +38,20 @@ describe('PurchaseSubscriptionSidebar — settings menu', () => { useAuthStore.setState({ primaryRole: null, context: null } as any); }); + it('«مدیریت نوبت دهی» را با scope منشی به واریانتِ درست لینک می‌کند', () => { + // منشیِ کلینیک → صفحهٔ کلینیک (نه صفحهٔ پزشکِ مستقل که uuid کلینیک را + // به‌جای doctor می‌فرستد و ۴۰۴ می‌گرفت). + useAuthStore.setState({ + primaryRole: 'secretary', + context: { scope: 'clinic', permissions: { resources: { appointment_settings: { view: true } } } }, + } as any); + renderWithProviders(, { route: '/admin/settings/appointment-settings' }); + + const link = screen.getByRole('link', { name: 'مدیریت نوبت دهی' }); + expect(link).toHaveAttribute('href', '/admin/settings/appointment-settings'); + useAuthStore.setState({ primaryRole: null, context: null } as any); + }); + // regression: the settings menu must stay visible on mobile (was `hidden lg:block`, // which dropped the whole menu below the lg breakpoint → no settings nav on phones). it('is not hidden on mobile', () => { diff --git a/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx b/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx index 21dbf7a6..92648774 100644 --- a/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx +++ b/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx @@ -49,17 +49,24 @@ const NAV_ITEMS: NavItem[] = [ export default function PurchaseSubscriptionSidebar({ active }: { active: string }) { const [query, setQuery] = useState(''); const primaryRole = useAuthStore((s) => s.primaryRole); + const scope = useAuthStore((s) => s.context?.scope); const { can } = usePermissions(); const items = useMemo( () => NAV_ITEMS - // منشی: بر اساس مجوز، نه نقش. آیتمِ بدونِ perm/alwaysOpen برای منشی پنهان است. - .filter((i) => - primaryRole === 'secretary' - ? (i.alwaysOpen || (i.perm ? can(i.perm[0], i.perm[1]) : false)) - : (!i.roles || (primaryRole != null && i.roles.includes(primaryRole))), - ) + // منشی: بر اساس مجوز، نه نقش. آیتمِ بدونِ perm/alwaysOpen پنهان است. برای + // آیتم‌هایی که واریانتِ نقشی دارند (مثلِ «نوبت‌دهی» با doctor vs clinic)، با + // scope منشی تطبیق داده می‌شود تا واریانتِ درست انتخاب شود. + .filter((i) => { + if (primaryRole !== 'secretary') { + return !i.roles || (primaryRole != null && i.roles.includes(primaryRole)); + } + if (i.alwaysOpen) return true; + if (!i.perm || !can(i.perm[0], i.perm[1])) return false; + if (i.roles) return i.roles.includes(scope === 'clinic' ? 'clinic' : 'doctor'); + return true; + }) .filter((i) => i.label.includes(query.trim())), - [query, primaryRole, can], + [query, primaryRole, scope, can], ); return ( diff --git a/assets/admin/components/layout/SettingsLayout.tsx b/assets/admin/components/layout/SettingsLayout.tsx index fd5725bd..fd986793 100644 --- a/assets/admin/components/layout/SettingsLayout.tsx +++ b/assets/admin/components/layout/SettingsLayout.tsx @@ -46,11 +46,15 @@ export const SETTINGS_MENU: SettingsMenuItem[] = [ export function menuForRole( role: string | null | undefined, can?: (resource: string, action: string) => boolean, + scope?: string | null, ): SettingsMenuItem[] { return SETTINGS_MENU.filter((i) => { if (role === 'secretary') { if (i.alwaysOpen) return true; - return i.perm && can ? can(i.perm[0], i.perm[1]) : false; + if (!i.perm || !can || !can(i.perm[0], i.perm[1])) return false; + // واریانتِ نقشی (نوبت‌دهی/پزشکان کلینیک) را با scope منشی تطبیق بده. + if (i.roles) return i.roles.includes(scope === 'clinic' ? 'clinic' : 'doctor'); + return true; } return !i.roles || (role != null && i.roles.includes(role)); }); diff --git a/assets/admin/pages/SettingsMenuPage.tsx b/assets/admin/pages/SettingsMenuPage.tsx index e543eb38..3f2b2e15 100644 --- a/assets/admin/pages/SettingsMenuPage.tsx +++ b/assets/admin/pages/SettingsMenuPage.tsx @@ -13,8 +13,9 @@ import { usePermissions } from '../hooks/usePermissions'; */ export default function SettingsMenuPage() { const primaryRole = useAuthStore((s) => s.primaryRole); + const scope = useAuthStore((s) => s.context?.scope); const { can } = usePermissions(); - const items = menuForRole(primaryRole, can); + const items = menuForRole(primaryRole, can, scope); return (