From 9b0352de44dd3663c300d137e56dbdce001d5685 Mon Sep 17 00:00:00 2001 From: colinislit Date: Sat, 3 Jan 2026 11:52:13 +0100 Subject: [PATCH] feat(cortex): Epic 1 - Patient Sidebar (E1.S1-S4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapsible overlay sidebar voor snelle patient selectie met search en recent patients. E1.S1 - Store uitbreiding: - recentPatients: Patient[] (max 5, nieuwste eerst, geen duplicaten) - patientSidebarOpen: boolean - addRecentPatient, togglePatientSidebar, setPatientSidebarOpen actions E1.S2 - PatientSidebar component: - Overlay sidebar (w-80, z-50) met backdrop - Search input met debounce (200ms) - Recent patients sectie - Hergebruik usePatientSearch, usePatientSelection hooks - Hergebruik PatientListItem component E1.S3 - Toggle button + shortcut: - Users icon button in ContextBar - Cmd/Ctrl+P keyboard shortcut - Escape sluit sidebar (prioriteit boven artifacts) E1.S4 - Click-to-select flow: - Selectie zet activePatient - Voegt toe aan recentPatients - Sluit sidebar automatisch Bouwplan v1.3: Epic 1 compleet (10/23 SP - 43%) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../cortex/command-center/command-center.tsx | 31 ++- .../cortex/command-center/context-bar.tsx | 16 +- components/cortex/patient-sidebar/index.ts | 1 + .../patient-sidebar/patient-sidebar.tsx | 210 ++++++++++++++++++ .../bouwplan-patient-selectie-v1.md | 26 ++- stores/cortex-store.ts | 36 +++ 6 files changed, 301 insertions(+), 19 deletions(-) create mode 100644 components/cortex/patient-sidebar/index.ts create mode 100644 components/cortex/patient-sidebar/patient-sidebar.tsx diff --git a/components/cortex/command-center/command-center.tsx b/components/cortex/command-center/command-center.tsx index 06625ed..4c751c0 100644 --- a/components/cortex/command-center/command-center.tsx +++ b/components/cortex/command-center/command-center.tsx @@ -23,6 +23,7 @@ import { cn } from '@/lib/utils'; import { ContextBar } from './context-bar'; import { OfflineBanner } from './offline-banner'; import { NudgeToast } from './nudge-toast'; +import { PatientSidebar } from '../patient-sidebar'; import { ChatPanel } from '../chat/chat-panel'; import { ArtifactArea } from '../artifacts/artifact-area'; import { getArtifactTitle } from '../artifacts/artifact-container'; @@ -40,16 +41,27 @@ export function CommandCenter() { suggestions, acceptSuggestion, dismissSuggestion, + // Patient sidebar (E1) + togglePatientSidebar, + patientSidebarOpen, + setPatientSidebarOpen, } = useCortexStore(); const inputRef = useRef(null); // Global keyboard shortcuts const handleKeyDown = useCallback( (e: KeyboardEvent) => { - // Escape: close all artifacts - if (e.key === 'Escape' && openArtifacts.length > 0) { - e.preventDefault(); - closeAllArtifacts(); + // Escape: close sidebar first, then artifacts + if (e.key === 'Escape') { + if (patientSidebarOpen) { + e.preventDefault(); + setPatientSidebarOpen(false); + return; + } + if (openArtifacts.length > 0) { + e.preventDefault(); + closeAllArtifacts(); + } } // Cmd/Ctrl + K: focus input (chat input in v3.0) @@ -57,8 +69,14 @@ export function CommandCenter() { e.preventDefault(); inputRef.current?.focus(); } + + // Cmd/Ctrl + P: toggle patient sidebar (E1.S3) + if ((e.metaKey || e.ctrlKey) && e.key === 'p') { + e.preventDefault(); + togglePatientSidebar(); + } }, - [openArtifacts, closeAllArtifacts] + [openArtifacts, closeAllArtifacts, patientSidebarOpen, setPatientSidebarOpen, togglePatientSidebar] ); useEffect(() => { @@ -114,6 +132,9 @@ export function CommandCenter() { return (
+ {/* Patient Sidebar (E1.S2) */} + + {/* Offline Banner */} diff --git a/components/cortex/command-center/context-bar.tsx b/components/cortex/command-center/context-bar.tsx index 23390a7..1128af8 100644 --- a/components/cortex/command-center/context-bar.tsx +++ b/components/cortex/command-center/context-bar.tsx @@ -8,7 +8,7 @@ */ import { useCortexStore, type ShiftType } from '@/stores/cortex-store'; -import { Sun, Moon, Sunrise, Sunset, X, User, ArrowLeft } from 'lucide-react'; +import { Sun, Moon, Sunrise, Sunset, X, User, ArrowLeft, Users } from 'lucide-react'; import Link from 'next/link'; import { useOffline } from './offline-banner'; @@ -20,7 +20,7 @@ const SHIFT_CONFIG: Record {shiftConfig.label}
+ +
+ + {/* Patient Sidebar Toggle (E1.S3) */} +
{/* Center: Active Patient */} diff --git a/components/cortex/patient-sidebar/index.ts b/components/cortex/patient-sidebar/index.ts new file mode 100644 index 0000000..10d20df --- /dev/null +++ b/components/cortex/patient-sidebar/index.ts @@ -0,0 +1 @@ +export { PatientSidebar } from './patient-sidebar'; diff --git a/components/cortex/patient-sidebar/patient-sidebar.tsx b/components/cortex/patient-sidebar/patient-sidebar.tsx new file mode 100644 index 0000000..81f245d --- /dev/null +++ b/components/cortex/patient-sidebar/patient-sidebar.tsx @@ -0,0 +1,210 @@ +'use client'; + +/** + * PatientSidebar Component + * + * Collapsible overlay sidebar for quick patient selection. + * Features search and recent patients list. + * + * Epic: E1.S2 (Patient Selectie - Sidebar) + */ + +import { useEffect, useRef } from 'react'; +import { X, Search, Clock, Users } from 'lucide-react'; +import { useCortexStore } from '@/stores/cortex-store'; +import { usePatientSearch, type PatientSearchResult } from '@/lib/cortex/hooks/use-patient-search'; +import { usePatientSelection } from '@/lib/cortex/hooks/use-patient-selection'; +import { + PatientListItem, + PatientListEmpty, + PatientListLoading, +} from '@/components/cortex/shared/patient-list-item'; +import { Input } from '@/components/ui/input'; + +export function PatientSidebar() { + const { + patientSidebarOpen, + togglePatientSidebar, + setPatientSidebarOpen, + recentPatients, + addRecentPatient, + } = useCortexStore(); + + const { query, setQuery, results, isSearching, clearResults } = usePatientSearch({ + debounceMs: 200, + limit: 5, + }); + + const { selectPatient, isSelecting, selectedId } = usePatientSelection({ + onSuccess: (patient) => { + // Add to recent patients and close sidebar + addRecentPatient(patient); + setPatientSidebarOpen(false); + clearResults(); + }, + showSuccessToast: true, + }); + + const inputRef = useRef(null); + + // Focus input when sidebar opens + useEffect(() => { + if (patientSidebarOpen) { + // Small delay to ensure DOM is ready + const timer = setTimeout(() => { + inputRef.current?.focus(); + }, 100); + return () => clearTimeout(timer); + } + }, [patientSidebarOpen]); + + // Close on Escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && patientSidebarOpen) { + e.preventDefault(); + setPatientSidebarOpen(false); + clearResults(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [patientSidebarOpen, setPatientSidebarOpen, clearResults]); + + // Don't render if closed + if (!patientSidebarOpen) return null; + + const showResults = query.length >= 2; + const showRecent = !showResults && recentPatients.length > 0; + + // Map DB patient to search result format for PatientListItem + const mapPatientToSearchResult = (patient: typeof recentPatients[0]): PatientSearchResult => ({ + id: patient.id, + name: `${patient.name_given?.[0] || ''} ${patient.name_family || ''}`.trim() || 'Onbekend', + birthDate: patient.birth_date || '', + identifier_bsn: patient.identifier_bsn || undefined, + identifier_client_number: patient.identifier_client_number || undefined, + matchScore: 1, + }); + + const handleBackdropClick = () => { + setPatientSidebarOpen(false); + clearResults(); + }; + + return ( + <> + {/* Backdrop */} +