fix(secretary): route «نوبتدهی» settings to the scope-correct variant
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) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,20 @@ describe('PurchaseSubscriptionSidebar — settings menu', () => {
|
|||||||
useAuthStore.setState({ primaryRole: null, context: null } as any);
|
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(<PurchaseSubscriptionSidebar active="appointment" />, { 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`,
|
// 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).
|
// which dropped the whole menu below the lg breakpoint → no settings nav on phones).
|
||||||
it('is not hidden on mobile', () => {
|
it('is not hidden on mobile', () => {
|
||||||
|
|||||||
@@ -49,17 +49,24 @@ const NAV_ITEMS: NavItem[] = [
|
|||||||
export default function PurchaseSubscriptionSidebar({ active }: { active: string }) {
|
export default function PurchaseSubscriptionSidebar({ active }: { active: string }) {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const primaryRole = useAuthStore((s) => s.primaryRole);
|
const primaryRole = useAuthStore((s) => s.primaryRole);
|
||||||
|
const scope = useAuthStore((s) => s.context?.scope);
|
||||||
const { can } = usePermissions();
|
const { can } = usePermissions();
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() => NAV_ITEMS
|
() => NAV_ITEMS
|
||||||
// منشی: بر اساس مجوز، نه نقش. آیتمِ بدونِ perm/alwaysOpen برای منشی پنهان است.
|
// منشی: بر اساس مجوز، نه نقش. آیتمِ بدونِ perm/alwaysOpen پنهان است. برای
|
||||||
.filter((i) =>
|
// آیتمهایی که واریانتِ نقشی دارند (مثلِ «نوبتدهی» با doctor vs clinic)، با
|
||||||
primaryRole === 'secretary'
|
// scope منشی تطبیق داده میشود تا واریانتِ درست انتخاب شود.
|
||||||
? (i.alwaysOpen || (i.perm ? can(i.perm[0], i.perm[1]) : false))
|
.filter((i) => {
|
||||||
: (!i.roles || (primaryRole != null && i.roles.includes(primaryRole))),
|
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())),
|
.filter((i) => i.label.includes(query.trim())),
|
||||||
[query, primaryRole, can],
|
[query, primaryRole, scope, can],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -46,11 +46,15 @@ export const SETTINGS_MENU: SettingsMenuItem[] = [
|
|||||||
export function menuForRole(
|
export function menuForRole(
|
||||||
role: string | null | undefined,
|
role: string | null | undefined,
|
||||||
can?: (resource: string, action: string) => boolean,
|
can?: (resource: string, action: string) => boolean,
|
||||||
|
scope?: string | null,
|
||||||
): SettingsMenuItem[] {
|
): SettingsMenuItem[] {
|
||||||
return SETTINGS_MENU.filter((i) => {
|
return SETTINGS_MENU.filter((i) => {
|
||||||
if (role === 'secretary') {
|
if (role === 'secretary') {
|
||||||
if (i.alwaysOpen) return true;
|
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));
|
return !i.roles || (role != null && i.roles.includes(role));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import { usePermissions } from '../hooks/usePermissions';
|
|||||||
*/
|
*/
|
||||||
export default function SettingsMenuPage() {
|
export default function SettingsMenuPage() {
|
||||||
const primaryRole = useAuthStore((s) => s.primaryRole);
|
const primaryRole = useAuthStore((s) => s.primaryRole);
|
||||||
|
const scope = useAuthStore((s) => s.context?.scope);
|
||||||
const { can } = usePermissions();
|
const { can } = usePermissions();
|
||||||
const items = menuForRole(primaryRole, can);
|
const items = menuForRole(primaryRole, can, scope);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in" style={{ maxWidth: 720, margin: '0 auto' }}>
|
<div className="fade-in" style={{ maxWidth: 720, margin: '0 auto' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user