Skip to content

Commit c7ba685

Browse files
committed
feat: implement shift report with history, rvu and sdoh metrics
1 parent 18d369a commit c7ba685

11 files changed

Lines changed: 11518 additions & 7 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to the Halkyone Clinical OS will be documented in this file.
44

5+
## [1.3.7] - 2026-05-22
6+
- Implemented Shift Report modal for practitioners and admins with dynamic database views.
7+
- Added date filtering capabilities for historical clinical shift reports.
8+
- Integrated simulated RVU calculations and SDOH tracking metrics into clinical reports.
9+
510
## [1.3.6] - 2026-05-22
611
- Fixed clinical alert case creation mapping in mobile patient chat (SendMobileChatMessageCommand).
712
- Resolved PostgreSQL DbUpdateException by dynamically assigning a default Practitioner during CareNavigationCase initialization.

emr-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "emr-client",
3-
"version": "1.3.6",
3+
"version": "1.3.7",
44
"private": true,
55
"scripts": {
66
"dev": "next dev -p 3671",

emr-client/src/app/dashboard/page.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { useState, useEffect } from "react";
3838
import { useRecentlyBrowsed } from "@/hooks/useRecentlyBrowsed";
3939
import BookingDrawer from "@/components/BookingDrawer";
4040
import AddPatientDrawer from "@/components/AddPatientDrawer";
41+
import { ShiftReportModal } from "@/components/ShiftReportModal";
4142
import { PermissionGate } from "@/components/PermissionGate";
4243
import {
4344
AreaChart,
@@ -128,6 +129,8 @@ export default function Dashboard() {
128129
const [greeting, setGreeting] = useState("Good morning");
129130
const [isBookingOpen, setIsBookingOpen] = useState(false);
130131
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
132+
const [isReportOpen, setIsReportOpen] = useState(false);
133+
const [reportAsAdmin, setReportAsAdmin] = useState(false);
131134

132135
useEffect(() => {
133136
const hour = new Date().getHours();
@@ -152,6 +155,7 @@ export default function Dashboard() {
152155
>
153156
<BookingDrawer open={isBookingOpen} onClose={() => setIsBookingOpen(false)} onBooked={() => { }} />
154157
<AddPatientDrawer open={isAddPatientOpen} onClose={() => setIsAddPatientOpen(false)} onSuccess={() => { }} />
158+
<ShiftReportModal open={isReportOpen} onClose={() => setIsReportOpen(false)} isAdminView={reportAsAdmin} />
155159
{/* Premium Hero Header - Compact */}
156160
<motion.div variants={itemVariants} className="relative overflow-hidden rounded-[2rem] bg-slate-900 border border-white/10 p-6 sm:p-8 shadow-xl">
157161
<div className="absolute top-0 right-0 w-[400px] h-[400px] bg-gradient-to-br from-teal-500/20 to-blue-600/20 blur-[100px] pointer-events-none" />
@@ -172,7 +176,7 @@ export default function Dashboard() {
172176
<div className="flex flex-wrap gap-2">
173177
<PermissionGate permission="scheduling:manage">
174178
<button
175-
onClick={() => router.push("/dashboard/schedule?action=new")}
179+
onClick={() => setIsBookingOpen(true)}
176180
className="px-5 h-10 rounded-xl bg-white text-slate-900 text-xs font-bold hover:bg-teal-50 transition-all flex items-center gap-2 shadow-xl active:scale-95"
177181
>
178182
<Plus className="w-4 h-4" />
@@ -181,7 +185,11 @@ export default function Dashboard() {
181185
</PermissionGate>
182186
<PermissionGate permission="analytics:view">
183187
<button
184-
onClick={() => showToast("Preparing clinical report...", "info")}
188+
onClick={() => {
189+
const isAdmin = session?.user?.roles?.includes('Admin') || false;
190+
setReportAsAdmin(isAdmin);
191+
setIsReportOpen(true);
192+
}}
185193
className="px-5 h-10 rounded-xl bg-white/5 border border-white/10 text-white text-xs font-bold hover:bg-white/10 transition-all flex items-center gap-2 backdrop-blur-md active:scale-95"
186194
>
187195
<BarChart3 className="w-4 h-4 text-teal-400" />
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
'use client';
2+
3+
import { motion, AnimatePresence } from 'framer-motion';
4+
import { X, FileText, ClipboardList, Stethoscope, Pill, CheckCircle2, AlertTriangle, TrendingUp, Download, Loader2 } from 'lucide-react';
5+
import { useEffect, useState } from 'react';
6+
import { useSession } from 'next-auth/react';
7+
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
8+
9+
interface ShiftReportModalProps {
10+
open: boolean;
11+
onClose: () => void;
12+
practitionerId?: string;
13+
isAdminView?: boolean;
14+
}
15+
16+
interface ReportData {
17+
totalEncounters: number;
18+
unsignedNotes: number;
19+
diagnosesAdded: number;
20+
prescriptionsAuthorized: number;
21+
vitalsLogged: number;
22+
medicationsAdministered: number;
23+
triageActionsResolved: number;
24+
casesTouched: number;
25+
sdohAssessmentsCompleted: number;
26+
barriersMitigated: number;
27+
simulatedRvus: number;
28+
}
29+
30+
export function ShiftReportModal({ open, onClose, practitionerId, isAdminView }: ShiftReportModalProps) {
31+
const { data: session } = useSession();
32+
const [loading, setLoading] = useState(true);
33+
const [data, setData] = useState<ReportData | null>(null);
34+
const [selectedDate, setSelectedDate] = useState<string>(new Date().toISOString().split('T')[0]);
35+
36+
useEffect(() => {
37+
if (open) {
38+
setLoading(true);
39+
40+
let url = `${process.env.NEXT_PUBLIC_API_URL}/api/Report/shift-summary`;
41+
42+
const queryParams = new URLSearchParams();
43+
if (selectedDate) {
44+
queryParams.append('date', selectedDate);
45+
}
46+
if (!isAdminView && practitionerId) {
47+
queryParams.append('practitionerId', practitionerId);
48+
} else if (!isAdminView && session?.user?.id) {
49+
queryParams.append('practitionerId', session.user.id);
50+
}
51+
52+
if (queryParams.toString()) {
53+
url += `?${queryParams.toString()}`;
54+
}
55+
56+
fetch(url, {
57+
headers: {
58+
'Authorization': `Bearer ${(session?.user as any)?.token || ''}`
59+
}
60+
})
61+
.then(res => res.json())
62+
.then((resData) => {
63+
setData(resData);
64+
setLoading(false);
65+
})
66+
.catch(err => {
67+
console.error(err);
68+
setLoading(false);
69+
});
70+
}
71+
}, [open, practitionerId, isAdminView, session, selectedDate]);
72+
73+
if (!open) return null;
74+
75+
const chartData = data ? [
76+
{ name: 'Encounters', value: data.totalEncounters, color: '#0ea5e9' },
77+
{ name: 'Unsigned', value: data.unsignedNotes, color: '#f43f5e' },
78+
{ name: 'Diagnoses', value: data.diagnosesAdded, color: '#8b5cf6' },
79+
{ name: 'Rx', value: data.prescriptionsAuthorized, color: '#10b981' },
80+
{ name: 'Vitals', value: data.vitalsLogged, color: '#f59e0b' }
81+
] : [];
82+
83+
return (
84+
<AnimatePresence>
85+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 overflow-hidden">
86+
<motion.div
87+
initial={{ opacity: 0 }}
88+
animate={{ opacity: 1 }}
89+
exit={{ opacity: 0 }}
90+
onClick={onClose}
91+
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
92+
/>
93+
94+
<motion.div
95+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
96+
animate={{ opacity: 1, scale: 1, y: 0 }}
97+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
98+
className="relative w-full max-w-4xl max-h-full flex flex-col glass-morphism rounded-2xl shadow-2xl overflow-hidden border border-white/10"
99+
>
100+
{/* Header */}
101+
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-white/5">
102+
<div className="flex items-center gap-3">
103+
<div className="w-10 h-10 rounded-xl bg-teal-500/20 flex items-center justify-center border border-teal-500/30">
104+
<FileText className="w-5 h-5 text-teal-400" />
105+
</div>
106+
<div>
107+
<h2 className="text-lg font-bold text-white tracking-tight">
108+
{isAdminView ? 'System-Wide Clinical Report' : 'Your Clinical Shift Report'}
109+
</h2>
110+
<div className="flex items-center gap-2 mt-1">
111+
<p className="text-xs text-white/50 uppercase tracking-widest font-bold">
112+
{new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
113+
</p>
114+
<input
115+
type="date"
116+
value={selectedDate}
117+
onChange={(e) => setSelectedDate(e.target.value)}
118+
className="bg-white/10 border border-white/20 text-white text-xs rounded px-2 py-1 outline-none focus:border-teal-500"
119+
/>
120+
</div>
121+
</div>
122+
</div>
123+
<div className="flex items-center gap-2">
124+
<button
125+
onClick={() => window.print()}
126+
className="p-2 rounded-lg hover:bg-white/10 transition-colors text-white/70 hover:text-white"
127+
title="Print / PDF"
128+
>
129+
<Download className="w-4 h-4" />
130+
</button>
131+
<button
132+
onClick={onClose}
133+
className="p-2 rounded-lg hover:bg-white/10 transition-colors text-white/70 hover:text-white"
134+
>
135+
<X className="w-5 h-5" />
136+
</button>
137+
</div>
138+
</div>
139+
140+
{/* Content */}
141+
<div className="p-6 overflow-y-auto custom-scrollbar">
142+
{loading ? (
143+
<div className="flex flex-col items-center justify-center py-20">
144+
<Loader2 className="w-8 h-8 text-teal-500 animate-spin mb-4" />
145+
<p className="text-sm font-bold text-white/50 uppercase tracking-widest">Aggregating Clinical Data...</p>
146+
</div>
147+
) : data ? (
148+
<div className="space-y-6">
149+
150+
{/* Highlights */}
151+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
152+
<div className="bg-white/5 border border-white/10 rounded-xl p-4">
153+
<div className="flex items-center gap-2 mb-2">
154+
<Stethoscope className="w-4 h-4 text-blue-400" />
155+
<span className="text-xs font-bold text-white/60 uppercase tracking-wider">Encounters</span>
156+
</div>
157+
<div className="text-3xl font-bold text-white">{data.totalEncounters}</div>
158+
</div>
159+
160+
<div className="bg-rose-500/10 border border-rose-500/20 rounded-xl p-4">
161+
<div className="flex items-center gap-2 mb-2">
162+
<AlertTriangle className="w-4 h-4 text-rose-400" />
163+
<span className="text-xs font-bold text-rose-200/60 uppercase tracking-wider">Unsigned Notes</span>
164+
</div>
165+
<div className="text-3xl font-bold text-rose-400">{data.unsignedNotes}</div>
166+
</div>
167+
168+
<div className="bg-white/5 border border-white/10 rounded-xl p-4">
169+
<div className="flex items-center gap-2 mb-2">
170+
<Pill className="w-4 h-4 text-emerald-400" />
171+
<span className="text-xs font-bold text-white/60 uppercase tracking-wider">Prescriptions</span>
172+
</div>
173+
<div className="text-3xl font-bold text-white">{data.prescriptionsAuthorized}</div>
174+
</div>
175+
176+
<div className="bg-teal-500/10 border border-teal-500/20 rounded-xl p-4">
177+
<div className="flex items-center gap-2 mb-2">
178+
<TrendingUp className="w-4 h-4 text-teal-400" />
179+
<span className="text-xs font-bold text-teal-200/60 uppercase tracking-wider">Simulated RVUs</span>
180+
</div>
181+
<div className="text-3xl font-bold text-teal-400">{data.simulatedRvus.toFixed(2)}</div>
182+
</div>
183+
</div>
184+
185+
{/* Chart and Secondary Stats */}
186+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
187+
<div className="md:col-span-2 bg-white/5 border border-white/10 rounded-xl p-4 h-64">
188+
<h3 className="text-xs font-bold text-white/60 uppercase tracking-wider mb-4">Activity Overview</h3>
189+
<ResponsiveContainer width="100%" height="100%">
190+
<BarChart data={chartData} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}>
191+
<XAxis dataKey="name" stroke="#ffffff40" fontSize={10} tickLine={false} axisLine={false} />
192+
<YAxis stroke="#ffffff40" fontSize={10} tickLine={false} axisLine={false} />
193+
<Tooltip
194+
cursor={{ fill: '#ffffff10' }}
195+
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px' }}
196+
itemStyle={{ color: '#fff', fontSize: '12px', fontWeight: 'bold' }}
197+
/>
198+
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
199+
{chartData.map((entry, index) => (
200+
<Cell key={`cell-${index}`} fill={entry.color} />
201+
))}
202+
</Bar>
203+
</BarChart>
204+
</ResponsiveContainer>
205+
</div>
206+
207+
<div className="space-y-4">
208+
<h3 className="text-xs font-bold text-white/60 uppercase tracking-wider mb-2">Ancillary Actions</h3>
209+
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/5">
210+
<span className="text-sm text-white/80">Vitals Logged</span>
211+
<span className="text-sm font-bold text-white">{data.vitalsLogged}</span>
212+
</div>
213+
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/5">
214+
<span className="text-sm text-white/80">Diagnoses Added</span>
215+
<span className="text-sm font-bold text-white">{data.diagnosesAdded}</span>
216+
</div>
217+
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/5">
218+
<span className="text-sm text-white/80">SDOH Assessments</span>
219+
<span className="text-sm font-bold text-white">{data.sdohAssessmentsCompleted}</span>
220+
</div>
221+
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/5">
222+
<span className="text-sm text-white/80">Barriers Mitigated</span>
223+
<span className="text-sm font-bold text-white">{data.barriersMitigated}</span>
224+
</div>
225+
</div>
226+
</div>
227+
228+
</div>
229+
) : (
230+
<div className="text-center text-white/50 py-10">Failed to load report data.</div>
231+
)}
232+
</div>
233+
234+
{/* Footer */}
235+
<div className="px-6 py-4 bg-white/5 border-t border-white/10 flex justify-end">
236+
<button
237+
onClick={onClose}
238+
className="px-6 py-2 rounded-xl bg-white text-slate-900 text-sm font-bold hover:bg-teal-50 transition-colors shadow-lg"
239+
>
240+
Close Report
241+
</button>
242+
</div>
243+
</motion.div>
244+
</div>
245+
</AnimatePresence>
246+
);
247+
}

emr-client/src/data/changelog.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
{
2-
"version": "1.3.6",
3-
"build": "20260522.2",
2+
"version": "1.3.7",
3+
"build": "20260522.3",
44
"changelog": [
5+
{
6+
"v": "v1.3.7",
7+
"date": "2026-05-22",
8+
"items": [
9+
"Implemented Shift Report modal for practitioners and admins with dynamic database views.",
10+
"Added date filtering capabilities for historical clinical shift reports.",
11+
"Integrated simulated RVU calculations and SDOH tracking metrics into clinical reports."
12+
]
13+
},
514
{
615
"v": "v1.3.6",
716
"date": "2026-05-22",

0 commit comments

Comments
 (0)