import { ArchiveBoxIcon, ArrowLeftOnRectangleIcon, ArrowsRightLeftIcon, BanknotesIcon, BuildingOffice2Icon, CalendarDaysIcon, ChartBarIcon, ChatBubbleLeftEllipsisIcon, ChevronDownIcon, ClipboardDocumentCheckIcon, Cog6ToothIcon, CreditCardIcon, DevicePhoneMobileIcon, DocumentTextIcon, FolderOpenIcon, HeartIcon, KeyIcon, LockClosedIcon, PlusIcon, ShieldCheckIcon, StarIcon, TagIcon, UserCircleIcon, UserGroupIcon, UsersIcon, WrenchScrewdriverIcon, } from "@heroicons/react/24/outline"; import { useState } from "react"; import { NavLink, useLocation, useNavigate } from "react-router-dom"; import { useSubscription } from "../../hooks/useSubscription"; import { usePermissions } from "../../hooks/usePermissions"; import { useAuthStore } from "../../stores/authStore"; import { useUiStore } from "../../stores/uiStore"; type SubItem = { to: string; label: string; icon?: React.ElementType }; type SectionItem = { to: string; icon: React.ElementType; label: string; feature?: string; /** وقتی مقدار داشته باشد، آیتم به یک منوی بازشونده تبدیل می‌شود. */ children?: SubItem[]; }; type Section = { label: string; items: SectionItem[] }; /** زیرمنوهای مشترکِ «نوبت‌ها» (نوبت‌های تأییدشده + افزودن نوبت). */ const APPOINTMENTS_CHILDREN: SubItem[] = [ { to: "/admin/appointments", label: "نوبت ها", icon: CalendarDaysIcon }, { to: "/admin/appointments/new", label: "افزودن نوبت", icon: PlusIcon }, ]; /** * همان زیرمنوها + «رزرو نوبت». برای نقش‌هایی که مالک برنامهٔ نوبت‌دهی‌اند * (ادمین/کلینیک/پزشک) تا همهٔ عملیات نوبت زیر یک بخش جمع شود. پزشکِ مهمانِ کلینیک * از نسخهٔ بدون رزرو (`APPOINTMENTS_CHILDREN`) استفاده می‌کند. */ const APPOINTMENTS_CHILDREN_WITH_RESERVE: SubItem[] = [ ...APPOINTMENTS_CHILDREN, { to: "/admin/appointments/reserve", label: "رزرو نوبت", icon: ArchiveBoxIcon }, ]; function buildSections( primaryRole: string | null, dbUuid: string | null, scope: string | null, can: (resource: string, action: string) => boolean, ): Section[] { // پزشکِ مهمان در محیط کلینیک (scope=clinic): منو از روی مجوزهایی که کلینیک // برایش تعیین کرده ساخته می‌شود، نه به‌صورت hardcode. if (primaryRole === "doctor" && scope === "clinic") { const items: SectionItem[] = [ { to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد" }, ]; if (can("appointments", "view")) { items.push({ to: "/admin/appointments", icon: CalendarDaysIcon, label: "نوبت‌های من", children: APPOINTMENTS_CHILDREN, }); } if (can("patients", "view")) { items.push({ to: "/admin/patients", icon: FolderOpenIcon, label: "پرونده بیماران", feature: "patient_records", }); } return [{ label: "عمومی", items }]; } if (primaryRole === "admin") { return [ { label: "عمومی", items: [ { to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد", }, { to: "/admin/users", icon: UserGroupIcon, label: "کاربران", }, { to: "/admin/doctors", icon: HeartIcon, label: "پزشکان" }, { to: "/admin/doctor-claims", icon: HeartIcon, label: "تصاحب پروفایل", }, { to: "/admin/clinics", icon: BuildingOffice2Icon, label: "کلینیک‌ها", }, ], }, { label: "مدیریت", items: [ { to: "/admin/appointments", icon: CalendarDaysIcon, label: "نوبت‌ها", children: APPOINTMENTS_CHILDREN_WITH_RESERVE, }, { to: "/admin/payments", icon: CreditCardIcon, label: "پرداخت‌ها", }, { to: "/admin/settlements", icon: BanknotesIcon, label: "تسویه‌حساب", }, { to: "/admin/financial-report", icon: BanknotesIcon, label: "گزارش مالی", }, { to: "/admin/pre-registrations", icon: ClipboardDocumentCheckIcon, label: "درخواست‌های ثبت‌نام", }, ], }, { label: "محتوا", items: [ { to: "/admin/comments", icon: ChatBubbleLeftEllipsisIcon, label: "نظرات", }, { to: "/admin/ratings", icon: StarIcon, label: "امتیازها" }, { to: "/admin/blogs", icon: DocumentTextIcon, label: "بلاگ", }, { to: "/admin/sms", icon: DevicePhoneMobileIcon, label: "پیامک", }, ], }, { label: "سیستم", items: [ { to: "/admin/logs", icon: ClipboardDocumentCheckIcon, label: "لاگ‌ها", }, { to: "/admin/categories", icon: TagIcon, label: "دسته‌بندی‌ها", }, { to: "/admin/representations", icon: UsersIcon, label: "نمایندگان", }, { to: "/admin/secretaries", icon: KeyIcon, label: "منشی‌ها", }, { to: "/admin/admin-subscription", icon: CreditCardIcon, label: "اشتراک‌ها", }, { to: "/admin/settings", icon: Cog6ToothIcon, label: "تنظیمات", }, ], }, ]; } if (primaryRole === "clinic") { const clinicTo = dbUuid ? `/admin/clinics/${dbUuid}` : "/admin/settings/clinic-doctors"; return [ { label: "عمومی", items: [ { to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد", }, { to: clinicTo, icon: BuildingOffice2Icon, label: "کلینیک من", }, ], }, { label: "مدیریت", items: [ { to: "/admin/appointments", icon: CalendarDaysIcon, label: "نوبت‌ها", children: APPOINTMENTS_CHILDREN_WITH_RESERVE, }, { to: "/admin/patients", icon: FolderOpenIcon, label: "پرونده بیماران", feature: "patient_records", }, { to: "/admin/my-payments", icon: CreditCardIcon, label: "پرداخت‌ها", }, { to: "/admin/claims", icon: DocumentTextIcon, label: "مطالبات بیمه", feature: "insurance", }, { to: "/admin/inventory", icon: ArchiveBoxIcon, label: "انبارداری", }, { to: "/admin/clinic-services", icon: WrenchScrewdriverIcon, label: "سرویس ها", }, ], }, { // مدیریت بیمه / کیف پول پیامک زیر همین «تنظیمات» است // (منوی داخل صفحهٔ اشتراک)، نه در سایدبار اصلی. label: "تنظیمات", items: [ { to: "/admin/subscription", icon: Cog6ToothIcon, label: "تنظیمات", }, ], }, ]; } if (primaryRole === "doctor") { return [ { label: "عمومی", items: [ { to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد", }, ], }, { label: "پروفایل", items: [ { to: "/admin/profile", icon: UserCircleIcon, label: "پروفایل من", }, ], }, { label: "مدیریت", items: [ { to: "/admin/appointments", icon: CalendarDaysIcon, label: "نوبت‌های من", children: APPOINTMENTS_CHILDREN, }, { to: "/admin/patients", icon: FolderOpenIcon, label: "پرونده بیماران", feature: "patient_records", }, { to: "/admin/my-payments", icon: CreditCardIcon, label: "پرداخت‌ها", }, { to: "/admin/claims", icon: DocumentTextIcon, label: "مطالبات بیمه", feature: "insurance", }, { to: "/admin/inventory", icon: ArchiveBoxIcon, label: "انبارداری", }, { to: "/admin/clinic-services", icon: WrenchScrewdriverIcon, label: "سرویس ها", }, ], }, { // مدیریت بیمه / کیف پول پیامک زیر همین «تنظیمات» است // (منوی داخل صفحهٔ اشتراک)، نه در سایدبار اصلی. label: "تنظیمات", items: [ { to: "/admin/subscription", icon: Cog6ToothIcon, label: "تنظیمات", }, ], }, ]; } if (primaryRole === "secretary") { // منو از روی مجوزهای همان منشی ساخته می‌شود: آیتمی که مجوز مشاهده‌اش را // ندارد اصلاً نمایش داده نمی‌شود (هم‌راستا با enforcement سمت API). const items: SectionItem[] = []; if (can("appointments", "view")) { items.push({ to: "/admin/appointments", icon: CalendarDaysIcon, label: "نوبت‌ها", children: APPOINTMENTS_CHILDREN_WITH_RESERVE, }); } if (can("patients", "view")) { items.push({ to: "/admin/patients", icon: FolderOpenIcon, label: "پرونده بیماران", 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: "انبارداری", }); } // منابعِ زیرمجموعهٔ «تنظیمات» (staff/discounts/sms/tags/appointment_settings/ // clinic_doctors) در سایدبار اصلی نمی‌آیند — دقیقاً مثل پزشک/کلینیک، فقط داخل // صفحهٔ «تنظیمات» با منویِ permission-aware نمایش داده می‌شوند. return [ { label: "عمومی", items: [{ to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد" }], }, { label: "مدیریت", items }, { label: "تنظیمات", items: [ { // لندینگِ تنظیمات؛ حساب کاربری همیشه باز است و منویِ کناری // بقیهٔ بخش‌های مجاز را بر اساس permission نشان می‌دهد. to: "/admin/account-settings", icon: Cog6ToothIcon, label: "تنظیمات", }, ], }, ]; } if (primaryRole === "representation") { return [ { label: "عمومی", items: [ { to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد", }, ], }, { label: "مدیریت", items: [ { to: "/admin/doctors", icon: HeartIcon, label: "پزشکان" }, { to: "/admin/clinics", icon: BuildingOffice2Icon, label: "کلینیک‌ها", }, ], }, { label: "مالی", items: [ { to: "/admin/representation-finance", icon: CreditCardIcon, label: "گزارش مالی", }, { to: "/admin/representation-settlement", icon: BanknotesIcon, label: "تسویه حساب", }, ], }, { label: "حساب", items: [ { to: "/admin/representation-profile", icon: UserCircleIcon, label: "پروفایل", }, ], }, ]; } return [ { label: "عمومی", items: [ { to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد", }, ], }, ]; } const ROLE_LABELS: Record = { admin: "مدیر کل", clinic: "مالک کلینیک", doctor: "پزشک", secretary: "منشی", representation: "نماینده", user: "کاربر", }; const HUES = [256, 205, 162, 295, 272]; function avatarBg(name: string): string { const hue = HUES[(name.charCodeAt(0) ?? 0) % HUES.length]; return `linear-gradient(145deg, oklch(0.62 0.15 ${hue}), oklch(0.48 0.16 ${hue}))`; } interface Props { mobileOpen?: boolean; onMobileClose?: () => void; } /** * یک آیتم سایدبار: منوی ساده (NavLink) یا منوی بازشونده با زیرمنو. * منوی بازشونده وقتی یکی از زیرمنوهایش فعال است به‌صورت خودکار باز می‌شود. */ function NavItem({ item, sidebarOpen, isLocked, }: { item: SectionItem; sidebarOpen: boolean; isLocked: boolean; }) { const { icon: Icon, label, to, children } = item; const location = useLocation(); const childActive = !!children?.some((c) => location.pathname === c.to); // منوی «نوبت‌ها» همیشه باز می‌ماند و جمع نمی‌شود. const alwaysOpen = !!children?.some((c) => c.to.startsWith("/admin/appointments"), ); const [openState, setOpen] = useState(childActive); const open = alwaysOpen || openState; // منوی ساده — رفتار قبلی بدون تغییر. if (!children || children.length === 0) { const dest = isLocked ? "/admin/subscription" : to; return ( `nav-item${isActive && !isLocked ? " active" : ""}${isLocked ? " locked" : ""}` } style={isLocked ? { opacity: 0.55 } : undefined} > {label} {isLocked && ( )} ); } // منوی بازشونده. return (
{open && (
{children.map((c) => { const CIcon = c.icon; return ( `nav-item nav-subitem${isActive ? " active" : ""}` } > {CIcon ? ( ) : ( )} {c.label} ); })}
)}
); } export default function Sidebar({ mobileOpen: _m, onMobileClose: _c }: Props) { const sidebarOpen = useUiStore((s) => s.sidebarOpen); const { logout, primaryRole, userName, availableContexts, dbUuid, context, } = useAuthStore(); const { hasFeature } = useSubscription(); const navigate = useNavigate(); const { can } = usePermissions(); const sections = buildSections(primaryRole, dbUuid, context?.scope ?? null, can); const initials = (userName ?? "U").charAt(0).toUpperCase(); return ( ); }