feat(clinic-doctor): full permission coverage + enforcement, parity with secretary
The clinic-member-doctor permission system (ClinicDoctorPermission) lagged the secretary system: only 6 resources, enforced in ~6 places, dead toggles (services.update never checked), and a sidebar showing just appointments+patients. Bring it to parity so a clinic owner can control exactly what each member doctor does — while an independent doctor stays completely unrestricted. Coverage: add insurances, addresses, inventory, tags, staff, discounts, sms to ClinicDoctorPermission::DEFAULT_PERMISSIONS + DoctorPermissionsModal (subscription/clinic_doctors stay owner-only by design). New App\Clinic\Security\ClinicDoctorAccessChecker (parallel to SecretaryAccessChecker): - denyUnlessGranted(user, resource, action): 403 only for a clinic-member doctor in the clinic context; owner/admin/secretary/independent-doctor pass through. - memberClinicId(user): resolves the member doctor to the CLINIC's tenant so the role-based controllers (Inventory/Tag/Staff/Discount/Sms) stop showing them their personal tenant in clinic context. Enforcement wired into 10 controllers alongside the existing secretary gates: ClinicService (services), Insurance (insurances), Patient (patients+payments), Staff, Discount, Inventory, Tag, SmsWallet, Payment, PaymentMethod. Frontend: the guest-doctor sidebar branch now exposes every permitted resource (gated by can()) plus a «تنظیمات» entry; both settings navs (PurchaseSubscription Sidebar + SETTINGS_MENU) are now permission-filtered for a scope=clinic doctor, not just secretaries; my-payments route gets the missing payments permission. CRUD-button gating already applies (usePermissions is role-agnostic). Tests: ClinicDoctorPermissionEnforcementTest (member denied/allowed + independent-doctor-unrestricted); guest-doctor sidebar gating. Backend 375 pass, frontend 503 pass. docs/api/clinic.md updated with the full resource set + enforcement notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -142,3 +142,31 @@ describe("Sidebar — گِیت منوی منشی بر اساس مجوز", () =>
|
||||
expect(screen.queryByText("پزشکان کلینیک")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sidebar — گِیت منوی پزشکِ عضوِ کلینیک بر اساس مجوز", () => {
|
||||
const setMemberDoctor = (resources: Record<string, Record<string, boolean>>) =>
|
||||
useAuthStore.setState({
|
||||
primaryRole: "doctor",
|
||||
dbUuid: "c1",
|
||||
userName: "دکتر عضو",
|
||||
availableContexts: [],
|
||||
context: { scope: "clinic", permissions: { resources } },
|
||||
} as any);
|
||||
|
||||
it("پزشکِ عضو فقط منابعِ مجاز را در سایدبار میبیند + «تنظیمات» همیشه", () => {
|
||||
setMemberDoctor({ appointments: { view: true }, services: { view: true } });
|
||||
renderWithProviders(<Sidebar />, { route: "/admin/dashboard" });
|
||||
expect(screen.getByText("نوبتهای من")).toBeInTheDocument();
|
||||
expect(screen.getByText("سرویس ها").closest("a")).toHaveAttribute("href", "/admin/clinic-services");
|
||||
expect(screen.getByRole("link", { name: "تنظیمات" })).toHaveAttribute("href", "/admin/account-settings");
|
||||
// بدون مجوز → پنهان
|
||||
expect(screen.queryByText("انبارداری")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("پرداختها")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("پزشکِ عضو بدونِ مجوزِ نوبت، آیتم نوبت را نمیبیند", () => {
|
||||
setMemberDoctor({ services: { view: true } });
|
||||
renderWithProviders(<Sidebar />, { route: "/admin/dashboard" });
|
||||
expect(screen.queryByText("نوبتهای من")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user