Skip to content

Commit a8b2b66

Browse files
committed
Staff-CheckIN-Patient
1 parent b1582ee commit a8b2b66

9 files changed

Lines changed: 165 additions & 38 deletions

File tree

frontend/src/app/staff/dashboard/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export default function StaffLayout({
6060
{ label: 'Patients', icon: User, path: '/staff/dashboard/patients' },
6161
] : [
6262
{ label: 'Dashboard', icon: LayoutDashboard, path: '/staff/dashboard' },
63+
{ label: "Today's Queue", icon: Activity, path: '/staff/dashboard/queue' },
6364
{ label: 'Beds', icon: Bed, path: '/staff/dashboard/beds' },
6465
{ label: 'Admissions', icon: ClipboardPlus, path: '/staff/dashboard/admissions' },
6566
{ label: 'Discharge', icon: FileMinus, path: '/staff/dashboard/discharge' },

frontend/src/app/staff/dashboard/queue/page.tsx

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,99 @@ import { useEmergency } from '../../../../features/appointments/hooks/useEmergen
1212
import { useRequestAdmission } from '../../../../features/admissions/hooks/useRequestAdmission';
1313
import { Activity, Clock, Users, Timer, CheckCircle, AlertCircle } from 'lucide-react';
1414
import { PrescribeModal } from '../../../../features/pharmacy/components/PrescribeModal';
15+
import { useDepartments, useDoctorsByDepartment } from '../../../../features/staff/hooks/useStaffData';
1516

1617
export default function QueuePage() {
1718
const { auth } = useStaffAuth();
18-
const doctorId = auth.staff?.id;
19+
const isDoctor = auth.staff?.role === 'DOCTOR';
1920

21+
const [selectedDeptId, setSelectedDeptId] = useState('');
22+
const [selectedDoctorId, setSelectedDoctorId] = useState(isDoctor ? auth.staff?.id : '');
2023
const [showEmergency, setShowEmergency] = useState(false);
2124
const [patientId, setPatientId] = useState('');
2225
const [prescribePatient, setPrescribePatient] = useState<{ id: string, name: string } | null>(null);
2326

27+
const { data: departments } = useDepartments();
28+
const { data: doctorsInDept, isLoading: isLoadingDoctors } = useDoctorsByDepartment(selectedDeptId);
29+
30+
const statuses = isDoctor ? ['CHECKED_IN', 'IN_PROGRESS'] : ['SCHEDULED', 'CHECKED_IN', 'IN_PROGRESS'];
31+
const { data, isLoading } = useDoctorQueue(selectedDoctorId, statuses);
32+
33+
const startMutation = useStartAppointment();
34+
const completeMutation = useCompleteAppointment();
35+
const checkInMutation = useCheckInAppointment();
36+
const emergencyMutation = useEmergency(selectedDoctorId || '');
37+
const admitMutation = useRequestAdmission();
38+
2439
if (auth.isRestoring) return (
2540
<div className="flex justify-center p-8">
2641
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600" />
2742
</div>
2843
);
29-
if (!doctorId) return (
30-
<div className="bg-white rounded-2xl border shadow-sm p-6 text-slate-500 text-center">
31-
Doctor information not available.
32-
</div>
33-
);
3444

35-
const { data, isLoading } = useDoctorQueue(doctorId);
45+
if (!isDoctor && !selectedDoctorId) {
46+
return (
47+
<div className="space-y-6">
48+
<div className="bg-white p-8 rounded-2xl shadow-sm border text-center space-y-6 max-w-2xl mx-auto">
49+
<div className="space-y-2">
50+
<Activity size={48} className="mx-auto text-indigo-500 opacity-50" />
51+
<h2 className="text-2xl font-bold text-slate-800">Staff Check-in Panel</h2>
52+
<p className="text-slate-500">Select a department and doctor to manage their queue.</p>
53+
</div>
3654

37-
const startMutation = useStartAppointment();
38-
const completeMutation = useCompleteAppointment();
39-
const checkInMutation = useCheckInAppointment();
40-
const emergencyMutation = useEmergency(doctorId);
41-
const admitMutation = useRequestAdmission();
55+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-left">
56+
<div className="space-y-2">
57+
<label className="text-sm font-semibold text-slate-700">Department</label>
58+
<select
59+
value={selectedDeptId}
60+
onChange={(e) => setSelectedDeptId(e.target.value)}
61+
className="w-full border border-slate-300 rounded-xl px-4 py-3 focus:ring-2 focus:ring-indigo-500 outline-none bg-white transition"
62+
>
63+
<option value="">Select Department...</option>
64+
{departments?.map((dept: any) => (
65+
<option key={dept.id} value={dept.id}>{dept.name}</option>
66+
))}
67+
</select>
68+
</div>
69+
70+
<div className="space-y-2">
71+
<label className="text-sm font-semibold text-slate-700">Doctor</label>
72+
<select
73+
disabled={!selectedDeptId || isLoadingDoctors}
74+
onChange={(e) => setSelectedDoctorId(e.target.value)}
75+
className="w-full border border-slate-300 rounded-xl px-4 py-3 focus:ring-2 focus:ring-indigo-500 outline-none bg-white disabled:bg-slate-50 disabled:text-slate-400 transition"
76+
value="" // Keep it as placeholder since we set selectedDoctorId on change
77+
>
78+
<option value="">
79+
{isLoadingDoctors ? 'Loading doctors...' : selectedDeptId ? 'Select Doctor...' : 'Select Department first...'}
80+
</option>
81+
{doctorsInDept?.map((doc: any) => (
82+
<option key={doc.id} value={doc.id}>{doc.name}</option>
83+
))}
84+
</select>
85+
</div>
86+
</div>
87+
</div>
88+
</div>
89+
);
90+
}
91+
92+
const doctorId = selectedDoctorId;
4293

4394
if (isLoading) return (
4495
<div className="flex justify-center p-8">
4596
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600" />
4697
</div>
4798
);
99+
48100
if (!data) return (
49101
<div className="bg-white rounded-2xl border shadow-sm p-6 text-slate-500 text-center">
50-
No queue found.
102+
No queue found for this doctor.
103+
{!isDoctor && (
104+
<button onClick={() => setSelectedDoctorId('')} className="block mx-auto mt-4 text-indigo-600 font-medium">
105+
Change Doctor
106+
</button>
107+
)}
51108
</div>
52109
);
53110

@@ -59,12 +116,22 @@ export default function QueuePage() {
59116

60117
{/* 🔴 Header */}
61118
<div className="flex justify-between items-center bg-white p-6 rounded-2xl shadow-sm border">
62-
<h1 className="text-2xl font-bold text-slate-800 flex items-center gap-3">
63-
<div className="p-2.5 rounded-xl bg-indigo-100 text-indigo-600">
64-
<Activity size={24} />
65-
</div>
66-
Today's Queue
67-
</h1>
119+
<div className="flex items-center gap-4">
120+
<h1 className="text-2xl font-bold text-slate-800 flex items-center gap-3">
121+
<div className="p-2.5 rounded-xl bg-indigo-100 text-indigo-600">
122+
<Activity size={24} />
123+
</div>
124+
{isDoctor ? "Today's Queue" : "Staff Check-in"}
125+
</h1>
126+
{!isDoctor && (
127+
<button
128+
onClick={() => setSelectedDoctorId('')}
129+
className="px-3 py-1 text-xs bg-slate-100 hover:bg-slate-200 text-slate-600 rounded-lg transition"
130+
>
131+
Switch Doctor
132+
</button>
133+
)}
134+
</div>
68135

69136
<button
70137
onClick={() => setShowEmergency(true)}
@@ -116,7 +183,7 @@ export default function QueuePage() {
116183
<div className="grid grid-cols-1 gap-4">
117184
{queue.length === 0 ? (
118185
<div className="bg-white rounded-2xl border shadow-sm p-6 text-slate-500 text-center">
119-
The queue is currently empty.
186+
{isDoctor ? "No patients ready for consultation." : "No upcoming appointments for this doctor."}
120187
</div>
121188
) : queue.map((item: any) => (
122189
<div
@@ -154,8 +221,8 @@ export default function QueuePage() {
154221

155222
<div className="flex gap-3 flex-wrap">
156223

157-
{/* Check In */}
158-
{item.status === 'SCHEDULED' && (
224+
{/* Check In - STAFF ONLY */}
225+
{!isDoctor && item.status === 'SCHEDULED' && (
159226
<button
160227
onClick={() => checkInMutation.mutate(item.id)}
161228
className="px-5 py-2.5 bg-yellow-500 hover:bg-yellow-600 text-white rounded-xl text-sm font-medium transition shadow-sm"
@@ -164,8 +231,8 @@ export default function QueuePage() {
164231
</button>
165232
)}
166233

167-
{/* Start Consultation */}
168-
{item.status === 'CHECKED_IN' && !someoneInProgress && (
234+
{/* Start Consultation - DOCTOR ONLY */}
235+
{isDoctor && item.status === 'CHECKED_IN' && !someoneInProgress && (
169236
<button
170237
onClick={() => startMutation.mutate(item.id)}
171238
className="px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-sm font-medium transition shadow-sm"
@@ -174,8 +241,8 @@ export default function QueuePage() {
174241
</button>
175242
)}
176243

177-
{/* Complete Consultation */}
178-
{item.status === 'IN_PROGRESS' && (
244+
{/* Complete Consultation - DOCTOR ONLY */}
245+
{isDoctor && item.status === 'IN_PROGRESS' && (
179246
<>
180247
<button
181248
onClick={() => setPrescribePatient({ id: item.patient_id, name: item.patient_name })}
@@ -202,7 +269,7 @@ export default function QueuePage() {
202269
onClick={() =>
203270
admitMutation.mutate({
204271
patientId: item.patient_id,
205-
doctorId: doctorId,
272+
doctorId: doctorId as string,
206273
departmentId: auth.staff?.department_id as string
207274
})
208275
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { api } from '@/src/lib/api';
22

3-
export async function getDoctorQueue(doctorId: string) {
4-
const { data } = await api.get(`/appointments/doctor/today/${doctorId}`);
3+
export async function getDoctorQueue(doctorId: string, statuses?: string[]) {
4+
const params = statuses ? { statuses: statuses.join(',') } : {};
5+
const { data } = await api.get(`/appointments/doctor/today/${doctorId}`, {
6+
params,
7+
});
58
return data;
69
}

frontend/src/features/appointments/hooks/useDoctorQueue.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { useQuery } from '@tanstack/react-query';
22
import { getDoctorQueue } from '../api/getDoctorQueue';
33

4-
export function useDoctorQueue(doctorId?: string) {
4+
export function useDoctorQueue(doctorId?: string, statuses?: string[]) {
55
return useQuery({
6-
queryKey: ['doctor-queue', doctorId],
6+
queryKey: ['doctor-queue', doctorId, statuses],
77
queryFn: async () => {
88
// This will only run if doctorId exists
9-
return getDoctorQueue(doctorId as string);
9+
return getDoctorQueue(doctorId as string, statuses);
1010
},
1111
enabled: !!doctorId, // Prevent query from running if undefined
1212
refetchInterval: 10000, // Auto refresh every 10 seconds
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { api } from '@/src/lib/api';
2+
3+
export async function getDepartments() {
4+
const { data } = await api.get('/staff/departments');
5+
return data;
6+
}
7+
8+
export async function getDoctorsByDepartment(departmentId: string) {
9+
const { data } = await api.get(
10+
`/staff/doctors?department_id=${departmentId}`
11+
);
12+
return data;
13+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { getDepartments, getDoctorsByDepartment } from '../api/staff.api';
3+
4+
export function useDepartments() {
5+
return useQuery({
6+
queryKey: ['departments'],
7+
queryFn: getDepartments,
8+
staleTime: 5 * 60 * 1000, // 5 minutes
9+
});
10+
}
11+
12+
export function useDoctorsByDepartment(departmentId: string) {
13+
return useQuery({
14+
queryKey: ['doctors', departmentId],
15+
queryFn: () => getDoctorsByDepartment(departmentId),
16+
enabled: !!departmentId,
17+
staleTime: 2 * 60 * 1000, // 2 minutes
18+
});
19+
}

services/patient-service/src/modules/appointments/appointment.controller.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,21 @@ export class AppointmentController {
154154
const date =
155155
(req.query.date as string) || new Date().toISOString().split('T')[0];
156156

157+
const statusesQuery = req.query.statuses;
158+
let statuses: string[] | undefined;
159+
160+
if (statusesQuery) {
161+
if (Array.isArray(statusesQuery)) {
162+
statuses = statusesQuery as string[];
163+
} else {
164+
statuses = (statusesQuery as string).split(',');
165+
}
166+
}
167+
157168
const queue = await appointmentService.getDoctorQueueForDay(
158169
doctorId,
159-
date
170+
date,
171+
statuses
160172
);
161173

162174
return res.json(queue);

services/patient-service/src/modules/appointments/appointment.repository.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ export class AppointmentRepository {
4545
}
4646
}
4747

48-
async getDoctorAppointmentsForDay(doctorId: string, date: string) {
48+
async getDoctorAppointmentsForDay(
49+
doctorId: string,
50+
date: string,
51+
statuses: string[] = ['SCHEDULED', 'CHECKED_IN', 'IN_PROGRESS']
52+
) {
4953
const result = await pool.query(
5054
`
5155
SELECT
@@ -60,12 +64,12 @@ export class AppointmentRepository {
6064
LEFT JOIN patients p ON p.id = a.patient_id
6165
WHERE a.doctor_id = $1
6266
AND a.appointment_time::date = $2
63-
AND a.status IN ('SCHEDULED', 'CHECKED_IN', 'IN_PROGRESS')
67+
AND a.status = ANY($3)
6468
ORDER BY
6569
CASE WHEN a.priority = 'HIGH' THEN 0 ELSE 1 END,
6670
a.appointment_time ASC
6771
`,
68-
[doctorId, date]
72+
[doctorId, date, statuses]
6973
);
7074

7175
return result.rows;

services/patient-service/src/modules/appointments/appointment.service.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ export class AppointmentService {
119119
return updated.rows[0];
120120
}
121121

122-
async getDoctorQueueForDay(doctorId: string, date: string) {
122+
async getDoctorQueueForDay(
123+
doctorId: string,
124+
date: string,
125+
statuses?: string[]
126+
) {
123127
const appointments =
124-
await appointmentRepository.getDoctorAppointmentsForDay(doctorId, date);
128+
await appointmentRepository.getDoctorAppointmentsForDay(
129+
doctorId,
130+
date,
131+
statuses
132+
);
125133

126134
const GRACE_MINUTES = 10;
127135
const now = new Date();

0 commit comments

Comments
 (0)