From 0dc821c61d706fc448643e07e98f65e38fa53c40 Mon Sep 17 00:00:00 2001 From: maniamartial Date: Tue, 9 Jun 2026 16:52:47 +0300 Subject: [PATCH] feat: Handle the nursing issues --- .../clinicalNotes/ClinicalNotesList.tsx | 60 +- .../discharges/DischargeDetailPanel.tsx | 625 ++++++++++++++++++ .../components/discharges/DischargeList.tsx | 26 +- .../EnvironmentalChecklistDetailPanel.tsx | 325 +++++++++ .../EnvironmentalChecklistList.tsx | 71 +- .../src/components/labTests/LabTestList.tsx | 260 ++++++-- .../medication/LongActingMedicineList.tsx | 210 +++--- .../ReceptionLongActingMedicineList.tsx | 141 ++-- .../components/nurseTask/NurseTaskList.tsx | 217 ++++-- .../nursing/CreateMentalStateModal.tsx | 113 +++- .../nursing/GroomingChartDetailPanel.tsx | 294 ++++++++ .../components/nursing/GroomingChartList.tsx | 457 +++++++------ .../nursing/MainNursingNoteList.tsx | 225 ++++++- .../nursing/MentalStateDetailPanel.tsx | 324 +++++++++ .../components/nursing/MentalStateList.tsx | 482 +++++++------- .../nursing/SickLeaveDetailPanel.tsx | 211 ++++++ .../src/components/nursing/SickLeaveList.tsx | 370 ++++++----- .../CreatePatientAssessmentModal.tsx | 37 ++ .../PatientAssessmentDetailPanel.tsx | 378 +++++++++++ .../PatientAssessmentList.tsx | 456 ++++++------- .../serviceRequests/ServiceRequestList.tsx | 83 ++- .../sleeping/SleepingPatternDetail.tsx | 43 +- .../sleeping/SleepingPatternDetailPanel.tsx | 331 ++++++++++ .../sleeping/SleepingPatternList.tsx | 562 +++++++++------- .../components/vitalSigns/VitalSignsList.tsx | 408 ++++++++---- .../warnings/WarningMessageDetailPanel.tsx | 329 +++++++++ .../warnings/WarningMessagesList.tsx | 49 +- frontend/src/pages/Nurse.tsx | 211 ++---- frontend/src/services/common.ts | 35 + frontend/src/services/discharges.ts | 71 +- .../src/services/environmentalChecklist.ts | 15 +- frontend/src/services/groomingCharts.ts | 29 +- frontend/src/services/labTests.ts | 69 +- frontend/src/services/mainNursingNote.ts | 12 +- frontend/src/services/mentalState.ts | 28 +- frontend/src/services/nurseTask.ts | 6 + frontend/src/services/patientAssessment.ts | 72 +- frontend/src/services/serviceRequests.ts | 22 +- frontend/src/services/sickLeave.ts | 18 +- frontend/src/services/sleepingPattern.ts | 28 +- frontend/src/services/vitalSigns.ts | 12 +- frontend/src/services/warningMessages.ts | 31 +- healthcare/api/common.py | 297 ++++++++- healthcare/api/discharge.py | 33 +- healthcare/api/inpatient_admission.py | 1 + healthcare/api/lab_test.py | 314 ++++++++- healthcare/api/nurse_task.py | 25 +- healthcare/api/sleeping_pattern.py | 71 +- healthcare/api/vital_signs.py | 14 +- healthcare/api/warning_message.py | 105 ++- .../healthcare/api/environmental_checklist.py | 42 ++ .../ip_grooming_chart/ip_grooming_chart.json | 26 +- .../doctype/ip_service/ip_service.py | 26 +- .../healthcare/doctype/lab_test/lab_test.json | 14 +- .../lab_test_sample_instance.json | 5 +- .../lab_test_template/lab_test_template.json | 16 +- .../doctype/mental_state/mental_state.json | 26 +- .../doctype/nurse_task/nurse_task.json | 38 +- .../patient_assessment.json | 9 +- .../doctype/sick_leave/sick_leave.json | 26 +- .../public/frontend/assets/index-B6CWSqlB.js | 117 ---- ...{index-BwYnwq9z.css => index-BaCUbtzS.css} | 2 +- .../public/frontend/assets/index-CDg6RO-V.js | 117 ++++ healthcare/public/frontend/index.html | 4 +- healthcare/www/health_frontend.html | 4 +- 65 files changed, 7045 insertions(+), 2033 deletions(-) create mode 100644 frontend/src/components/discharges/DischargeDetailPanel.tsx create mode 100644 frontend/src/components/environmental/EnvironmentalChecklistDetailPanel.tsx create mode 100644 frontend/src/components/nursing/GroomingChartDetailPanel.tsx create mode 100644 frontend/src/components/nursing/MentalStateDetailPanel.tsx create mode 100644 frontend/src/components/nursing/SickLeaveDetailPanel.tsx create mode 100644 frontend/src/components/patientAssessment/PatientAssessmentDetailPanel.tsx create mode 100644 frontend/src/components/sleeping/SleepingPatternDetailPanel.tsx create mode 100644 frontend/src/components/warnings/WarningMessageDetailPanel.tsx delete mode 100644 healthcare/public/frontend/assets/index-B6CWSqlB.js rename healthcare/public/frontend/assets/{index-BwYnwq9z.css => index-BaCUbtzS.css} (56%) create mode 100644 healthcare/public/frontend/assets/index-CDg6RO-V.js diff --git a/frontend/src/components/clinicalNotes/ClinicalNotesList.tsx b/frontend/src/components/clinicalNotes/ClinicalNotesList.tsx index 98f2277ff2..5c1de35d76 100644 --- a/frontend/src/components/clinicalNotes/ClinicalNotesList.tsx +++ b/frontend/src/components/clinicalNotes/ClinicalNotesList.tsx @@ -23,6 +23,21 @@ interface ClinicalNotesListProps { noteType?: string hideTypes?: boolean onPatientClick?: (patient: string) => void + title?: string + onAdd?: () => void + addButtonTitle?: string +} + +function resolveListTitle(clinicalNoteType?: string, title?: string): string { + if (title) return title + if (!clinicalNoteType) return 'Clinical Notes' + if (clinicalNoteType.endsWith(' Note')) { + return clinicalNoteType.replace(/ Note$/, ' Notes') + } + if (clinicalNoteType.endsWith(' Order')) { + return clinicalNoteType.replace(/ Order$/, ' Orders') + } + return clinicalNoteType } export const ClinicalNotesList = ({ @@ -31,7 +46,12 @@ export const ClinicalNotesList = ({ noteType, hideTypes = false, onPatientClick, + title, + onAdd, + addButtonTitle, }: ClinicalNotesListProps) => { + const listTitle = resolveListTitle(clinicalNoteType, title) + const resolvedAddTitle = addButtonTitle ?? `Add ${clinicalNoteType || 'Clinical Note'}` const { mode, activeVisit, activeAdmission } = useCareContext() const [clinicalNotes, setClinicalNotes] = useState([]) const [pendingEncounters, setPendingEncounters] = useState([]) @@ -469,18 +489,34 @@ export const ClinicalNotesList = ({ return (
- {!inDashboardCard && (Boolean(patient) || applyDefaultPractitionerFilter) && ( -
- + {!inDashboardCard && ( +
+

{listTitle}

+
+ {(Boolean(patient) || applyDefaultPractitionerFilter) && ( + + )} + {onAdd && ( + + )} +
)} diff --git a/frontend/src/components/discharges/DischargeDetailPanel.tsx b/frontend/src/components/discharges/DischargeDetailPanel.tsx new file mode 100644 index 0000000000..ca97e6b09b --- /dev/null +++ b/frontend/src/components/discharges/DischargeDetailPanel.tsx @@ -0,0 +1,625 @@ +import { useEffect, useMemo, useState, type ReactNode } from 'react' +import { + Building2, + Calendar, + Check, + ClipboardCheck, + ClipboardList, + FileText, + Link2, + LogOut, + Stethoscope, + User, + Users, + X, +} from 'lucide-react' +import { + fetchDischarge, + type Discharge, + type DischargeChecklistRow, + type DischargeDoc, + type DischargePatientDocument, + type DischargePatientRelative, +} from '../../services/discharges' +import { DetailSlideOver } from '../ui/DetailSlideOver' +import { PrintFormatDropdown } from '../ui/PrintFormatDropdown' +import { MODAL_SECTION_CLASS, MODAL_SECTION_TITLE_CLASS } from '../ui/CreateModalChrome' +import { RichTextContent } from '../ui/RichTextContent' + +interface DischargeDetailPanelProps { + name: string + onClose: () => void + preview?: Discharge + onPatientClick?: (patient: string) => void +} + +function displayValue(value: unknown): string { + if (value == null || value === '') return '—' + return String(value) +} + +function formatDate(value?: string | null): string { + if (!value) return '—' + try { + return new Date(value).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }) + } catch { + return value + } +} + +function formatDateTime(value?: string | null): string { + if (!value) return '—' + try { + return new Date(value).toLocaleString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } catch { + return value + } +} + +function formatTime(value?: string | null): string { + if (!value) return '—' + if (value.includes('T') || value.includes('-')) { + return formatDateTime(value) + } + return value +} + +function isChecked(value: unknown): boolean { + return value === 1 || value === true || value === '1' +} + +function statusLabel(docstatus: number | undefined): { text: string; className: string } { + if (docstatus === 1) { + return { text: 'Submitted', className: 'bg-emerald-100 text-emerald-800' } + } + if (docstatus === 2) { + return { text: 'Cancelled', className: 'bg-red-100 text-red-800' } + } + return { text: 'Draft', className: 'bg-amber-100 text-amber-800' } +} + +function checklistStats(rows: DischargeChecklistRow[] | undefined) { + const list = rows ?? [] + const completed = list.filter((row) => isChecked(row.click)).length + const total = list.length + const pct = total > 0 ? Math.round((completed / total) * 100) : null + return { completed, total, pct } +} + +function InfoTile({ + icon, + label, + value, + onClick, +}: { + icon: ReactNode + label: string + value: string + onClick?: () => void +}) { + const valueEl = ( +

+ {value} +

+ ) + + return ( +
+
{icon}
+
+

{label}

+ {onClick ? ( + + ) : ( + valueEl + )} +
+
+ ) +} + +function ClinicalBlock({ title, value }: { title: string; value?: string | null }) { + if (!value?.trim()) return null + return ( +
+

{title}

+ +
+ ) +} + +function ChecklistSection({ + title, + icon, + rows, + loading, +}: { + title: string + icon: ReactNode + rows: DischargeChecklistRow[] + loading: boolean +}) { + const { completed, total, pct } = checklistStats(rows) + + if (!loading && total === 0) return null + + return ( +
+
+ {icon} +

{title}

+ {pct != null ? ( + = 100 + ? 'bg-emerald-100 text-emerald-800' + : pct >= 50 + ? 'bg-amber-100 text-amber-800' + : 'bg-slate-100 text-slate-700' + }`} + > + {completed} / {total} ({pct}%) + + ) : null} +
+ + {total > 0 ? ( +
+
+
+ ) : null} + + {loading && total === 0 ? ( +

Loading checklist…

+ ) : total === 0 ? ( +

No checklist items.

+ ) : ( +
    + {rows.map((row, index) => { + const checked = isChecked(row.click) + return ( +
  • +
    + {checked ? ( + + ) : ( + + )} +
    +

    + {row.action_required || '—'} +

    +
    + {row.department ? Dept: {row.department} : null} + {row.user ? User: {row.user} : null} + {row.name1 ? {row.name1} : null} + {row.date_time ? {formatDateTime(row.date_time)} : null} +
    + {row.description ? ( +
    + +
    + ) : null} +
    +
    +
  • + ) + })} +
+ )} +
+ ) +} + +function DocumentsSection({ documents }: { documents: DischargePatientDocument[] }) { + if (documents.length === 0) return null + + return ( +
+
+ +

Documents

+
+
    + {documents.map((doc, index) => { + const label = doc.file_name || doc.document_type || 'Document' + const url = doc.document + return ( +
  • +
    +

    {label}

    +
    + {doc.document_type ? Type: {doc.document_type} : null} + {doc.transaction_no != null && doc.transaction_no !== '' ? ( + Txn: {doc.transaction_no} + ) : null} +
    + {doc.upload_remarks ? ( +

    {doc.upload_remarks}

    + ) : null} +
    + {url ? ( + + Open + + ) : null} +
  • + ) + })} +
+
+ ) +} + +function RelativesSection({ relatives }: { relatives: DischargePatientRelative[] }) { + if (relatives.length === 0) return null + + return ( +
+
+ +

Patient relatives

+
+
    + {relatives.map((relative, index) => ( +
  • +

    {relative.relative_name || '—'}

    +
    + {relative.relationship_with_patient ? ( + {relative.relationship_with_patient} + ) : null} + {relative.relative_phone_no ? {relative.relative_phone_no} : null} + {relative.cpr__id_no ? CPR: {relative.cpr__id_no} : null} +
    + {relative.any_remarks ? ( +

    {relative.any_remarks}

    + ) : null} +
  • + ))} +
+
+ ) +} + +export function DischargeDetailPanel({ + name, + onClose, + preview, + onPatientClick, +}: DischargeDetailPanelProps) { + const [doc, setDoc] = useState(preview ? { ...preview, name } : null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + setLoading(true) + setError(null) + fetchDischarge(name) + .then((data) => { + if (!cancelled) setDoc(data) + }) + .catch((err) => { + if (!cancelled) { + setError(err instanceof Error ? err.message : 'Failed to load discharge') + } + }) + .finally(() => { + if (!cancelled) setLoading(false) + }) + return () => { + cancelled = true + } + }, [name]) + + const source = doc ?? preview + const status = statusLabel(doc?.docstatus ?? preview?.docstatus) + + const headerSubtitle = useMemo(() => { + if (!source) return name + const parts = [ + source.patient_name || source.file_no, + source.discharge_type, + source.discharge_date ? formatDate(source.discharge_date) : null, + ].filter(Boolean) + return parts.length ? parts.join(' · ') : name + }, [source, name]) + + const clinicalFields = doc + ? [ + { title: 'Discharge diagnosis', value: doc.discharge_diagnosis }, + { title: 'Discharge reason', value: doc.discharge_reason }, + { title: 'Treatment plan', value: doc.discharge_treatment_plan }, + { title: 'Conditions', value: doc.discharge_conditions }, + { title: 'Instructions', value: doc.discharge_instructions }, + { title: 'Mental status summary', value: doc.final_exam_mental_status_summary }, + { title: 'Management in hospital', value: doc.management_in_hospital }, + { title: 'Prognosis', value: doc.prognosis }, + ].filter((field) => field.value?.trim()) + : [] + + const hasClinical = clinicalFields.length > 0 + const dischargeChecklist = doc?.discharge_checklist ?? [] + const nursingChecklist = doc?.nursing_checklist ?? [] + const documents = doc?.patient_documents ?? [] + const relatives = doc?.patient_relatives ?? [] + const hasFollowUp = Boolean(doc?.next_appointment_date || doc?.next_appointment_time) + + return ( + } + onClose={onClose} + maxWidthClass="max-w-2xl" + headerActions={ + + } + > + {loading && !source ? ( +
+ + Loading discharge… +
+ ) : null} + + {error ? ( +
{error}
+ ) : null} + + {source && !error ? ( +
+
+
+ +

Discharge summary

+ + {status.text} + +
+
+
+

Discharge type

+

+ {displayValue(doc?.discharge_type || preview?.discharge_type)} +

+
+ {doc?.ama_type ? ( +
+

AMA type

+

{doc.ama_type}

+
+ ) : null} +
+

Discharge date

+

+ {formatDate(doc?.discharge_date || preview?.discharge_date)} + {doc?.discharge_time ? ` · ${formatTime(doc.discharge_time)}` : ''} +

+
+ {doc?.final_discharge_date ? ( +
+

Final discharge

+

+ {formatDate(doc.final_discharge_date)} + {doc.final_discharge_time ? ` · ${formatTime(doc.final_discharge_time)}` : ''} +

+
+ ) : null} +
+
+ + {hasClinical ? ( +
+
+ +

Clinical information

+
+
+ {clinicalFields.map((field) => ( + + ))} +
+
+ ) : null} + + } + rows={dischargeChecklist} + loading={loading} + /> + + } + rows={nursingChecklist} + loading={loading} + /> + + + + + + {hasFollowUp ? ( +
+
+ +

Follow-up

+
+
+
+

Next appointment

+

+ {formatDate(doc?.next_appointment_date)} + {doc?.next_appointment_time ? ` · ${formatTime(doc.next_appointment_time)}` : ''} +

+
+
+
+ ) : null} + +
+

+ + Details +

+
+ } + label="Patient" + value={displayValue(doc?.patient_name || doc?.file_no || preview?.patient_name || preview?.file_no)} + onClick={ + (doc?.file_no || preview?.file_no) && onPatientClick + ? () => onPatientClick((doc?.file_no || preview?.file_no)!) + : undefined + } + /> + } + label="Admission" + value={displayValue(doc?.admission || preview?.admission)} + /> + } + label="Discharge date" + value={formatDate(doc?.discharge_date || preview?.discharge_date)} + /> + } + label="Discharge type" + value={displayValue(doc?.discharge_type || preview?.discharge_type)} + /> + } + label="Discharged by" + value={displayValue( + doc?.user_name || + doc?.discharged_by_user_name || + doc?.discharged_by_user || + preview?.discharged_by_user_name || + preview?.discharged_by_user + )} + /> + } + label="Final discharge by" + value={displayValue( + doc?.final_discharger_username || + doc?.final_discharge_user_name || + doc?.final_discharge_user_id || + preview?.final_discharge_user_name || + preview?.final_discharge_user_id + )} + /> + } + label="Receiving doctor" + value={displayValue( + doc?.receiving_doctor_name || + doc?.receiving_doctors || + preview?.receiving_doctor_name || + preview?.receiving_doctors + )} + /> + {doc?.discharge_doctor ? ( + } + label="Discharge doctor" + value={displayValue(doc.discharge_doctor)} + /> + ) : null} + {doc?.discharge_nurse ? ( + } + label="Discharge nurse" + value={displayValue(doc.discharge_nurse)} + /> + ) : null} + {doc?.discharge_receptionist ? ( + } + label="Receptionist" + value={displayValue(doc.discharge_receptionist)} + /> + ) : null} + {doc?.discharge_template || preview?.discharge_template ? ( + } + label="Discharge template" + value={displayValue( + doc?.template_name || doc?.discharge_template || preview?.template_name || preview?.discharge_template + )} + /> + ) : null} + {doc?.nurse_discharge_template ? ( + } + label="Nursing template" + value={displayValue(doc.nurse_discharge_template)} + /> + ) : null} + {doc?.cost_center || preview?.cost_center ? ( + } + label="Cost center" + value={displayValue(doc?.cost_center || preview?.cost_center)} + /> + ) : null} + } + label="Discharge ID" + value={displayValue(doc?.name || preview?.name || name)} + /> +
+
+ + {doc?.creation ? ( +

+ Recorded {formatDateTime(doc.creation)} + {doc.modified && doc.modified !== doc.creation ? ` · Updated ${formatDateTime(doc.modified)}` : ''} +

+ ) : null} +
+ ) : null} +
+ ) +} diff --git a/frontend/src/components/discharges/DischargeList.tsx b/frontend/src/components/discharges/DischargeList.tsx index 7569a5ce72..abf69c9dd8 100644 --- a/frontend/src/components/discharges/DischargeList.tsx +++ b/frontend/src/components/discharges/DischargeList.tsx @@ -5,8 +5,7 @@ import { fetchInpatientRecords } from '../../services/inpatientRecords' import { StatusPill } from '../ui/StatusPill' import { PrintFormatDropdown } from '../ui/PrintFormatDropdown' import { PortalActionsMenu } from '../ui/PortalActionsMenu' -import { DetailSlideOver } from '../ui/DetailSlideOver' -import { DocDetailView } from '../ui/DocDetailView' +import { DischargeDetailPanel } from './DischargeDetailPanel' import { useCareContext } from '../../providers/CareContextProvider' import { PaginationControls, DEFAULT_PAGE_SIZE, type PageSize } from '../ui/PaginationControls' import { useCardFilters } from '../../contexts/CardFilterContext' @@ -40,7 +39,7 @@ export const DischargeList = ({ patient, admission, onPatientClick }: DischargeL const [discharges, setDischarges] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const [detailName, setDetailName] = useState(null) + const [detailRow, setDetailRow] = useState(null) const [statusFilter, setStatusFilter] = useState('') const [typeFilter, setTypeFilter] = useState('') const [admissionFilter, setAdmissionFilter] = useState('') @@ -459,7 +458,7 @@ export const DischargeList = ({ patient, admission, onPatientClick }: DischargeL setDetailName(discharge.name)} + onClick={() => setDetailRow(discharge)} > {discharge.name} @@ -530,7 +529,7 @@ export const DischargeList = ({ patient, admission, onPatientClick }: DischargeL
- {detailName && ( - setDetailName(null)} - > - - - )} + {detailRow ? ( + setDetailRow(null)} + onPatientClick={onPatientClick} + /> + ) : null}
) } \ No newline at end of file diff --git a/frontend/src/components/environmental/EnvironmentalChecklistDetailPanel.tsx b/frontend/src/components/environmental/EnvironmentalChecklistDetailPanel.tsx new file mode 100644 index 0000000000..d58b642b4e --- /dev/null +++ b/frontend/src/components/environmental/EnvironmentalChecklistDetailPanel.tsx @@ -0,0 +1,325 @@ +import { useEffect, useMemo, useState, type ReactNode } from 'react' +import { + Building2, + Calendar, + Check, + ClipboardCheck, + ClipboardList, + FileText, + Link2, + Stethoscope, + User, + X, +} from 'lucide-react' +import { + fetchEnvironmentalChecklist, + type EnvironmentalChecklistRecord, +} from '../../services/environmentalChecklist' +import { DetailSlideOver } from '../ui/DetailSlideOver' +import { PrintFormatDropdown } from '../ui/PrintFormatDropdown' +import { MODAL_SECTION_CLASS, MODAL_SECTION_TITLE_CLASS } from '../ui/CreateModalChrome' + +interface EnvironmentalChecklistDetailPanelProps { + name: string + onClose: () => void + preview?: EnvironmentalChecklistRecord + onPatientClick?: (patient: string) => void + onEdit?: (name: string) => void +} + +function displayValue(value: unknown): string { + if (value == null || value === '') return '—' + return String(value) +} + +function formatDateTime(value?: string | null): string { + if (!value) return '—' + try { + return new Date(value).toLocaleString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } catch { + return value + } +} + +function careContextLabel(doc: EnvironmentalChecklistRecord): string { + if (doc.inpatient_admission) return `Inpatient · ${doc.inpatient_admission}` + if (doc.patient_visit) return `Outpatient visit · ${doc.patient_visit}` + return '—' +} + +function InfoTile({ + icon, + label, + value, + onClick, +}: { + icon: ReactNode + label: string + value: string + onClick?: () => void +}) { + const valueEl = ( +

+ {value} +

+ ) + + return ( +
+
{icon}
+
+

{label}

+ {onClick ? ( + + ) : ( + valueEl + )} +
+
+ ) +} + +export function EnvironmentalChecklistDetailPanel({ + name, + onClose, + preview, + onPatientClick, + onEdit, +}: EnvironmentalChecklistDetailPanelProps) { + const [doc, setDoc] = useState( + preview ? { ...preview, name } : null + ) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + setLoading(true) + setError(null) + fetchEnvironmentalChecklist(name) + .then((data) => { + if (!cancelled) setDoc(data) + }) + .catch((err) => { + if (!cancelled) { + setError(err instanceof Error ? err.message : 'Failed to load environmental checklist') + } + }) + .finally(() => { + if (!cancelled) setLoading(false) + }) + return () => { + cancelled = true + } + }, [name]) + + const source = doc ?? preview + const details = doc?.details ?? [] + const completed = doc?.completed_count ?? preview?.completed_count ?? 0 + const total = doc?.total_count ?? preview?.total_count ?? details.length + const pct = total > 0 ? Math.round((completed / total) * 100) : null + + const headerSubtitle = useMemo(() => { + if (!source) return name + const parts = [ + source.patient_name || source.patient, + source.creation ? formatDateTime(source.creation) : null, + source.environmental_checklist_template, + ].filter(Boolean) + return parts.length ? parts.join(' · ') : name + }, [source, name]) + + return ( + } + onClose={onClose} + maxWidthClass="max-w-2xl" + headerActions={ +
+ {onEdit ? ( + + ) : null} + +
+ } + > + {loading && details.length === 0 ? ( +
+ + Loading checklist… +
+ ) : null} + + {error ? ( +
{error}
+ ) : null} + + {source && !error ? ( +
+ {total > 0 ? ( +
+
+ +

Progress

+ {pct != null ? ( + = 100 + ? 'bg-emerald-100 text-emerald-800' + : pct >= 50 + ? 'bg-amber-100 text-amber-800' + : 'bg-slate-100 text-slate-700' + }`} + > + {completed} / {total} ({pct}%) + + ) : null} +
+
+
+
+
+ ) : null} + +
+
+ +

Checklist

+
+ + {loading && details.length === 0 ? ( +

Loading items…

+ ) : details.length === 0 ? ( +

No checklist items.

+ ) : ( +
    + {details.map((item) => ( +
  • + {item.checked ? ( + + ) : ( + + )} + + {item.item_name || '—'} + +
  • + ))} +
+ )} +
+ +
+

+ + Details +

+
+ } + label="Patient" + value={displayValue(doc?.patient_name || doc?.patient || preview?.patient_name || preview?.patient)} + onClick={ + (doc?.patient || preview?.patient) && onPatientClick + ? () => onPatientClick((doc?.patient || preview?.patient)!) + : undefined + } + /> + } + label="Practitioner" + value={displayValue( + doc?.practitioner_name || doc?.practitioner || preview?.practitioner_name || preview?.practitioner + )} + /> + } + label="Created" + value={formatDateTime(doc?.creation || preview?.creation)} + /> + } + label="Template" + value={displayValue( + doc?.environmental_checklist_template || preview?.environmental_checklist_template + )} + /> + {(() => { + const contextSource = doc ?? preview + const careContext = contextSource ? careContextLabel(contextSource) : '—' + return careContext !== '—' ? ( + } + label="Care context" + value={careContext} + /> + ) : null + })()} + {doc?.cost_center || preview?.cost_center ? ( + } + label="Cost center" + value={displayValue(doc?.cost_center || preview?.cost_center)} + /> + ) : null} + } + label="Checklist ID" + value={displayValue(doc?.name || preview?.name || name)} + /> +
+
+ + {doc?.creation ? ( +

+ Recorded {formatDateTime(doc.creation)} + {doc.modified && doc.modified !== doc.creation + ? ` · Updated ${formatDateTime(doc.modified)}` + : ''} +

+ ) : null} +
+ ) : null} +
+ ) +} diff --git a/frontend/src/components/environmental/EnvironmentalChecklistList.tsx b/frontend/src/components/environmental/EnvironmentalChecklistList.tsx index 2b727c792a..81fbe4999f 100644 --- a/frontend/src/components/environmental/EnvironmentalChecklistList.tsx +++ b/frontend/src/components/environmental/EnvironmentalChecklistList.tsx @@ -1,5 +1,4 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import { ClipboardCheck } from 'lucide-react' import { fetchInpatientAdmissionOptions } from '../../services/common' import { fetchEnvironmentalChecklists, @@ -12,8 +11,7 @@ import { EnvironmentalChecklistModal } from './EnvironmentalChecklistModal' import { PrintFormatDropdown } from '../ui/PrintFormatDropdown' import { ClearFiltersButton } from '../ui/ClearFiltersButton' import { PortalActionsMenu } from '../ui/PortalActionsMenu' -import { DetailSlideOver } from '../ui/DetailSlideOver' -import { DocDetailView } from '../ui/DocDetailView' +import { EnvironmentalChecklistDetailPanel } from './EnvironmentalChecklistDetailPanel' interface EnvironmentalChecklistListProps { patient?: string @@ -70,7 +68,7 @@ export const EnvironmentalChecklistList = ({ const [error, setError] = useState(null) const [internalCreateOpen, setInternalCreateOpen] = useState(false) const [editChecklist, setEditChecklist] = useState(null) - const [detailName, setDetailName] = useState(null) + const [detailRow, setDetailRow] = useState(null) const [openActionRow, setOpenActionRow] = useState(null) const actionMenuRef = useRef(null) @@ -147,14 +145,14 @@ export const EnvironmentalChecklistList = ({ loadRows() } - const handleView = (name: string) => { + const handleView = (row: EnvironmentalChecklistRecord) => { setOpenActionRow(null) - setDetailName(name) + setDetailRow(row) } const handleEdit = (name: string) => { setOpenActionRow(null) - setDetailName(null) + setDetailRow(null) setEditChecklist(name) } @@ -259,7 +257,7 @@ export const EnvironmentalChecklistList = ({ handleView(row.name)} + onClick={() => handleView(row)} > {row.name} @@ -275,43 +273,43 @@ export const EnvironmentalChecklistList = ({ )} handleView(row.name)} + onClick={() => handleView(row)} > {row.inpatient_admission || '—'} handleView(row.name)} + onClick={() => handleView(row)} > {row.patient_visit || '—'} handleView(row.name)} + onClick={() => handleView(row)} > {row.practitioner_name || row.practitioner || '—'} handleView(row.name)} + onClick={() => handleView(row)} > {row.cost_center || '—'} handleView(row.name)} + onClick={() => handleView(row)} > {row.environmental_checklist_template || '—'} handleView(row.name)} + onClick={() => handleView(row)} > {row.completed_count ?? 0} / {row.total_count ?? 0} handleView(row.name)} + onClick={() => handleView(row)} > {row.creation ? new Date(row.creation).toLocaleDateString() : '—'} @@ -336,7 +334,7 @@ export const EnvironmentalChecklistList = ({ > - - - } - > - - - )} + {detailRow ? ( + setDetailRow(null)} + onPatientClick={onPatientClick} + onEdit={handleEdit} + /> + ) : null} {showCreateModal && ( (null) // Track if create sample modal is open to close the parent modal const [isCreatingSample, setIsCreatingSample] = useState(false) + const [isEditingSample, setIsEditingSample] = useState(false) + const [sampleFormQty, setSampleFormQty] = useState('') + const [sampleFormDetails, setSampleFormDetails] = useState('') + const [sampleFormLoadData, setSampleFormLoadData] = useState(false) + const [adHocSampleQty, setAdHocSampleQty] = useState('') + const [adHocSampleDetails, setAdHocSampleDetails] = useState('') + const [adHocCreateLoading, setAdHocCreateLoading] = useState(false) + + const resetSampleFormState = () => { + setSampleFormRowIndex(null) + setSampleFormError(null) + setSampleFormCollectionPoint('') + setSampleFormRefPractitioner('') + setRefPractitionerQuery('') + setRefPractitionerOptions([]) + setRefPractitionerOpen(false) + setSampleObsRows([]) + setTemplateSampleDetails('') + setIsCreatingSample(false) + setIsEditingSample(false) + setSampleFormQty('') + setSampleFormDetails('') + setSampleFormLoadData(false) + } + + const resetSampleModalState = () => { + resetSampleFormState() + setSampleModalLabTest(null) + setSampleModalError(null) + setAdHocSampleQty('') + setAdHocSampleDetails('') + setAdHocCreateLoading(false) + } + + const handleOpenSampleEdit = async (idx: number) => { + if (!sampleModalLabTest) return + const row = sampleModalLabTest.sample_instances?.[idx] + if (!row?.sample_collection) return + + setSampleFormError(null) + setSampleFormRowIndex(idx) + setIsCreatingSample(false) + setIsEditingSample(true) + setSampleFormLoadData(true) + + try { + const data = await getSampleCollectionForLabSample(sampleModalLabTest.name, idx) + setSampleFormQty(data.sample_qty != null ? String(data.sample_qty) : '') + setSampleFormDetails(data.sample_details || row.sample_details || '') + setSampleFormCollectionPoint(data.collection_point || '') + setSampleFormRefPractitioner(data.referring_practitioner || '') + setRefPractitionerQuery(data.referring_practitioner_name || data.referring_practitioner || '') + setSampleObsRows(data.observation_rows?.length ? data.observation_rows : []) + } catch (e) { + const msg = e instanceof Error ? e.message : 'Failed to load sample collection' + setSampleFormError(msg) + toast.error(msg) + resetSampleFormState() + } finally { + setSampleFormLoadData(false) + } + } + + const handleAdHocSampleCreate = async () => { + if (!sampleModalLabTest) return + try { + setAdHocCreateLoading(true) + setSampleModalError(null) + const qty = adHocSampleQty.trim() === '' ? undefined : parseFloat(adHocSampleQty) + const res = await createSampleCollectionForLabSample( + sampleModalLabTest.name, + 0, + adHocSampleDetails.trim() || undefined, + undefined, + undefined, + undefined, + qty + ) + toast.success(`Sample Collection ${res.sample_collection} created`) + await refetch() + resetSampleModalState() + } catch (e) { + const msg = e instanceof Error ? e.message : 'Failed to create Sample Collection' + setSampleModalError(msg) + toast.error(msg) + } finally { + setAdHocCreateLoading(false) + } + } useEffect(() => { const loadPractitioners = async () => { @@ -3177,7 +3269,7 @@ export const LabTestList = ({ {/* ── Sample Collection Modal with instructions visible by default ── */} - {sampleModalLabTest && !isCreatingSample && ( + {sampleModalLabTest && !isCreatingSample && !isEditingSample && (
@@ -3185,7 +3277,7 @@ export const LabTestList = ({

Sample Collection — {sampleModalLabTest.name}

Patient: {sampleModalLabTest.patient_name || sampleModalLabTest.patient || '-'}{sampleModalLabTest.lab_test_name ? ` · ${sampleModalLabTest.lab_test_name}` : ''}

-
@@ -3194,9 +3286,51 @@ export const LabTestList = ({ {!sampleModalLoading && !sampleModalError && ( <> {(!sampleModalLabTest.sample_instances || sampleModalLabTest.sample_instances.length === 0) ? ( -
- No sample instances are defined for this lab test. Please configure Sample Requirements on the Lab Test Template. -
+ + + + + + + + + + + + + +
SampleQtyCollection InstructionsSample Collection
+ setAdHocSampleQty(e.target.value)} + placeholder="Qty" + className="w-20 border border-slate-300 rounded px-2 py-1.5 text-sm focus:ring-1 focus:ring-primary focus:outline-none" + /> + +