feat: move "منابع" to main sidebar and update related components

This commit is contained in:
hamed
2026-08-03 09:52:41 +03:30
parent 17ce271b2c
commit 82abd1bb52
9 changed files with 199 additions and 172 deletions
@@ -36,15 +36,22 @@ describe('SettingsLayout', () => {
/** /**
* سایدبار و فهرست موبایل یک منبع دارند؛ آیتمی که به منو اضافه می‌شود باید در هر دو * سایدبار و فهرست موبایل یک منبع دارند؛ آیتمی که به منو اضافه می‌شود باید در هر دو
* دیده شود. قبلاً دو کپی بود و «منابع»/«دسته‌بندی‌ها» فقط در موبایل ظاهر شدند. * دیده شود. قبلاً دو کپی بود و «دسته‌بندی‌ها» فقط در موبایل ظاهر شد.
*/ */
it('shows the resource-first entries in the desktop sidebar too', () => { it('shows the resource-first entries in the desktop sidebar too', () => {
useAuthStore.setState({ primaryRole: 'clinic' }); useAuthStore.setState({ primaryRole: 'clinic' });
renderWithProviders(<SettingsLayout active="resources"><div /></SettingsLayout>); renderWithProviders(<SettingsLayout active="service-categories"><div /></SettingsLayout>);
expect(screen.getByText('منابع').closest('a')).toHaveAttribute('href', '/admin/resources');
expect(screen.getByText('دسته‌بندی‌ها').closest('a')).toHaveAttribute('href', '/admin/service-categories'); expect(screen.getByText('دسته‌بندی‌ها').closest('a')).toHaveAttribute('href', '/admin/service-categories');
expect(screen.getByText('منابع').closest('a')).toHaveAttribute('aria-current', 'page'); expect(screen.getByText('دسته‌بندی‌ها').closest('a')).toHaveAttribute('aria-current', 'page');
});
/** «منابع» کارِ روزمره است و به سایدبار اصلی رفت؛ نباید در منوی تنظیمات تکرار شود. */
it('does not list منابع in the settings menu any more', () => {
useAuthStore.setState({ primaryRole: 'clinic' });
renderWithProviders(<SettingsLayout active="service-categories"><div /></SettingsLayout>);
expect(screen.queryByText('منابع')).not.toBeInTheDocument();
}); });
it('role-gates the desktop sidebar: a doctor does not see the clinic-doctors tab', () => { it('role-gates the desktop sidebar: a doctor does not see the clinic-doctors tab', () => {
@@ -141,6 +141,22 @@ describe("Sidebar — گِیت منوی منشی بر اساس مجوز", () =>
expect(screen.queryByText("تنظیمات نوبت‌دهی")).not.toBeInTheDocument(); expect(screen.queryByText("تنظیمات نوبت‌دهی")).not.toBeInTheDocument();
expect(screen.queryByText("پزشکان کلینیک")).not.toBeInTheDocument(); expect(screen.queryByText("پزشکان کلینیک")).not.toBeInTheDocument();
}); });
/**
* «منابع» از منوی تنظیمات به سایدبار اصلی منتقل شد. گِیتش عوض نشده — همان
* `appointment_settings.view` که در منوی تنظیمات داشت.
*/
it("«منابع» در سایدبار اصلی می‌آید و به مجوز appointment_settings گِیت است", () => {
setSecretary({ appointment_settings: { view: true } });
renderWithProviders(<Sidebar />, { route: "/admin/dashboard" });
expect(screen.getByText("منابع").closest("a")).toHaveAttribute("href", "/admin/resources");
});
it("منشیِ بدون مجوز، «منابع» را نمی‌بیند", () => {
setSecretary({ appointments: { view: true } });
renderWithProviders(<Sidebar />, { route: "/admin/dashboard" });
expect(screen.queryByText("منابع")).not.toBeInTheDocument();
});
}); });
describe("Sidebar — گِیت منوی پزشکِ عضوِ کلینیک بر اساس مجوز", () => { describe("Sidebar — گِیت منوی پزشکِ عضوِ کلینیک بر اساس مجوز", () => {
@@ -11,6 +11,7 @@ import {
ClipboardDocumentCheckIcon, ClipboardDocumentCheckIcon,
Cog6ToothIcon, Cog6ToothIcon,
CreditCardIcon, CreditCardIcon,
CubeIcon,
CurrencyDollarIcon, CurrencyDollarIcon,
DevicePhoneMobileIcon, DevicePhoneMobileIcon,
DocumentTextIcon, DocumentTextIcon,
@@ -102,6 +103,9 @@ function buildSections(
if (can("services", "view")) { if (can("services", "view")) {
items.push({ to: "/admin/clinic-services", icon: WrenchScrewdriverIcon, label: "سرویس ها" }); items.push({ to: "/admin/clinic-services", icon: WrenchScrewdriverIcon, label: "سرویس ها" });
} }
if (can("appointment_settings", "view")) {
items.push({ to: "/admin/resources", icon: CubeIcon, label: "منابع" });
}
if (can("inventory", "view")) { if (can("inventory", "view")) {
items.push({ to: "/admin/inventory", icon: ArchiveBoxIcon, label: "انبارداری" }); items.push({ to: "/admin/inventory", icon: ArchiveBoxIcon, label: "انبارداری" });
} }
@@ -306,6 +310,11 @@ function buildSections(
icon: WrenchScrewdriverIcon, icon: WrenchScrewdriverIcon,
label: "سرویس ها", label: "سرویس ها",
}, },
{
to: "/admin/resources",
icon: CubeIcon,
label: "منابع",
},
], ],
}, },
{ {
@@ -381,6 +390,11 @@ function buildSections(
icon: WrenchScrewdriverIcon, icon: WrenchScrewdriverIcon,
label: "سرویس ها", label: "سرویس ها",
}, },
{
to: "/admin/resources",
icon: CubeIcon,
label: "منابع",
},
], ],
}, },
{ {
@@ -448,6 +462,13 @@ function buildSections(
label: "سرویس ها", label: "سرویس ها",
}); });
} }
if (can("appointment_settings", "view")) {
items.push({
to: "/admin/resources",
icon: CubeIcon,
label: "منابع",
});
}
if (can("inventory", "view")) { if (can("inventory", "view")) {
items.push({ items.push({
to: "/admin/inventory", to: "/admin/inventory",
@@ -3,7 +3,7 @@ import {
CreditCardIcon, UserIcon, CalendarDaysIcon, BuildingOffice2Icon, CreditCardIcon, UserIcon, CalendarDaysIcon, BuildingOffice2Icon,
BanknotesIcon, UsersIcon, ShieldCheckIcon, BanknotesIcon, UsersIcon, ShieldCheckIcon,
TagIcon, ChatBubbleLeftRightIcon, UserCircleIcon, UserPlusIcon, ReceiptPercentIcon, TagIcon, ChatBubbleLeftRightIcon, UserCircleIcon, UserPlusIcon, ReceiptPercentIcon,
CubeIcon, RectangleStackIcon, RectangleStackIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
/** /**
@@ -34,7 +34,8 @@ export const SETTINGS_MENU: SettingsMenuItem[] = [
{ key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/appointment-settings', roles: ['doctor'], perm: ['appointment_settings', 'view'] }, { key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/appointment-settings', roles: ['doctor'], perm: ['appointment_settings', 'view'] },
{ key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/settings/appointment-settings', roles: ['clinic'], perm: ['appointment_settings', 'view'] }, { key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/settings/appointment-settings', roles: ['clinic'], perm: ['appointment_settings', 'view'] },
{ key: 'clinic-doctors', label: 'پزشکان کلینیک', icon: BuildingOffice2Icon, to: '/admin/settings/clinic-doctors', roles: ['clinic'], perm: ['clinic_doctors', 'view'] }, { key: 'clinic-doctors', label: 'پزشکان کلینیک', icon: BuildingOffice2Icon, to: '/admin/settings/clinic-doctors', roles: ['clinic'], perm: ['clinic_doctors', 'view'] },
{ key: 'resources', label: 'منابع', icon: CubeIcon, to: '/admin/resources', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] }, // «منابع» به سایدبار اصلی («مدیریت») منتقل شد — کارِ روزمره است، نه تنظیمات.
// گِیتش آنجا همین است: `appointment_settings.view` در هر چهار نقشی که اینجا می‌دیدندش.
{ key: 'service-categories', label: 'دسته‌بندی‌ها', icon: RectangleStackIcon, to: '/admin/service-categories', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] }, { key: 'service-categories', label: 'دسته‌بندی‌ها', icon: RectangleStackIcon, to: '/admin/service-categories', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] },
{ key: 'holidays', label: 'تعطیلات رسمی', icon: CalendarDaysIcon, to: '/admin/holidays', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] }, { key: 'holidays', label: 'تعطیلات رسمی', icon: CalendarDaysIcon, to: '/admin/holidays', roles: ['doctor', 'clinic'], perm: ['appointment_settings', 'view'] },
{ key: 'payment', label: 'مدیریت پرداخت', icon: BanknotesIcon, to: '/admin/my-financial', perm: ['payments', 'view'] }, { key: 'payment', label: 'مدیریت پرداخت', icon: BanknotesIcon, to: '/admin/my-financial', perm: ['payments', 'view'] },
@@ -9,7 +9,6 @@ import ResourceExceptionsPanel from '../components/resources/ResourceExceptionsP
import ResourceServicesPanel from '../components/resources/ResourceServicesPanel'; import ResourceServicesPanel from '../components/resources/ResourceServicesPanel';
import ResourceSkillsPanel from '../components/resources/ResourceSkillsPanel'; import ResourceSkillsPanel from '../components/resources/ResourceSkillsPanel';
import ResourceCategoriesPanel from '../components/resources/ResourceCategoriesPanel'; import ResourceCategoriesPanel from '../components/resources/ResourceCategoriesPanel';
import SettingsLayout from '../components/layout/SettingsLayout';
import { useUrlState } from '../hooks/useUrlState'; import { useUrlState } from '../hooks/useUrlState';
import { usePermissions } from '../hooks/usePermissions'; import { usePermissions } from '../hooks/usePermissions';
import { useAddresses } from '../hooks/useAddresses'; import { useAddresses } from '../hooks/useAddresses';
@@ -61,16 +60,13 @@ export default function ResourceDetailPage() {
// یک لحظه می‌پرد و دوباره برمی‌گردد. // یک لحظه می‌پرد و دوباره برمی‌گردد.
if (loading || !resource) { if (loading || !resource) {
return ( return (
<SettingsLayout active="resources">
<div style={{ padding: 24, color: 'var(--text-3)', fontSize: 13 }}> <div style={{ padding: 24, color: 'var(--text-3)', fontSize: 13 }}>
{loading ? 'در حال بارگذاری...' : 'منبع یافت نشد.'} {loading ? 'در حال بارگذاری...' : 'منبع یافت نشد.'}
</div> </div>
</SettingsLayout>
); );
} }
return ( return (
<SettingsLayout active="resources">
<div className="fade-in"> <div className="fade-in">
<PageHeader <PageHeader
title={resource.name} title={resource.name}
@@ -153,7 +149,6 @@ export default function ResourceDetailPage() {
} }
/> />
</div> </div>
</SettingsLayout>
); );
} }
-3
View File
@@ -11,7 +11,6 @@ import { usePermissions } from '../hooks/usePermissions';
import { useAddresses } from '../hooks/useAddresses'; import { useAddresses } from '../hooks/useAddresses';
import { useResourcePools, useResources, useResourceTypes } from '../hooks/useResources'; import { useResourcePools, useResources, useResourceTypes } from '../hooks/useResources';
import type { ResourcePool } from '../types'; import type { ResourcePool } from '../types';
import SettingsLayout from '../components/layout/SettingsLayout';
import ResourcesSubNav from '../components/resources/ResourcesSubNav'; import ResourcesSubNav from '../components/resources/ResourcesSubNav';
/** /**
@@ -67,7 +66,6 @@ export default function ResourcePoolsPage() {
]; ];
return ( return (
<SettingsLayout active="resources">
<div className="fade-in"> <div className="fade-in">
<PageHeader <PageHeader
title="استخر منابع" title="استخر منابع"
@@ -145,7 +143,6 @@ export default function ResourcePoolsPage() {
onCancel={() => setToDelete(null)} onCancel={() => setToDelete(null)}
/> />
</div> </div>
</SettingsLayout>
); );
} }
-3
View File
@@ -9,7 +9,6 @@ import { useUrlState } from '../hooks/useUrlState';
import { usePermissions } from '../hooks/usePermissions'; import { usePermissions } from '../hooks/usePermissions';
import { useResourceTypes } from '../hooks/useResources'; import { useResourceTypes } from '../hooks/useResources';
import type { ResourceType } from '../types'; import type { ResourceType } from '../types';
import SettingsLayout from '../components/layout/SettingsLayout';
import ResourcesSubNav from '../components/resources/ResourcesSubNav'; import ResourcesSubNav from '../components/resources/ResourcesSubNav';
/** نوع منبع — کلینیک خودش تعریفش می‌کند؛ سه نوع سیستمی را backfill می‌سازد. */ /** نوع منبع — کلینیک خودش تعریفش می‌کند؛ سه نوع سیستمی را backfill می‌سازد. */
@@ -44,7 +43,6 @@ export default function ResourceTypesPage() {
]; ];
return ( return (
<SettingsLayout active="resources">
<div className="fade-in"> <div className="fade-in">
<PageHeader <PageHeader
title="نوع منابع" title="نوع منابع"
@@ -114,7 +112,6 @@ export default function ResourceTypesPage() {
onCancel={() => setToDelete(null)} onCancel={() => setToDelete(null)}
/> />
</div> </div>
</SettingsLayout>
); );
} }
-4
View File
@@ -16,7 +16,6 @@ import ResourceSkillsModal from '../components/resources/ResourceSkillsModal';
import ResourceServicesModal from '../components/resources/ResourceServicesModal'; import ResourceServicesModal from '../components/resources/ResourceServicesModal';
import { useAllServiceItems } from '../hooks/useServiceCatalog'; import { useAllServiceItems } from '../hooks/useServiceCatalog';
import type { ClinicResource } from '../types'; import type { ClinicResource } from '../types';
import SettingsLayout from '../components/layout/SettingsLayout';
import ResourcesSubNav from '../components/resources/ResourcesSubNav'; import ResourcesSubNav from '../components/resources/ResourcesSubNav';
/** برچسب فارسی پلِ هر منبع؛ `null` یعنی تجهیزاتی که پشتش موجودیت دیگری نیست. */ /** برچسب فارسی پلِ هر منبع؛ `null` یعنی تجهیزاتی که پشتش موجودیت دیگری نیست. */
@@ -114,12 +113,10 @@ export default function ResourcesPage() {
]; ];
return ( return (
<SettingsLayout active="resources">
<div className="fade-in"> <div className="fade-in">
<PageHeader <PageHeader
title="منابع" title="منابع"
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار هم‌زمان." description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار هم‌زمان."
backTo="/admin/settings-menu"
action={ action={
canUpdate ? ( canUpdate ? (
<button type="button" className="btn primary" onClick={() => setEditing({ open: true, resource: null })}> <button type="button" className="btn primary" onClick={() => setEditing({ open: true, resource: null })}>
@@ -272,6 +269,5 @@ export default function ResourcesPage() {
onCancel={() => setToDelete(null)} onCancel={() => setToDelete(null)}
/> />
</div> </div>
</SettingsLayout>
); );
} }
-3
View File
@@ -9,7 +9,6 @@ import { useUrlState } from '../hooks/useUrlState';
import { usePermissions } from '../hooks/usePermissions'; import { usePermissions } from '../hooks/usePermissions';
import { useSkills } from '../hooks/useResources'; import { useSkills } from '../hooks/useResources';
import type { Skill } from '../types'; import type { Skill } from '../types';
import SettingsLayout from '../components/layout/SettingsLayout';
import ResourcesSubNav from '../components/resources/ResourcesSubNav'; import ResourcesSubNav from '../components/resources/ResourcesSubNav';
/** /**
@@ -41,7 +40,6 @@ export default function SkillsPage() {
]; ];
return ( return (
<SettingsLayout active="resources">
<div className="fade-in"> <div className="fade-in">
<PageHeader <PageHeader
title="مهارت‌ها" title="مهارت‌ها"
@@ -111,7 +109,6 @@ export default function SkillsPage() {
onCancel={() => setToDelete(null)} onCancel={() => setToDelete(null)}
/> />
</div> </div>
</SettingsLayout>
); );
} }