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:
@@ -69,9 +69,7 @@ function buildSections(
|
||||
// پزشکِ مهمان در محیط کلینیک (scope=clinic): منو از روی مجوزهایی که کلینیک
|
||||
// برایش تعیین کرده ساخته میشود، نه بهصورت hardcode.
|
||||
if (primaryRole === "doctor" && scope === "clinic") {
|
||||
const items: SectionItem[] = [
|
||||
{ to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد" },
|
||||
];
|
||||
const items: SectionItem[] = [];
|
||||
if (can("appointments", "view")) {
|
||||
items.push({
|
||||
to: "/admin/appointments",
|
||||
@@ -88,8 +86,36 @@ function buildSections(
|
||||
feature: "patient_records",
|
||||
});
|
||||
}
|
||||
if (can("payments", "view")) {
|
||||
items.push({ to: "/admin/my-payments", icon: CreditCardIcon, label: "پرداختها" });
|
||||
}
|
||||
if (can("insurances", "view")) {
|
||||
items.push(
|
||||
{ to: "/admin/insurance-pricing", icon: ShieldCheckIcon, label: "قیمتگذاری بیمه", feature: "insurance" },
|
||||
{ to: "/admin/claims", icon: DocumentTextIcon, label: "مطالبات بیمه", feature: "insurance" },
|
||||
);
|
||||
}
|
||||
if (can("services", "view")) {
|
||||
items.push({ to: "/admin/clinic-services", icon: WrenchScrewdriverIcon, label: "سرویس ها" });
|
||||
}
|
||||
if (can("inventory", "view")) {
|
||||
items.push({ to: "/admin/inventory", icon: ArchiveBoxIcon, label: "انبارداری" });
|
||||
}
|
||||
|
||||
return [{ label: "عمومی", items }];
|
||||
// زیرمنوهای «تنظیمات» (services/tags/staff/discounts/sms/appointment_settings)
|
||||
// مثل پزشکِ مستقل داخل صفحهٔ تنظیماتاند، نه سایدبار اصلی؛ منویِ کناریِ آن
|
||||
// صفحه بر اساس مجوز فیلتر میشود.
|
||||
return [
|
||||
{
|
||||
label: "عمومی",
|
||||
items: [{ to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد" }],
|
||||
},
|
||||
{ label: "مدیریت", items },
|
||||
{
|
||||
label: "تنظیمات",
|
||||
items: [{ to: "/admin/account-settings", icon: Cog6ToothIcon, label: "تنظیمات" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (primaryRole === "admin") {
|
||||
|
||||
Reference in New Issue
Block a user