import { useEffect, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { Calendar, ChevronRight, ChevronsLeft, ChevronsRight, Circle, ClipboardList, Cpu, Eye, FileClock, FileText, FlaskConical, Folder, Layers, ListChecks, Pill, ShieldCheck, Stethoscope, StickyNote, User, Users, type LucideIcon, } from "lucide-react"; type PatientChartSidebarSubSection = { id: string; label: string }; type PatientChartSidebarSection = { id: string; label: string; subSections?: PatientChartSidebarSubSection[]; /** Other top-level paths this section owns, so it stays lit on them. */ alsoActiveOn?: string[]; }; interface PatientChartSidebarProps { sections: PatientChartSidebarSection[]; basePath: string; /** A section that opens with the nav on its icon rail, for the room its page needs. */ railOn?: string; } const COLLAPSED_STORAGE_KEY = "akashic:patientChartSidebar:collapsed"; // Section id -> nav icon. Keeps PatientPage's section data presentation-agnostic; // all nav iconography lives here. Unmapped ids fall back to a neutral dot. // Icons are provisional (Spencer: "best guess for now, refine later"). const SECTION_ICONS: Record = { profile: User, "related-persons": Users, appointments: Calendar, tasks: ListChecks, insurance: ShieldCheck, notes: StickyNote, "questionnaire-responses": FileText, documents: Folder, "access-log": Eye, record: FileClock, clinical: Stethoscope, diagnostics: FlaskConical, medications: Pill, "device-data": Cpu, "care-provision": ClipboardList, management: Layers, }; /** * Secondary left-nav for the staff patient chart. Mirrors the primary dashboard nav * (DashboardPage renderNavItem): ghost buttons, active = * `bg-gray-50 text-primary border border-primary`, a ChevronRight that rotates 90deg * when a group is expanded, children indented under a left border, and a collapsible * icon rail for tablet/narrow layouts (collapse state persisted to localStorage). * * Behavior (matches the primary nav): * - Parent rows (with subSections) NEVER navigate - clicking one only expands/collapses * its child group. On the collapsed rail there's no room for inline children, so a * parent click expands the rail and opens that group. * - Leaf rows navigate to their page. * * Read-only navigation - it derives active state from the URL and calls navigate(); * it fetches nothing. */ const PatientChartSidebar = ({ sections, basePath, railOn }: PatientChartSidebarProps) => { const location = useLocation(); const navigate = useNavigate(); const isUnder = (id: string) => location.pathname === `${basePath}/${id}` || location.pathname.startsWith(`${basePath}/${id}/`); const isSectionActive = (section: PatientChartSidebarSection) => isUnder(section.id) || (section.alsoActiveOn ?? []).some(isUnder); const isSubSectionActive = ( section: PatientChartSidebarSection, sub: PatientChartSidebarSubSection, ) => location.pathname === `${basePath}/${section.id}/${sub.id}` || location.pathname.startsWith(`${basePath}/${section.id}/${sub.id}/`); const activeSection = sections.find(isSectionActive) ?? sections[0]; const [storedCollapsed, setStoredCollapsed] = useState(() => { try { return localStorage.getItem(COLLAPSED_STORAGE_KEY) === "1"; } catch { return false; } }); useEffect(() => { try { localStorage.setItem(COLLAPSED_STORAGE_KEY, storedCollapsed ? "1" : "0"); } catch { // ignore storage failures (private mode, etc.) } }, [storedCollapsed]); // Railed on its own page only; opening it there is not remembered as a preference. // A phone opens every section on the rail, so the section itself is on screen rather than below the nav. const [isPhone] = useState(() => window.matchMedia?.("(max-width: 639px)")?.matches ?? false); const onRail = (Boolean(railOn) && isUnder(railOn as string)) || isPhone; const [openedOnRail, setOpenedOnRail] = useState(false); const collapsed = onRail ? !openedOnRail : storedCollapsed; // Choosing a section on a phone folds the nav back to its row. const go = (path: string) => { if (isPhone) setOpenedOnRail(false); navigate(path); }; const setCollapsed = (next: boolean) => (onRail ? setOpenedOnRail(!next) : setStoredCollapsed(next)); const [expanded, setExpanded] = useState>(() => activeSection ? new Set([activeSection.id]) : new Set(), ); // Keep the active section's group open when navigation changes which section is active // (e.g. deep-linking or a route change from outside this component). useEffect(() => { if (!activeSection) return; setExpanded((prev) => { if (prev.has(activeSection.id)) return prev; const next = new Set(prev); next.add(activeSection.id); return next; }); }, [activeSection?.id]); const toggleExpanded = (sectionId: string) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(sectionId)) { next.delete(sectionId); } else { next.add(sectionId); } return next; }); }; const openGroup = (sectionId: string) => { setExpanded((prev) => { if (prev.has(sectionId)) return prev; const next = new Set(prev); next.add(sectionId); return next; }); }; const handleSectionClick = (section: PatientChartSidebarSection) => { const hasSubSections = !!section.subSections?.length; if (hasSubSections) { // Parent rows never navigate - they only reveal their children. if (collapsed) { // No room for inline children on the rail: expand the rail, then open the group. setCollapsed(false); openGroup(section.id); return; } toggleExpanded(section.id); return; } go(`${basePath}/${section.id}`); }; return ( // Below tablet the nav sits above the section, full width, so the page never scrolls sideways. ); }; export default PatientChartSidebar;