From e005f86ed38c31f3ae6bc7950a2751ef3b1305c2 Mon Sep 17 00:00:00 2001 From: linasliyakath Date: Fri, 13 Mar 2026 12:41:36 +0530 Subject: [PATCH 1/7] Bed management --- api-gateway/src/app.ts | 28 ++- .../src/middlewares/rbac.middleware.ts | 27 ++- api-gateway/src/proxy/staff.data.proxy.ts | 12 +- api-gateway/src/routes/staff.data.router.ts | 7 +- frontend/src/app/admin/dashboard/page.tsx | 58 +++++- .../app/staff/dashboard/admissions/page.tsx | 18 ++ .../src/app/staff/dashboard/beds/page.tsx | 23 +++ .../app/staff/dashboard/discharge/page.tsx | 14 ++ frontend/src/app/staff/dashboard/layout.tsx | 20 ++ frontend/src/app/staff/dashboard/page.tsx | 3 + .../src/app/staff/dashboard/patients/page.tsx | 16 ++ .../src/app/staff/dashboard/queue/page.tsx | 43 +++- frontend/src/features/admin/api/beds.api.ts | 30 +++ .../admin/components/CreateBedForm.tsx | 105 ++++++++++ .../admin/components/CreateRoomForm.tsx | 76 +++++++ .../admin/components/CreateWardForm.tsx | 37 ++++ .../src/features/admin/hooks/useCreateBed.ts | 8 + .../src/features/admin/hooks/useCreateRoom.ts | 8 + .../src/features/admin/hooks/useCreateWard.ts | 13 ++ .../features/admissions/api/admissions.api.ts | 11 + .../features/admissions/api/assignBed.api.ts | 12 ++ .../admissions/api/dischargePatients.api.ts | 9 + .../api/getDischargeRequests.api.ts | 7 + .../admissions/api/getDoctorAdmissions.api.ts | 7 + .../admissions/api/requestAdmission.api.ts | 11 + .../admissions/api/requestDischarge.api.ts | 7 + .../admissions/components/AdmissionQueue.tsx | 75 +++++++ .../admissions/components/AssignBedModal.tsx | 121 +++++++++++ .../components/DischargeRequests.tsx | 67 ++++++ .../admissions/components/DoctorPatients.tsx | 62 ++++++ .../admissions/components/StaffQueueCard.tsx | 41 ++++ .../admissions/hooks/useAdmissions.ts | 9 + .../features/admissions/hooks/useAssignBed.ts | 15 ++ .../src/features/admissions/hooks/useBeds.ts | 13 ++ .../admissions/hooks/useDischargePatient.ts | 19 ++ .../admissions/hooks/useDischargeRequests.ts | 9 + .../admissions/hooks/useDoctorAdmissions.ts | 9 + .../admissions/hooks/useRequestAdmission.ts | 8 + .../admissions/hooks/useRequestDischarge.ts | 8 + .../src/features/admissions/hooks/useRooms.ts | 13 ++ .../src/features/admissions/hooks/useWards.ts | 12 ++ frontend/src/features/beds/api/beds.api.ts | 16 ++ .../features/beds/components/BedsBoard.tsx | 40 ++++ .../src/features/beds/hooks/useAssignedBed.ts | 13 ++ frontend/src/features/beds/hooks/useBeds.ts | 9 + frontend/src/staff/auth/staff.auth.types.ts | 2 + pnpm-lock.yaml | 3 + services/patient-service/scripts/init-db.ts | 14 ++ services/patient-service/src/app.ts | 2 + .../admissions/admission.controller.ts | 74 +++++++ .../admissions/admission.repository.ts | 109 ++++++++++ .../modules/admissions/admission.routes.ts | 22 ++ .../modules/admissions/admission.service.ts | 32 +++ .../appointments/appointment.service.ts | 192 +----------------- services/staff-service/package.json | 5 +- services/staff-service/scripts/init-db.ts | 71 ++++++- services/staff-service/src/app.ts | 5 + .../src/modules/auth/staff.auth.controller.ts | 32 ++- .../src/modules/beds/admin.bed.routes.ts | 19 ++ .../src/modules/beds/bed.controller.ts | 79 +++++++ .../src/modules/beds/bed.routes.ts | 20 ++ .../src/modules/beds/bed.service.ts | 174 ++++++++++++++++ .../src/modules/staff/staff.routes.ts | 12 +- 63 files changed, 1811 insertions(+), 225 deletions(-) create mode 100644 frontend/src/app/staff/dashboard/admissions/page.tsx create mode 100644 frontend/src/app/staff/dashboard/beds/page.tsx create mode 100644 frontend/src/app/staff/dashboard/discharge/page.tsx create mode 100644 frontend/src/app/staff/dashboard/patients/page.tsx create mode 100644 frontend/src/features/admin/api/beds.api.ts create mode 100644 frontend/src/features/admin/components/CreateBedForm.tsx create mode 100644 frontend/src/features/admin/components/CreateRoomForm.tsx create mode 100644 frontend/src/features/admin/components/CreateWardForm.tsx create mode 100644 frontend/src/features/admin/hooks/useCreateBed.ts create mode 100644 frontend/src/features/admin/hooks/useCreateRoom.ts create mode 100644 frontend/src/features/admin/hooks/useCreateWard.ts create mode 100644 frontend/src/features/admissions/api/admissions.api.ts create mode 100644 frontend/src/features/admissions/api/assignBed.api.ts create mode 100644 frontend/src/features/admissions/api/dischargePatients.api.ts create mode 100644 frontend/src/features/admissions/api/getDischargeRequests.api.ts create mode 100644 frontend/src/features/admissions/api/getDoctorAdmissions.api.ts create mode 100644 frontend/src/features/admissions/api/requestAdmission.api.ts create mode 100644 frontend/src/features/admissions/api/requestDischarge.api.ts create mode 100644 frontend/src/features/admissions/components/AdmissionQueue.tsx create mode 100644 frontend/src/features/admissions/components/AssignBedModal.tsx create mode 100644 frontend/src/features/admissions/components/DischargeRequests.tsx create mode 100644 frontend/src/features/admissions/components/DoctorPatients.tsx create mode 100644 frontend/src/features/admissions/components/StaffQueueCard.tsx create mode 100644 frontend/src/features/admissions/hooks/useAdmissions.ts create mode 100644 frontend/src/features/admissions/hooks/useAssignBed.ts create mode 100644 frontend/src/features/admissions/hooks/useBeds.ts create mode 100644 frontend/src/features/admissions/hooks/useDischargePatient.ts create mode 100644 frontend/src/features/admissions/hooks/useDischargeRequests.ts create mode 100644 frontend/src/features/admissions/hooks/useDoctorAdmissions.ts create mode 100644 frontend/src/features/admissions/hooks/useRequestAdmission.ts create mode 100644 frontend/src/features/admissions/hooks/useRequestDischarge.ts create mode 100644 frontend/src/features/admissions/hooks/useRooms.ts create mode 100644 frontend/src/features/admissions/hooks/useWards.ts create mode 100644 frontend/src/features/beds/api/beds.api.ts create mode 100644 frontend/src/features/beds/components/BedsBoard.tsx create mode 100644 frontend/src/features/beds/hooks/useAssignedBed.ts create mode 100644 frontend/src/features/beds/hooks/useBeds.ts create mode 100644 services/patient-service/src/modules/admissions/admission.controller.ts create mode 100644 services/patient-service/src/modules/admissions/admission.repository.ts create mode 100644 services/patient-service/src/modules/admissions/admission.routes.ts create mode 100644 services/patient-service/src/modules/admissions/admission.service.ts create mode 100644 services/staff-service/src/modules/beds/admin.bed.routes.ts create mode 100644 services/staff-service/src/modules/beds/bed.controller.ts create mode 100644 services/staff-service/src/modules/beds/bed.routes.ts create mode 100644 services/staff-service/src/modules/beds/bed.service.ts diff --git a/api-gateway/src/app.ts b/api-gateway/src/app.ts index 16147a7..29363fa 100644 --- a/api-gateway/src/app.ts +++ b/api-gateway/src/app.ts @@ -4,11 +4,12 @@ import healthRoutes from './routes/health'; import { patientAuthProxy } from './proxy/patient.auth.proxy'; import { patientDataProxy } from './proxy/patient.data.proxy'; import { staffAuthProxy } from './proxy/staff.auth.proxy'; -import staffDataRouter from './routes/staff.data.router'; +// import staffDataRouter from './routes/staff.data.router'; import { authenticate } from './middlewares/auth.middleware'; import { requirePatientSelf } from './middlewares/patient.guard'; import { createProxyMiddleware } from 'http-proxy-middleware'; - +import { requirePermission } from './middlewares/rbac.middleware'; +import { staffDataProxy } from './proxy/staff.data.proxy'; const app = express(); app.use( @@ -39,8 +40,29 @@ app.use( ); // STAFF auth app.use('/staff/public', staffAuthProxy); -app.use('/staff', authenticate, staffDataRouter); +app.use('/staff', authenticate, staffDataProxy); + +app.use('/admin', authenticate, staffDataProxy); + +app.use( + '/admissions', + authenticate, + (req: any, _res, next) => { + if (req.user?.sub) req.headers['x-user-id'] = String(req.user.sub); + + if (req.user?.role) req.headers['x-user-role'] = String(req.user.role); + + if (req.user?.type) req.headers['x-user-type'] = String(req.user.type); + + next(); + }, + createProxyMiddleware({ + target: process.env.PATIENT_SERVICE_URL, + changeOrigin: true, + pathRewrite: (path) => `/admissions${path}`, + }) +); //Appointment routes (accessible by PATIENT + STAFF + ADMIN) app.use( diff --git a/api-gateway/src/middlewares/rbac.middleware.ts b/api-gateway/src/middlewares/rbac.middleware.ts index 9632051..e1235d9 100644 --- a/api-gateway/src/middlewares/rbac.middleware.ts +++ b/api-gateway/src/middlewares/rbac.middleware.ts @@ -1,13 +1,16 @@ import { Response, NextFunction } from 'express'; import { AuthenticatedRequest } from './auth.middleware'; -// what actions exist in the system -export type Permission = 'CREATE_STAFF' | 'UPDATE_STAFF' | 'DEACTIVATE_STAFF'; +export type Permission = + | 'CREATE_STAFF' + | 'UPDATE_STAFF' + | 'DEACTIVATE_STAFF' + | 'MANAGE_BEDS'; -// staff role → permissions const STAFF_ROLE_PERMISSIONS: Record = { DOCTOR: [], - NURSE: [], + NURSE: ['MANAGE_BEDS'], + RECEPTIONIST: ['MANAGE_BEDS'], }; export function requirePermission(permission: Permission) { @@ -18,12 +21,22 @@ export function requirePermission(permission: Permission) { return res.status(401).json({ error: 'Unauthorized' }); } - // ✅ ADMIN can do everything + // ADMIN can do everything if (user.type === 'ADMIN') { return next(); } - // ❌ STAFF cannot do admin actions - return res.status(403).json({ error: 'Forbidden' }); + // Only STAFF can have role permissions + if (user.type !== 'STAFF' || !user.role) { + return res.status(403).json({ error: 'Forbidden' }); + } + + const rolePermissions = STAFF_ROLE_PERMISSIONS[user.role] ?? []; + + if (!rolePermissions.includes(permission)) { + return res.status(403).json({ error: 'Forbidden' }); + } + + next(); }; } diff --git a/api-gateway/src/proxy/staff.data.proxy.ts b/api-gateway/src/proxy/staff.data.proxy.ts index abced43..87e56a5 100644 --- a/api-gateway/src/proxy/staff.data.proxy.ts +++ b/api-gateway/src/proxy/staff.data.proxy.ts @@ -3,7 +3,15 @@ import { createProxyMiddleware } from 'http-proxy-middleware'; export const staffDataProxy = createProxyMiddleware({ target: 'http://localhost:3002', changeOrigin: true, + pathRewrite: (path, req: any) => { + if (req.originalUrl.startsWith('/admin')) { + return `/admin${path}`; + } - // /staff/* → /staff/* - pathRewrite: (path) => `/staff${path}`, + if (req.originalUrl.startsWith('/staff')) { + return `/staff${path}`; + } + + return path; + }, }); diff --git a/api-gateway/src/routes/staff.data.router.ts b/api-gateway/src/routes/staff.data.router.ts index 14bd51b..cb1fe15 100644 --- a/api-gateway/src/routes/staff.data.router.ts +++ b/api-gateway/src/routes/staff.data.router.ts @@ -4,10 +4,13 @@ import { staffDataProxy } from '../proxy/staff.data.proxy'; const router = express.Router(); -// 🔒 ADMIN ONLY: create staff +// 🔒 ADMIN ONLY router.post('/', requirePermission('CREATE_STAFF'), staffDataProxy); -// everything else just passes through +// 🔒 BED MANAGEMENT +router.use('/beds', requirePermission('MANAGE_BEDS'), staffDataProxy); + +// everything else router.use(staffDataProxy); export default router; diff --git a/frontend/src/app/admin/dashboard/page.tsx b/frontend/src/app/admin/dashboard/page.tsx index 072c01e..b5ad02d 100644 --- a/frontend/src/app/admin/dashboard/page.tsx +++ b/frontend/src/app/admin/dashboard/page.tsx @@ -1,17 +1,59 @@ import CreateStaffForm from "@/src/features/admin/components/CreateStaffForm"; - +import CreateWardForm from "@/src/features/admin/components/CreateWardForm"; +import CreateRoomForm from "@/src/features/admin/components/CreateRoomForm"; +import CreateBedForm from "@/src/features/admin/components/CreateBedForm"; export default function AdminDashboard() { return ( -
-

- Admin Dashboard -

+
+
{/* Narrower container for better readability */} + + {/* Page Header */} +
+

Admin Management

+

Add new staff members and hospital infrastructure

+
+ + {/* Form 1: Staff */} +
+
+

1. Staff Registration

+
+
+ +
+
+ + {/* Form 2: Wards */} +
+
+

2. Ward Management

+
+
+ +
+
+ + {/* Form 3: Rooms */} +
+
+

3. Room Assignment

+
+
+ +
+
-

Welcome Admin

+ {/* Form 4: Beds */} +
+
+

4. Bed Setup

+
+
+ +
+
-
-
); diff --git a/frontend/src/app/staff/dashboard/admissions/page.tsx b/frontend/src/app/staff/dashboard/admissions/page.tsx new file mode 100644 index 0000000..2c35a4f --- /dev/null +++ b/frontend/src/app/staff/dashboard/admissions/page.tsx @@ -0,0 +1,18 @@ +'use client' + +import AdmissionQueue from '@/src/features/admissions/components/AdmissionQueue' + +export default function StaffAdmissionsPage() { + + return ( +
+ +

+ Admissions +

+ + + +
+ ) +} \ No newline at end of file diff --git a/frontend/src/app/staff/dashboard/beds/page.tsx b/frontend/src/app/staff/dashboard/beds/page.tsx new file mode 100644 index 0000000..a00be7d --- /dev/null +++ b/frontend/src/app/staff/dashboard/beds/page.tsx @@ -0,0 +1,23 @@ +'use client' + +import BedsBoard from '@/src/features/beds/components/BedsBoard' +import CreateWardForm from '@/src/features/admin/components/CreateWardForm' +import CreateRoomForm from '@/src/features/admin/components/CreateRoomForm' +import CreateBedForm from '@/src/features/admin/components/CreateBedForm' + +export default function StaffBedsPage() { + + return ( + +
+ +

+ Bed Management +

+ + + +
+ + ) +} \ No newline at end of file diff --git a/frontend/src/app/staff/dashboard/discharge/page.tsx b/frontend/src/app/staff/dashboard/discharge/page.tsx new file mode 100644 index 0000000..8402610 --- /dev/null +++ b/frontend/src/app/staff/dashboard/discharge/page.tsx @@ -0,0 +1,14 @@ +import DischargeRequests from "@/src/features/admissions/components/DischargeRequests"; + +export default function DischargeRequestsPage(){ + return( +
+

+ Discharge Requests +

+
+ +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/app/staff/dashboard/layout.tsx b/frontend/src/app/staff/dashboard/layout.tsx index 59222f4..407c2e3 100644 --- a/frontend/src/app/staff/dashboard/layout.tsx +++ b/frontend/src/app/staff/dashboard/layout.tsx @@ -70,6 +70,26 @@ export default function StaffLayout({ label="Manage Availability" active={pathname === '/staff/dashboard/availability'} /> + + + +
diff --git a/frontend/src/app/staff/dashboard/page.tsx b/frontend/src/app/staff/dashboard/page.tsx index baa1db5..01a7b48 100644 --- a/frontend/src/app/staff/dashboard/page.tsx +++ b/frontend/src/app/staff/dashboard/page.tsx @@ -3,6 +3,7 @@ import { useStaffAuth } from '../../../staff/auth/staff.auth.provider'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; +import Link from 'next/link'; export default function StaffDashboardPage() { const { auth, logout } = useStaffAuth(); @@ -48,6 +49,8 @@ export default function StaffDashboardPage() {

Role: {auth.staff?.role}

Job Title: {auth.staff?.job_title}

+ + ); } diff --git a/frontend/src/app/staff/dashboard/patients/page.tsx b/frontend/src/app/staff/dashboard/patients/page.tsx new file mode 100644 index 0000000..1e4ffe2 --- /dev/null +++ b/frontend/src/app/staff/dashboard/patients/page.tsx @@ -0,0 +1,16 @@ +import DoctorPatients from "@/src/features/admissions/components/DoctorPatients" + +export default function DoctorPatientsPage(){ + return ( +
+

+ My Admitted Patients +

+ +
+ ) +} + + + + diff --git a/frontend/src/app/staff/dashboard/queue/page.tsx b/frontend/src/app/staff/dashboard/queue/page.tsx index eda0b13..31d5b96 100644 --- a/frontend/src/app/staff/dashboard/queue/page.tsx +++ b/frontend/src/app/staff/dashboard/queue/page.tsx @@ -9,8 +9,10 @@ import { useCheckInAppointment, } from '../../../../features/appointments/hooks/useStaffActions'; import { useEmergency } from '../../../../features/appointments/hooks/useEmergency'; +import { useRequestAdmission } from '../../../../features/admissions/hooks/useRequestAdmission'; export default function QueuePage() { + const { auth } = useStaffAuth(); const doctorId = auth.staff?.id; @@ -26,6 +28,7 @@ export default function QueuePage() { const completeMutation = useCompleteAppointment(); const checkInMutation = useCheckInAppointment(); const emergencyMutation = useEmergency(doctorId); + const admitMutation = useRequestAdmission(); if (isLoading) return

Loading queue...

; if (!data) return

No queue found.

; @@ -69,14 +72,17 @@ export default function QueuePage() { : 'bg-white' }`} > +

Patient ID: {item.patient_id}

+

Status: {item.status}

Position: {item.position}

-
+
+ {/* Check In */} {item.status === 'SCHEDULED' && ( )} + {/* Start Consultation */} {item.status === 'CHECKED_IN' && !someoneInProgress && ( )} + {/* Complete Consultation */} {item.status === 'IN_PROGRESS' && ( - + <> + + + {/* 🏥 Admit Patient */} + + )}
+
))} @@ -150,6 +178,7 @@ export default function QueuePage() { + )} diff --git a/frontend/src/features/admin/api/beds.api.ts b/frontend/src/features/admin/api/beds.api.ts new file mode 100644 index 0000000..4743c1f --- /dev/null +++ b/frontend/src/features/admin/api/beds.api.ts @@ -0,0 +1,30 @@ +import { api } from '@/src/lib/api'; + +export async function createWard(data: { name: string; description?: string }) { + const res = await api.post('/admin/beds/wards', data); + return res.data; +} + +export async function createRoom(data: { wardId: string; roomNumber: string }) { + const res = await api.post('/admin/beds/rooms', data); + return res.data; +} + +export async function createBed(data: { roomId: string; bedNumber: string }) { + const res = await api.post('/admin/beds/beds', data); + return res.data; +} + +export async function getBeds() { + const res = await api.get('/staff/beds'); + return res.data; +} +export async function getWards() { + const res = await api.get('/admin/beds/wards'); + return res.data; +} + +export async function getRooms(wardId: string) { + const res = await api.get(`/admin/beds/rooms/${wardId}`); + return res.data; +} diff --git a/frontend/src/features/admin/components/CreateBedForm.tsx b/frontend/src/features/admin/components/CreateBedForm.tsx new file mode 100644 index 0000000..3c85a1b --- /dev/null +++ b/frontend/src/features/admin/components/CreateBedForm.tsx @@ -0,0 +1,105 @@ +'use client' + +import { useState } from 'react' +import { useCreateBed } from '../hooks/useCreateBed' +import { useQuery } from '@tanstack/react-query' +import { getWards, getRooms } from '../api/beds.api' + +export default function CreateBedForm() { + + const [wardId, setWardId] = useState('') + const [roomId, setRoomId] = useState('') + const [bedNumber, setBedNumber] = useState('') + + const mutation = useCreateBed() + + const { data: wards } = useQuery({ + queryKey: ['wards'], + queryFn: getWards + }) + + const { data: rooms } = useQuery({ + queryKey: ['rooms', wardId], + queryFn: () => getRooms(wardId), + enabled: !!wardId + }) + + const handleSubmit = async () => { + + if (!roomId || !bedNumber) return + + try { + await mutation.mutateAsync({ + roomId, + bedNumber + }) + + alert('✅ Bed created successfully') + + setBedNumber('') + setRoomId('') + setWardId('') + + } catch { + alert('❌ Failed to create bed') + } + } + + return ( +
+ +

Create Bed

+ + {/* Ward dropdown */} + + + {/* Room dropdown */} + + + setBedNumber(e.target.value)} + placeholder="Bed Number" + className="border p-2 rounded w-full" + /> + + + +
+ ) +} \ No newline at end of file diff --git a/frontend/src/features/admin/components/CreateRoomForm.tsx b/frontend/src/features/admin/components/CreateRoomForm.tsx new file mode 100644 index 0000000..88f069a --- /dev/null +++ b/frontend/src/features/admin/components/CreateRoomForm.tsx @@ -0,0 +1,76 @@ +'use client' + +import { useState } from 'react' +import { useCreateRoom } from '../hooks/useCreateRoom' +import { useQuery } from '@tanstack/react-query' +import { getWards } from '../api/beds.api' + +export default function CreateRoomForm() { + + const [wardId, setWardId] = useState('') + const [roomNumber, setRoomNumber] = useState('') + + const mutation = useCreateRoom() + + const { data: wards } = useQuery({ + queryKey: ['wards'], + queryFn: getWards + }) + + const handleSubmit = async () => { + + if (!wardId || !roomNumber) return + + try { + await mutation.mutateAsync({ + wardId, + roomNumber + }) + + alert('✅ Room created successfully') + + setRoomNumber('') + setWardId('') + + } catch { + alert('❌ Failed to create room') + } + } + + return ( +
+ +

Create Room

+ + + + setRoomNumber(e.target.value)} + placeholder="Room Number" + className="border p-2 rounded w-full" + /> + + + +
+ ) +} \ No newline at end of file diff --git a/frontend/src/features/admin/components/CreateWardForm.tsx b/frontend/src/features/admin/components/CreateWardForm.tsx new file mode 100644 index 0000000..e3c07a9 --- /dev/null +++ b/frontend/src/features/admin/components/CreateWardForm.tsx @@ -0,0 +1,37 @@ +'use client' + +import { useState } from 'react' +import { useCreateWard } from '../hooks/useCreateWard' + +export default function CreateWardForm() { + + const [name,setName] = useState('') + const mutation = useCreateWard() + + const handleSubmit = () => { + mutation.mutate({ name }) + setName('') + } + + return ( + +
+ + setName(e.target.value)} + placeholder="Ward name" + className="border p-2 rounded" + /> + + + +
+ + ) +} \ No newline at end of file diff --git a/frontend/src/features/admin/hooks/useCreateBed.ts b/frontend/src/features/admin/hooks/useCreateBed.ts new file mode 100644 index 0000000..109d58f --- /dev/null +++ b/frontend/src/features/admin/hooks/useCreateBed.ts @@ -0,0 +1,8 @@ +import { useMutation } from '@tanstack/react-query'; +import { createBed } from '../api/beds.api'; + +export function useCreateBed() { + return useMutation({ + mutationFn: createBed, + }); +} diff --git a/frontend/src/features/admin/hooks/useCreateRoom.ts b/frontend/src/features/admin/hooks/useCreateRoom.ts new file mode 100644 index 0000000..d9a160a --- /dev/null +++ b/frontend/src/features/admin/hooks/useCreateRoom.ts @@ -0,0 +1,8 @@ +import { useMutation } from '@tanstack/react-query'; +import { createRoom } from '../api/beds.api'; + +export function useCreateRoom() { + return useMutation({ + mutationFn: createRoom, + }); +} diff --git a/frontend/src/features/admin/hooks/useCreateWard.ts b/frontend/src/features/admin/hooks/useCreateWard.ts new file mode 100644 index 0000000..dca165b --- /dev/null +++ b/frontend/src/features/admin/hooks/useCreateWard.ts @@ -0,0 +1,13 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { createWard } from '../api/beds.api'; + +export function useCreateWard() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createWard, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['beds'] }); + }, + }); +} diff --git a/frontend/src/features/admissions/api/admissions.api.ts b/frontend/src/features/admissions/api/admissions.api.ts new file mode 100644 index 0000000..eb39d53 --- /dev/null +++ b/frontend/src/features/admissions/api/admissions.api.ts @@ -0,0 +1,11 @@ +import { api } from '@/src/lib/api'; + +export async function getPendingAdmissions() { + const res = await api.get('/admissions/pending'); + return res.data; +} + +export async function requestAdmission(data: any) { + const res = await api.post('/admissions', data); + return res.data; +} diff --git a/frontend/src/features/admissions/api/assignBed.api.ts b/frontend/src/features/admissions/api/assignBed.api.ts new file mode 100644 index 0000000..9ba0ef9 --- /dev/null +++ b/frontend/src/features/admissions/api/assignBed.api.ts @@ -0,0 +1,12 @@ +import { api } from '@/src/lib/api'; +import { StepId } from 'framer-motion'; + +export async function assignBed(data: { + bedId: string; + patientId: string; + admissionId: string; +}) { + const res = await api.post('/staff/beds/assign', data); + + return res.data; +} diff --git a/frontend/src/features/admissions/api/dischargePatients.api.ts b/frontend/src/features/admissions/api/dischargePatients.api.ts new file mode 100644 index 0000000..4d4ac19 --- /dev/null +++ b/frontend/src/features/admissions/api/dischargePatients.api.ts @@ -0,0 +1,9 @@ +import { api } from '@/src/lib/api'; + +export async function dischargePatient(data: { + bedId: string; + admissionId: string; +}) { + const res = await api.post('/staff/beds/discharge', data); + return res.data; +} diff --git a/frontend/src/features/admissions/api/getDischargeRequests.api.ts b/frontend/src/features/admissions/api/getDischargeRequests.api.ts new file mode 100644 index 0000000..98fb62e --- /dev/null +++ b/frontend/src/features/admissions/api/getDischargeRequests.api.ts @@ -0,0 +1,7 @@ +import { api } from '@/src/lib/api'; + +export async function getDischargeRequests() { + const res = await api.get('/admissions/discharge-requests'); + + return res.data; +} diff --git a/frontend/src/features/admissions/api/getDoctorAdmissions.api.ts b/frontend/src/features/admissions/api/getDoctorAdmissions.api.ts new file mode 100644 index 0000000..2a4cca5 --- /dev/null +++ b/frontend/src/features/admissions/api/getDoctorAdmissions.api.ts @@ -0,0 +1,7 @@ +import { api } from '@/src/lib/api'; + +export async function getDoctorAdmissions() { + const res = await api.get('/admissions/doctor'); + + return res.data; +} diff --git a/frontend/src/features/admissions/api/requestAdmission.api.ts b/frontend/src/features/admissions/api/requestAdmission.api.ts new file mode 100644 index 0000000..a360bf2 --- /dev/null +++ b/frontend/src/features/admissions/api/requestAdmission.api.ts @@ -0,0 +1,11 @@ +import { api } from '@/src/lib/api'; + +export async function requestAdmission(data: { + patientId: string; + doctorId: string; + departmentId: string; +}) { + const res = await api.post('/admissions/request', data); + + return res.data; +} diff --git a/frontend/src/features/admissions/api/requestDischarge.api.ts b/frontend/src/features/admissions/api/requestDischarge.api.ts new file mode 100644 index 0000000..72cf774 --- /dev/null +++ b/frontend/src/features/admissions/api/requestDischarge.api.ts @@ -0,0 +1,7 @@ +import { api } from '@/src/lib/api'; + +export async function requestDischarge(admissionId: string) { + const res = await api.post(`/admissions/${admissionId}/request-discharge`); + + return res.data; +} diff --git a/frontend/src/features/admissions/components/AdmissionQueue.tsx b/frontend/src/features/admissions/components/AdmissionQueue.tsx new file mode 100644 index 0000000..2de8830 --- /dev/null +++ b/frontend/src/features/admissions/components/AdmissionQueue.tsx @@ -0,0 +1,75 @@ +'use client' + +import { useState } from 'react' +import { useAdmissions } from '../hooks/useAdmissions' +import AssignBedModal from './AssignBedModal' + +export default function AdmissionQueue() { + + const { data, isLoading } = useAdmissions() + + const [selectedAdmission,setSelectedAdmission] = useState(null) + + if (isLoading) { + return
Loading admissions...
+ } + + if (!data?.length) { + return ( +
+ No pending admissions +
+ ) + } + + return ( + + <> +
+ + {data.map((admission:any)=>( + +
+ +
+ +
+ Patient: {admission.patient_id} +
+ +
+ Doctor: {admission.doctor_id} +
+ +
+ Department: {admission.department_id} +
+ +
+ + + +
+ + ))} + +
+ + {selectedAdmission && ( + setSelectedAdmission(null)} + /> + )} + + + ) +} \ No newline at end of file diff --git a/frontend/src/features/admissions/components/AssignBedModal.tsx b/frontend/src/features/admissions/components/AssignBedModal.tsx new file mode 100644 index 0000000..268e2ef --- /dev/null +++ b/frontend/src/features/admissions/components/AssignBedModal.tsx @@ -0,0 +1,121 @@ +'use client' + +import { useState } from 'react' +import { useAssignBed } from '../hooks/useAssignBed' +import { useWards } from '../hooks/useWards' +import { useRooms } from '../hooks/useRooms' +import { useBeds } from '../hooks/useBeds' + +export default function AssignBedModal({ admission, onClose }: any) { + + const [wardId, setWardId] = useState('') + const [roomId, setRoomId] = useState('') + const [bedId, setBedId] = useState('') + + const { data: wards = [] } = useWards() + const { data: rooms = [] } = useRooms(wardId) + const { data: beds = [] } = useBeds(roomId) + + const assignMutation = useAssignBed() + + const handleAssign = () => { + + assignMutation.mutate({ + admissionId: admission.id, + patientId: admission.patient_id, + bedId + },{ + onSuccess(){ + onClose() + } + }) + + } + + return ( + +
+ +
+ +

+ Assign Bed +

+ + {/* Ward */} + + + + {/* Room */} + + + + {/* Bed */} + + + +
+ + + + + +
+ +
+ +
+ ) +} \ No newline at end of file diff --git a/frontend/src/features/admissions/components/DischargeRequests.tsx b/frontend/src/features/admissions/components/DischargeRequests.tsx new file mode 100644 index 0000000..4977b35 --- /dev/null +++ b/frontend/src/features/admissions/components/DischargeRequests.tsx @@ -0,0 +1,67 @@ +'use client' + +import { useDischargeRequests } from '../hooks/useDischargeRequests' +import { useDischargePatient } from '../hooks/useDischargePatient' + +export default function DischargeRequests() { + + const { data, isLoading } = useDischargeRequests() + + const dischargeMutation = useDischargePatient() + + if (isLoading) { + return
Loading discharge requests...
+ } + + if (!data?.length) { + return ( +
+ No discharge requests +
+ ) + } + + return ( + +
+ + {data.map((req:any)=>( +
+ +
+ +
+ Patient: {req.patient_id} +
+ +
+ Ward: {req.ward} +
+ +
+ Bed: {req.bed_number} +
+ +
+ + + +
+ ))} + +
+ ) +} \ No newline at end of file diff --git a/frontend/src/features/admissions/components/DoctorPatients.tsx b/frontend/src/features/admissions/components/DoctorPatients.tsx new file mode 100644 index 0000000..be0d8c4 --- /dev/null +++ b/frontend/src/features/admissions/components/DoctorPatients.tsx @@ -0,0 +1,62 @@ +'use client' + +import { useDoctorAdmissions } from '../hooks/useDoctorAdmissions' +import { useRequestDischarge } from '../hooks/useRequestDischarge' + +export default function DoctorPatients() { + + const { data, isLoading } = useDoctorAdmissions() + + const dischargeMutation = useRequestDischarge() + + if (isLoading) { + return
Loading patients...
+ } + + if (!data?.length) { + return ( +
+ No admitted patients +
+ ) + } + + return ( + +
+ + {data.map((admission:any)=>( +
+ +
+ +
+ Patient: {admission.patient_id} +
+ +
+ Ward: {admission.ward} +
+ +
+ Bed: {admission.bed_number} +
+ +
+ + + +
+ ))} + +
+ ) +} \ No newline at end of file diff --git a/frontend/src/features/admissions/components/StaffQueueCard.tsx b/frontend/src/features/admissions/components/StaffQueueCard.tsx new file mode 100644 index 0000000..bc55cfb --- /dev/null +++ b/frontend/src/features/admissions/components/StaffQueueCard.tsx @@ -0,0 +1,41 @@ +'use client' + +import { useRequestAdmission } from '@/src/features/admissions/hooks/useRequestAdmission' + +export default function StaffQueueCard({ appointment }: any) { + + const requestAdmissionMutation = useRequestAdmission() + + const handleAdmit = () => { + requestAdmissionMutation.mutate({ + patientId: appointment.patient_id, + doctorId: appointment.doctor_id, + departmentId: appointment.department_id + }) + } + + return ( + +
+ +
+
+ Patient: {appointment.patient_name} +
+ +
+ Time: {appointment.appointment_time} +
+
+ + + +
+ + ) +} \ No newline at end of file diff --git a/frontend/src/features/admissions/hooks/useAdmissions.ts b/frontend/src/features/admissions/hooks/useAdmissions.ts new file mode 100644 index 0000000..05117ac --- /dev/null +++ b/frontend/src/features/admissions/hooks/useAdmissions.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; +import { getPendingAdmissions } from '../api/admissions.api'; + +export function useAdmissions() { + return useQuery({ + queryKey: ['admissions'], + queryFn: getPendingAdmissions, + }); +} diff --git a/frontend/src/features/admissions/hooks/useAssignBed.ts b/frontend/src/features/admissions/hooks/useAssignBed.ts new file mode 100644 index 0000000..7b4e313 --- /dev/null +++ b/frontend/src/features/admissions/hooks/useAssignBed.ts @@ -0,0 +1,15 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { assignBed } from '../api/assignBed.api'; + +export const useAssignBed = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: assignBed, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['admissions'], + }); + }, + }); +}; diff --git a/frontend/src/features/admissions/hooks/useBeds.ts b/frontend/src/features/admissions/hooks/useBeds.ts new file mode 100644 index 0000000..d6e990b --- /dev/null +++ b/frontend/src/features/admissions/hooks/useBeds.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/src/lib/api'; + +export const useBeds = (roomId: string) => { + return useQuery({ + queryKey: ['beds', roomId], + enabled: !!roomId, + queryFn: async () => { + const res = await api.get(`/staff/beds/beds/${roomId}`); + return res.data; + }, + }); +}; diff --git a/frontend/src/features/admissions/hooks/useDischargePatient.ts b/frontend/src/features/admissions/hooks/useDischargePatient.ts new file mode 100644 index 0000000..390d334 --- /dev/null +++ b/frontend/src/features/admissions/hooks/useDischargePatient.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { dischargePatient } from '../api/dischargePatients.api'; + +export function useDischargePatient() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: dischargePatient, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['discharge-requests'], + }); + + queryClient.invalidateQueries({ + queryKey: ['beds'], + }); + }, + }); +} diff --git a/frontend/src/features/admissions/hooks/useDischargeRequests.ts b/frontend/src/features/admissions/hooks/useDischargeRequests.ts new file mode 100644 index 0000000..6f09312 --- /dev/null +++ b/frontend/src/features/admissions/hooks/useDischargeRequests.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; +import { getDischargeRequests } from '../api/getDischargeRequests.api'; + +export function useDischargeRequests() { + return useQuery({ + queryKey: ['discharge-requests'], + queryFn: getDischargeRequests, + }); +} diff --git a/frontend/src/features/admissions/hooks/useDoctorAdmissions.ts b/frontend/src/features/admissions/hooks/useDoctorAdmissions.ts new file mode 100644 index 0000000..108daa5 --- /dev/null +++ b/frontend/src/features/admissions/hooks/useDoctorAdmissions.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; +import { getDoctorAdmissions } from '../api/getDoctorAdmissions.api'; + +export const useDoctorAdmissions = () => { + return useQuery({ + queryKey: ['doctor-admissions'], + queryFn: getDoctorAdmissions, + }); +}; diff --git a/frontend/src/features/admissions/hooks/useRequestAdmission.ts b/frontend/src/features/admissions/hooks/useRequestAdmission.ts new file mode 100644 index 0000000..f20b763 --- /dev/null +++ b/frontend/src/features/admissions/hooks/useRequestAdmission.ts @@ -0,0 +1,8 @@ +import { useMutation } from '@tanstack/react-query'; +import { requestAdmission } from '../api/requestAdmission.api'; + +export function useRequestAdmission() { + return useMutation({ + mutationFn: requestAdmission, + }); +} diff --git a/frontend/src/features/admissions/hooks/useRequestDischarge.ts b/frontend/src/features/admissions/hooks/useRequestDischarge.ts new file mode 100644 index 0000000..82ee569 --- /dev/null +++ b/frontend/src/features/admissions/hooks/useRequestDischarge.ts @@ -0,0 +1,8 @@ +import { useMutation } from '@tanstack/react-query'; +import { requestDischarge } from '../api/requestDischarge.api'; + +export const useRequestDischarge = () => { + return useMutation({ + mutationFn: requestDischarge, + }); +}; diff --git a/frontend/src/features/admissions/hooks/useRooms.ts b/frontend/src/features/admissions/hooks/useRooms.ts new file mode 100644 index 0000000..c1f50ce --- /dev/null +++ b/frontend/src/features/admissions/hooks/useRooms.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/src/lib/api'; + +export const useRooms = (wardId: string) => { + return useQuery({ + queryKey: ['rooms', wardId], + enabled: !!wardId, + queryFn: async () => { + const res = await api.get(`/staff/beds/rooms/${wardId}`); + return res.data; + }, + }); +}; diff --git a/frontend/src/features/admissions/hooks/useWards.ts b/frontend/src/features/admissions/hooks/useWards.ts new file mode 100644 index 0000000..5cf5774 --- /dev/null +++ b/frontend/src/features/admissions/hooks/useWards.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/src/lib/api'; + +export const useWards = () => { + return useQuery({ + queryKey: ['wards'], + queryFn: async () => { + const res = await api.get('/staff/beds/wards'); + return res.data; + }, + }); +}; diff --git a/frontend/src/features/beds/api/beds.api.ts b/frontend/src/features/beds/api/beds.api.ts new file mode 100644 index 0000000..261dd94 --- /dev/null +++ b/frontend/src/features/beds/api/beds.api.ts @@ -0,0 +1,16 @@ +import { api } from '@/src/lib/api'; + +export async function getBeds() { + const res = await api.get('/staff/beds'); + return res.data; +} + +export async function assignBed(data: { bedId: string; patientId: string }) { + const res = await api.post('/staff/beds/assign', data); + return res.data; +} + +export async function dischargeBed(bedId: string) { + const res = await api.post('/staff/beds/discharge', { bedId }); + return res.data; +} diff --git a/frontend/src/features/beds/components/BedsBoard.tsx b/frontend/src/features/beds/components/BedsBoard.tsx new file mode 100644 index 0000000..355d037 --- /dev/null +++ b/frontend/src/features/beds/components/BedsBoard.tsx @@ -0,0 +1,40 @@ +'use client' + +import { useBeds } from '../hooks/useBeds' + +export default function BedsBoard() { + const { data, isLoading } = useBeds() + + if (isLoading) return

Loading beds...

+ + return ( +
+ {data?.map((bed: any) => ( +
+
+

+ Ward: {bed.ward} | Room: {bed.room_number} +

+ +

Bed: {bed.bed_number}

+
+ + + {bed.status} + +
+ ))} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/features/beds/hooks/useAssignedBed.ts b/frontend/src/features/beds/hooks/useAssignedBed.ts new file mode 100644 index 0000000..f694bd1 --- /dev/null +++ b/frontend/src/features/beds/hooks/useAssignedBed.ts @@ -0,0 +1,13 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { assignBed } from '../api/beds.api'; + +export function useAssignBed() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: assignBed, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['beds'] }); + }, + }); +} diff --git a/frontend/src/features/beds/hooks/useBeds.ts b/frontend/src/features/beds/hooks/useBeds.ts new file mode 100644 index 0000000..96ab64d --- /dev/null +++ b/frontend/src/features/beds/hooks/useBeds.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; +import { getBeds } from '../api/beds.api'; + +export function useBeds() { + return useQuery({ + queryKey: ['beds'], + queryFn: getBeds, + }); +} diff --git a/frontend/src/staff/auth/staff.auth.types.ts b/frontend/src/staff/auth/staff.auth.types.ts index 35269b7..88f047e 100644 --- a/frontend/src/staff/auth/staff.auth.types.ts +++ b/frontend/src/staff/auth/staff.auth.types.ts @@ -1,3 +1,4 @@ +// import { strict } from 'assert'; import { z } from 'zod'; export const StaffSchema = z.object({ @@ -5,6 +6,7 @@ export const StaffSchema = z.object({ name: z.string(), email: z.string().email(), department: z.string(), + department_id: z.string(), role: z.string(), job_title: z.string(), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4001343..0ef7cbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,6 +251,9 @@ importers: '@h-os/shared': specifier: workspace:* version: link:../shared + axios: + specifier: ^1.13.5 + version: 1.13.5 bcrypt: specifier: ^6.0.0 version: 6.0.0 diff --git a/services/patient-service/scripts/init-db.ts b/services/patient-service/scripts/init-db.ts index b8c77c6..711f558 100644 --- a/services/patient-service/scripts/init-db.ts +++ b/services/patient-service/scripts/init-db.ts @@ -21,6 +21,20 @@ async function init() { ); `); + // Admissions table + + await pool.query(`CREATE TABLE IF NOT EXISTS admissions ( + id UUID PRIMARY KEY, + patient_id UUID NOT NULL, + doctor_id UUID NOT NULL, + department_id UUID NOT NULL, + + status TEXT NOT NULL DEFAULT 'REQUESTED', + + created_at TIMESTAMP DEFAULT now(), + admitted_at TIMESTAMP, + discharged_at TIMESTAMP +);`); /** * PATIENT PROFILES TABLE * Extended medical + personal information diff --git a/services/patient-service/src/app.ts b/services/patient-service/src/app.ts index 4bc6ab3..f2fc315 100644 --- a/services/patient-service/src/app.ts +++ b/services/patient-service/src/app.ts @@ -6,6 +6,7 @@ import healthRoutes from './routes/health'; import authRoutes from './modules/auth/auth.routes'; import { errorMiddleware } from './middlewares/error.middleware'; import appointmentRoutes from './modules/appointments/appointment.routes'; +import admissionRoutes from './modules/admissions/admission.routes'; const app = express(); // Middleware @@ -35,6 +36,7 @@ app.use('/patients', patientRoutes); app.use('/appointments', appointmentRoutes); +app.use('/admissions', admissionRoutes); // Error handling app.use(errorMiddleware); diff --git a/services/patient-service/src/modules/admissions/admission.controller.ts b/services/patient-service/src/modules/admissions/admission.controller.ts new file mode 100644 index 0000000..6db22a0 --- /dev/null +++ b/services/patient-service/src/modules/admissions/admission.controller.ts @@ -0,0 +1,74 @@ +import { Request, Response } from 'express'; +import { admissionRepository } from './admission.repository'; +import { admissionService } from './admission.service'; + +export class AdmissionController { + async requestAdmission(req: Request, res: Response) { + const { patientId, doctorId, departmentId } = req.body; + + const admission = await admissionRepository.createAdmission({ + patientId, + doctorId, + departmentId, + }); + + res.json({ + message: 'Admission requested', + admission, + }); + } + + async getPending(req: Request, res: Response) { + const admissions = await admissionRepository.getPendingAdmissions(); + + res.json(admissions); + } + + async admitPatient(req: Request, res: Response) { + const admissionId = req.params.id as string; + + await admissionService.admitPatient(admissionId); + + res.json({ + message: 'Patient admitted', + }); + } + + async requestDischarge(req: Request, res: Response) { + const admissionId = req.params.id as string; + + await admissionService.requestDischarge(admissionId); + + res.json({ + message: 'Discharge requested', + }); + } + + async completeDischarge(req: Request, res: Response) { + const admissionId = req.params.id as string; + + await admissionService.completeDischarge(admissionId); + + res.json({ + message: 'Patient discharged', + }); + } + async getDoctorAdmissions(req: Request, res: Response) { + const doctorId = req.headers['x-user-id'] as string; + + if (!doctorId) { + return res.status(401).json({ + error: 'Unauthorized', + }); + } + + const admissions = await admissionService.getDoctorAdmissions(doctorId); + + res.json(admissions); + } + async getDischargeRequests(req: Request, res: Response) { + const requests = await admissionService.getDischargeRequests(); + + res.json(requests); + } +} diff --git a/services/patient-service/src/modules/admissions/admission.repository.ts b/services/patient-service/src/modules/admissions/admission.repository.ts new file mode 100644 index 0000000..c2e551b --- /dev/null +++ b/services/patient-service/src/modules/admissions/admission.repository.ts @@ -0,0 +1,109 @@ +import { pool } from '../../db'; +import { randomUUID } from 'crypto'; + +class AdmissionRepository { + async createAdmission(data: { + patientId: string; + doctorId: string; + departmentId: string; + }) { + const result = await pool.query( + ` + INSERT INTO admissions + (id, patient_id, doctor_id, department_id, status) + VALUES ($1,$2,$3,$4,'REQUESTED') + RETURNING * + `, + [randomUUID(), data.patientId, data.doctorId, data.departmentId] + ); + + return result.rows[0]; + } + + async getPendingAdmissions() { + const result = await pool.query( + ` + SELECT * + FROM admissions + WHERE status='REQUESTED' + ORDER BY created_at ASC + ` + ); + + return result.rows; + } + + async markAdmitted(id: string) { + await pool.query( + ` + UPDATE admissions + SET status='ADMITTED', + admitted_at = now() + WHERE id=$1 + `, + [id] + ); + } + async requestDischarge(id: string) { + await pool.query( + ` + UPDATE admissions + SET discharge_requested = true, + discharge_requested_at = now() + WHERE id = $1 + `, + [id] + ); + } + + async completeDischarge(id: string) { + await pool.query( + ` + UPDATE admissions + SET status = 'DISCHARGED', + discharge_requested = false + WHERE id = $1 + `, + [id] + ); + } + + async getDoctorAdmissions(doctorId: string) { + const result = await pool.query( + ` + SELECT + id, + patient_id, + doctor_id, + department_id, + status, + created_at + FROM admissions + WHERE doctor_id = $1 + AND status = 'ADMITTED' + ORDER BY created_at DESC + `, + [doctorId] + ); + + return result.rows; + } + async getDischargeRequests() { + const result = await pool.query( + ` + SELECT + id, + patient_id, + doctor_id + FROM admissions + WHERE discharge_requested = true + AND status = 'ADMITTED' + ORDER BY created_at ASC + ` + ); + + return result.rows; + } +} + +export const admissionRepository = new AdmissionRepository(); diff --git a/services/patient-service/src/modules/admissions/admission.routes.ts b/services/patient-service/src/modules/admissions/admission.routes.ts new file mode 100644 index 0000000..28fed7d --- /dev/null +++ b/services/patient-service/src/modules/admissions/admission.routes.ts @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import { AdmissionController } from './admission.controller'; + +const router = Router(); +const controller = new AdmissionController(); + +router.post('/request', controller.requestAdmission.bind(controller)); +router.get('/pending', controller.getPending.bind(controller)); +router.post('/:id/admit', controller.admitPatient.bind(controller)); + +router.get('/doctor', controller.getDoctorAdmissions.bind(controller)); +router.get( + '/discharge-requests', + controller.getDischargeRequests.bind(controller) +); + +router.post( + '/:id/request-discharge', + controller.requestDischarge.bind(controller) +); +router.post('/:id/discharged', controller.completeDischarge.bind(controller)); +export default router; diff --git a/services/patient-service/src/modules/admissions/admission.service.ts b/services/patient-service/src/modules/admissions/admission.service.ts new file mode 100644 index 0000000..197efa3 --- /dev/null +++ b/services/patient-service/src/modules/admissions/admission.service.ts @@ -0,0 +1,32 @@ +import { admissionRepository } from './admission.repository'; + +class AdmissionService { + async requestAdmission(data: any) { + return admissionRepository.createAdmission(data); + } + + async getPendingAdmissions() { + return admissionRepository.getPendingAdmissions(); + } + + async admitPatient(admissionId: string) { + return admissionRepository.markAdmitted(admissionId); + } + + async requestDischarge(admissionId: string) { + await admissionRepository.requestDischarge(admissionId); + } + + async completeDischarge(admissionId: string) { + await admissionRepository.completeDischarge(admissionId); + } + + async getDoctorAdmissions(doctorId: string) { + return admissionRepository.getDoctorAdmissions(doctorId); + } + async getDischargeRequests() { + return admissionRepository.getDischargeRequests(); + } +} + +export const admissionService = new AdmissionService(); diff --git a/services/patient-service/src/modules/appointments/appointment.service.ts b/services/patient-service/src/modules/appointments/appointment.service.ts index d0d5af1..8b6de8b 100644 --- a/services/patient-service/src/modules/appointments/appointment.service.ts +++ b/services/patient-service/src/modules/appointments/appointment.service.ts @@ -1,34 +1,31 @@ import axios from 'axios'; import { pool } from '../../db'; import { appointmentRepository } from './appointment.repository'; -import { error } from 'console'; export class AppointmentService { + async validateDoctor(doctorId: string) { + await axios.get(`${process.env.STAFF_SERVICE_URL}/staff/by-id/${doctorId}`); + } + async bookAppointment(data: { doctorId: string; patientId: string; appointmentTime: string; durationMinutes?: number; }) { - // Validate doctor exists try { - console.log('BOOK SERVICE HIT'); - await axios.get( - `${process.env.STAFF_SERVICE_URL}/staff/${data.doctorId}` - ); + await this.validateDoctor(data.doctorId); } catch { throw new Error('Doctor not found'); } - // 2️⃣ Validate booking is inside availability - const appointmentDate = new Date(data.appointmentTime); - const now = new Date(); if (appointmentDate <= now) { throw new Error('PAST_TIME_NOT_ALLOWED'); } + const dayOfWeek = appointmentDate.getDay(); const availabilityResponse = await axios.get( @@ -63,7 +60,6 @@ export class AppointmentService { throw new Error('OUTSIDE_WORKING_HOURS'); } - // validate slot alignment const diffMinutes = (appointmentDate.getTime() - start.getTime()) / 60000; if (diffMinutes % slot_duration !== 0) { @@ -156,17 +152,13 @@ export class AppointmentService { currentTime = new Date(appt.actual_end_time); } else if (appt.status === 'IN_PROGRESS' && appt.actual_start_time) { const actualStart = new Date(appt.actual_start_time); + const plannedEnd = new Date( actualStart.getTime() + appt.duration_minutes * 60000 ); const now = new Date(); - console.log('Raw DB time:', appt.appointment_time); - console.log('JS Date:', new Date(appt.appointment_time)); - console.log('ISO:', new Date(appt.appointment_time).toISOString()); - - // If doctor exceeded duration, use real time currentTime = now > plannedEnd ? now : plannedEnd; estimatedStart = actualStart; @@ -206,8 +198,6 @@ export class AppointmentService { }; }); - // 🧠 Doctor Delay Calculation - let doctorDelayMinutes = 0; if (queue.length > 0) { @@ -220,11 +210,9 @@ export class AppointmentService { ); } - // 🧠 Next Available Time const nextAvailableTime = queue.length > 0 ? queue[queue.length - 1].estimated_end_time : null; - // 🧠 Remaining Queue Minutes (from now) let remainingQueueMinutes = 0; if (nextAvailableTime) { @@ -247,100 +235,6 @@ export class AppointmentService { }; } - async setPriority(appointmentId: string, priority: 'NORMAL' | 'HIGH') { - const appointment = await appointmentRepository.updatePriority( - appointmentId, - priority - ); - - if (!appointment) { - throw new Error('NOT_FOUND'); - } - - return appointment; - } - - async getMyStatus(patientId: string) { - const today = new Date().toLocaleDateString('en-CA'); - - const myAppointment = - await appointmentRepository.getPatientActiveAppointment(patientId, today); - - if (!myAppointment) { - return null; - } - - // get full doctor queue - const doctorQueue = await this.getDoctorQueueForDay( - myAppointment.doctor_id, - today - ); - - const myQueueItem = doctorQueue.queue.find( - (q) => q.id === myAppointment.id - ); - - if (!myQueueItem) { - return null; - } - - return { - appointment_id: myAppointment.id, - doctor_id: myAppointment.doctor_id, - status: myQueueItem.status, - position: myQueueItem.position, - patients_ahead: myQueueItem.patients_ahead, - estimated_start_time: myQueueItem.estimated_start_time, - estimated_end_time: myQueueItem.estimated_end_time, - delay_minutes: myQueueItem.delay_minutes, - doctor_delay_minutes: doctorQueue.doctor_status.doctor_delay_minutes, - doctor_current_patient: doctorQueue.doctor_status.current_patient, - }; - } - - async cancelAppointment(appointmentId: string, patientId: string) { - const result = await pool.query( - `SELECT * FROM appointments WHERE id = $1`, - [appointmentId] - ); - - const appt = result.rows[0]; - - if (!appt) { - throw new Error('NOT_FOUND'); - } - - if (appt.patient_id !== patientId) { - throw new Error('FORBIDDEN'); - } - - if (appt.status !== 'SCHEDULED') { - throw new Error('CANNOT_CANCEL'); - } - - const now = new Date(); - const appointmentTime = new Date(appt.appointment_time); - - const diffMinutes = (appointmentTime.getTime() - now.getTime()) / 60000; - - if (diffMinutes < 60) { - throw new Error('TOO_LATE_TO_CANCEL'); - } - - const updated = await pool.query( - ` - UPDATE appointments - SET status = 'CANCELLED', - updated_at = now() - WHERE id = $1 - RETURNING * - `, - [appointmentId] - ); - - return updated.rows[0]; - } - async getPatientHistory(patientId: string) { const appointments = await appointmentRepository.getPatientHistory(patientId); @@ -349,7 +243,7 @@ export class AppointmentService { appointments.map(async (appt) => { try { const response = await axios.get( - `${process.env.STAFF_SERVICE_URL}/staff/${appt.doctor_id}` + `${process.env.STAFF_SERVICE_URL}/staff/by-id/${appt.doctor_id}` ); const doctor = response.data; @@ -372,66 +266,6 @@ export class AppointmentService { return enriched; } - async rescheduleAppointment( - appointmentId: string, - patientId: string, - newTime: string - ) { - const result = await pool.query( - `SELECT * FROM appointments WHERE id = $1`, - [appointmentId] - ); - - const appt = result.rows[0]; - - if (!appt) { - throw new Error('NOT_FOUND'); - } - - if (appt.patient_id !== patientId) { - throw new Error('FORBIDDEN'); - } - - if (appt.status !== 'SCHEDULED') { - throw new Error('CANNOT_RESCHEDULE'); - } - - const now = new Date(); - const oldTime = new Date(appt.appointment_time); - - const diffMinutes = (oldTime.getTime() - now.getTime()) / 60000; - - if (diffMinutes < 60) { - throw new Error('TOO_LATE_TO_RESCHEDULE'); - } - - // 1️⃣ Cancel old - await pool.query( - ` - UPDATE appointments - SET status = 'CANCELLED', - updated_at = now() - WHERE id = $1 - `, - [appointmentId] - ); - - // 2️⃣ Create new appointment - const newAppointment = await appointmentRepository.createAppointment({ - doctorId: appt.doctor_id, - patientId, - appointmentTime: new Date(newTime), - durationMinutes: appt.duration_minutes, - priority: 'NORMAL', - }); - - if (!newAppointment) { - throw new Error('SLOT_TAKEN'); - } - - return newAppointment; - } - async getAvailableSlots(doctorId: string, date: string) { const dayOfWeek = new Date(date).getDay(); @@ -447,7 +281,6 @@ export class AppointmentService { const { start_time, end_time, slot_duration } = availability; - // Generate slots const slots: string[] = []; const start = new Date(`${date}T${start_time}`); @@ -464,25 +297,20 @@ export class AppointmentService { current = new Date(current.getTime() + slot_duration * 60000); } - // Remove booked slots const booked = await appointmentRepository.getBookedSlots(doctorId, date); const available = slots.filter((slot) => !booked.includes(slot)); return available; } - async createEmergencyAppointment(doctorId: string, patientId: string) { - // Validate doctor exists try { - await axios.get(`${process.env.STAFF_SERVICE_URL}/staff/${doctorId}`); + await this.validateDoctor(doctorId); } catch { throw new Error('Doctor not found'); } const now = new Date(); - - // Get doctor availability to fetch slot duration const dayOfWeek = now.getDay(); const availabilityResponse = await axios.get( @@ -497,7 +325,6 @@ export class AppointmentService { const { slot_duration } = availability; - // Create emergency appointment immediately const appointment = await appointmentRepository.createAppointment({ doctorId, patientId, @@ -510,7 +337,6 @@ export class AppointmentService { throw new Error('SLOT_TAKEN'); } - // Immediately check in await this.updateStatus(appointment.id, 'CHECKED_IN'); return appointment; diff --git a/services/staff-service/package.json b/services/staff-service/package.json index f454291..1183025 100644 --- a/services/staff-service/package.json +++ b/services/staff-service/package.json @@ -15,11 +15,12 @@ "license": "ISC", "packageManager": "pnpm@10.28.2", "dependencies": { + "@h-os/shared": "workspace:*", + "axios": "^1.13.5", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "express": "^5.2.1", - "jsonwebtoken": "^9.0.3", - "@h-os/shared": "workspace:*" + "jsonwebtoken": "^9.0.3" }, "devDependencies": { "@types/bcrypt": "^6.0.0", diff --git a/services/staff-service/scripts/init-db.ts b/services/staff-service/scripts/init-db.ts index bc5ca6a..479c442 100644 --- a/services/staff-service/scripts/init-db.ts +++ b/services/staff-service/scripts/init-db.ts @@ -1,4 +1,4 @@ -import { pool } from '../src/db.ts'; +import { pool } from '../src/db'; import { randomUUID } from 'crypto'; async function init() { @@ -38,7 +38,7 @@ async function init() { } /** - * 2️⃣ Staff Table (FOREIGN KEY version) + * 2️⃣ Staff Table */ await pool.query(` CREATE TABLE IF NOT EXISTS staff ( @@ -71,7 +71,72 @@ async function init() { ); `); - console.log('✅ departments + staff + doctor_availability tables ready'); + /** + * 4️⃣ Wards Table + */ + await pool.query(` + CREATE TABLE IF NOT EXISTS wards ( + id UUID PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT now() + ); + `); + + /** + * 5️⃣ Rooms Table + */ + await pool.query(` + CREATE TABLE IF NOT EXISTS rooms ( + id UUID PRIMARY KEY, + ward_id UUID REFERENCES wards(id) ON DELETE CASCADE, + room_number TEXT NOT NULL, + created_at TIMESTAMP DEFAULT now(), + UNIQUE (ward_id, room_number) + ); + `); + + /** + * 6️⃣ Beds Table + */ + await pool.query(` + CREATE TABLE IF NOT EXISTS beds ( + id UUID PRIMARY KEY, + room_id UUID REFERENCES rooms(id) ON DELETE CASCADE, + bed_number TEXT NOT NULL, + bed_type TEXT DEFAULT 'GENERAL', + status TEXT DEFAULT 'AVAILABLE', + created_at TIMESTAMP DEFAULT now(), + UNIQUE (room_id, bed_number) + ); + `); + + /** + * 7️⃣ Bed Assignments Table + */ + await pool.query(` + CREATE TABLE IF NOT EXISTS bed_assignments ( + id UUID PRIMARY KEY, + bed_id UUID REFERENCES beds(id) ON DELETE CASCADE, + patient_id UUID NOT NULL, + assigned_at TIMESTAMP DEFAULT now(), + discharged_at TIMESTAMP + ); + `); + + /** + * 8️⃣ Index for fast lookup of active assignments + */ + await pool.query(` + CREATE INDEX IF NOT EXISTS idx_active_bed_assignments + ON bed_assignments (bed_id) + WHERE discharged_at IS NULL; + `); + + console.log( + '✅ departments + staff + doctor_availability + bed management tables ready' + ); + process.exit(0); } diff --git a/services/staff-service/src/app.ts b/services/staff-service/src/app.ts index e3f3ef5..977a9a5 100644 --- a/services/staff-service/src/app.ts +++ b/services/staff-service/src/app.ts @@ -3,6 +3,8 @@ import cookieParser from 'cookie-parser'; import healthRouter from './routes/health'; import authRouter from './modules/auth/auth.routes'; import staffRouter from './modules/staff/staff.routes'; +import bedRoutes from './modules/beds/bed.routes'; +import adminBedRoutes from './modules/beds/admin.bed.routes'; // import departmentRoutes from './modules/staff/department.routes' const app = express(); @@ -11,6 +13,9 @@ app.use(cookieParser()); app.use('/health', healthRouter); app.use('/auth', authRouter); +app.use('/staff/beds', bedRoutes); +app.use('/admin/beds', adminBedRoutes); + app.use('/staff', staffRouter); // app.use('/departments',departmentRoutes); diff --git a/services/staff-service/src/modules/auth/staff.auth.controller.ts b/services/staff-service/src/modules/auth/staff.auth.controller.ts index d1ef41c..8ebc807 100644 --- a/services/staff-service/src/modules/auth/staff.auth.controller.ts +++ b/services/staff-service/src/modules/auth/staff.auth.controller.ts @@ -119,6 +119,7 @@ export class StaffAuthController { role: staff.role, job_title: staff.job_title, department: staff.department, + department_id: staff.department_id, }, }); } catch { @@ -170,10 +171,25 @@ export class StaffAuthController { /** * 🔐 NEW ACCESS TOKEN */ + // fetch staff info + const staffResult = await pool.query( + ` +SELECT role, job_title, department_id +FROM staff +WHERE id = $1 +`, + [stored.staff_id] + ); + + const staff = staffResult.rows[0]; + const newAccessToken = jwt.sign( { sub: stored.staff_id, type: 'STAFF', + role: staff.role, + job_title: staff.job_title, + department_id: staff.department_id, }, process.env.JWT_SECRET!, { expiresIn: ACCESS_TOKEN_EXPIRES_IN } @@ -279,11 +295,17 @@ export class StaffAuthController { } const result = await pool.query( - ` - SELECT id, name, email, department_id, role, job_title - FROM staff - WHERE id = $1 - `, + `SELECT + s.id, + s.name, + s.email, + s.role, + s.job_title, + d.id AS department_id, + d.name AS department +FROM staff s +JOIN departments d ON s.department_id = d.id +WHERE s.id = $1`, [payload.sub] ); diff --git a/services/staff-service/src/modules/beds/admin.bed.routes.ts b/services/staff-service/src/modules/beds/admin.bed.routes.ts new file mode 100644 index 0000000..0d3f488 --- /dev/null +++ b/services/staff-service/src/modules/beds/admin.bed.routes.ts @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { BedsController } from './bed.controller'; + +const router = Router(); +const controller = new BedsController(); + +// ADMIN INFRASTRUCTURE MANAGEMENT + +router.get('/wards', controller.getWards.bind(controller)); + +router.get('/rooms/:wardId', controller.getRooms.bind(controller)); + +router.post('/wards', controller.createWard.bind(controller)); + +router.post('/rooms', controller.createRoom.bind(controller)); + +router.post('/beds', controller.createBed.bind(controller)); + +export default router; diff --git a/services/staff-service/src/modules/beds/bed.controller.ts b/services/staff-service/src/modules/beds/bed.controller.ts new file mode 100644 index 0000000..a31e3f6 --- /dev/null +++ b/services/staff-service/src/modules/beds/bed.controller.ts @@ -0,0 +1,79 @@ +import { Request, Response } from 'express'; +import { bedsService } from './bed.service'; + +export class BedsController { + async createWard(req: Request, res: Response) { + const { name, description } = req.body; + + const ward = await bedsService.createWard(name, description); + + res.json(ward); + } + + async createRoom(req: Request, res: Response) { + const { wardId, roomNumber } = req.body; + + const room = await bedsService.createRoom(wardId, roomNumber); + + res.json(room); + } + + async createBed(req: Request, res: Response) { + const { roomId, bedNumber } = req.body; + + const bed = await bedsService.createBed(roomId, bedNumber); + + res.json(bed); + } + + async getBeds(req: Request, res: Response) { + const beds = await bedsService.getBeds(); + + res.json(beds); + } + + async assignBed(req: Request, res: Response) { + const { bedId, patientId, admissionId } = req.body; + + const assignment = await bedsService.assignBed( + bedId, + patientId, + admissionId + ); + + res.json({ + message: 'Bed assigned successfully', + assignment, + }); + } + + async dischargePatient(req: Request, res: Response) { + const { admissionId } = req.body; + + await bedsService.dischargePatient(admissionId); + + res.json({ + message: 'Patient discharged and bed released', + }); + } + + async getWards(req: Request, res: Response) { + const wards = await bedsService.getWards(); + res.json(wards); + } + async getBedsByRoom(req: Request, res: Response) { + const roomId = req.params.roomId as string; + + const beds = await bedsService.getBedsByRoom(roomId); + + res.json(beds); + } + + async getRooms(req: Request, res: Response) { + const wardId = req.params.wardId as string; + + const rooms = await bedsService.getRoomsByWard(wardId); + + res.json(rooms); + } +} diff --git a/services/staff-service/src/modules/beds/bed.routes.ts b/services/staff-service/src/modules/beds/bed.routes.ts new file mode 100644 index 0000000..1e01095 --- /dev/null +++ b/services/staff-service/src/modules/beds/bed.routes.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; +import { BedsController } from './bed.controller'; + +const router = Router(); +const controller = new BedsController(); + +// STAFF OPERATIONS ONLY +router.get('/', controller.getBeds.bind(controller)); + +router.get('/wards', controller.getWards.bind(controller)); + +router.get('/rooms/:wardId', controller.getRooms.bind(controller)); + +router.get('/beds/:roomId', controller.getBedsByRoom.bind(controller)); + +router.post('/assign', controller.assignBed.bind(controller)); + +router.post('/discharge', controller.dischargePatient.bind(controller)); + +export default router; diff --git a/services/staff-service/src/modules/beds/bed.service.ts b/services/staff-service/src/modules/beds/bed.service.ts new file mode 100644 index 0000000..23d302d --- /dev/null +++ b/services/staff-service/src/modules/beds/bed.service.ts @@ -0,0 +1,174 @@ +import { pool } from '../../db'; +import { randomUUID } from 'crypto'; +import axios from 'axios'; + +class BedsService { + async createWard(name: string, description?: string) { + const result = await pool.query( + ` + INSERT INTO wards (id, name, description) + VALUES ($1,$2,$3) + RETURNING * + `, + [randomUUID(), name, description] + ); + + return result.rows[0]; + } + + async createRoom(wardId: string, roomNumber: string) { + const result = await pool.query( + ` + INSERT INTO rooms (id, ward_id, room_number) + VALUES ($1,$2,$3) + RETURNING * + `, + [randomUUID(), wardId, roomNumber] + ); + + return result.rows[0]; + } + + async createBed(roomId: string, bedNumber: string) { + const result = await pool.query( + ` + INSERT INTO beds (id, room_id, bed_number) + VALUES ($1,$2,$3) + RETURNING * + `, + [randomUUID(), roomId, bedNumber] + ); + + return result.rows[0]; + } + + async getBeds() { + const result = await pool.query( + ` + SELECT + b.id, + b.bed_number, + b.status, + r.room_number, + w.name AS ward + FROM beds b + JOIN rooms r ON b.room_id = r.id + JOIN wards w ON r.ward_id = w.id + ` + ); + + return result.rows; + } + + async assignBed(bedId: string, patientId: string, admissionId: string) { + const check = await pool.query(`SELECT status FROM beds WHERE id = $1`, [ + bedId, + ]); + + if (check.rows[0]?.status !== 'AVAILABLE') { + throw new Error('Bed not available'); + } + + // Mark bed occupied + await pool.query(`UPDATE beds SET status = 'OCCUPIED' WHERE id = $1`, [ + bedId, + ]); + + // Create assignment + const assignment = await pool.query( + ` + INSERT INTO bed_assignments (id, bed_id, patient_id, admission_id) + VALUES ($1,$2,$3,$4) + RETURNING * + `, + [randomUUID(), bedId, patientId, admissionId] + ); + + // 🔁 Notify patient-service admission is complete + await axios.post( + `${process.env.PATIENT_SERVICE_URL}/admissions/${admissionId}/admit` + ); + + return assignment.rows[0]; + } + + async dischargePatient(admissionId: string) { + const result = await pool.query( + ` + SELECT bed_id + FROM bed_assignments + WHERE admission_id = $1 + AND discharged_at IS NULL + `, + [admissionId] + ); + + const bedId = result.rows[0]?.bed_id; + + if (!bedId) { + throw new Error('Active bed assignment not found'); + } + + await pool.query( + ` + UPDATE bed_assignments + SET discharged_at = now() + WHERE admission_id = $1 + `, + [admissionId] + ); + + await pool.query( + ` + UPDATE beds + SET status = 'AVAILABLE' + WHERE id = $1 + `, + [bedId] + ); + + await axios.post( + `${process.env.PATIENT_SERVICE_URL}/admissions/${admissionId}/discharged` + ); + } + + async getWards() { + const result = await pool.query(` + SELECT id, name + FROM wards + ORDER BY name + `); + + return result.rows; + } + + async getRoomsByWard(wardId: string) { + const result = await pool.query( + ` + SELECT id, room_number + FROM rooms + WHERE ward_id = $1 + ORDER BY room_number + `, + [wardId] + ); + + return result.rows; + } + async getBedsByRoom(roomId: string) { + const result = await pool.query( + ` + SELECT id, bed_number, status + FROM beds + WHERE room_id = $1 + AND status = 'AVAILABLE' + ORDER BY bed_number + `, + [roomId] + ); + + return result.rows; + } +} + +export const bedsService = new BedsService(); diff --git a/services/staff-service/src/modules/staff/staff.routes.ts b/services/staff-service/src/modules/staff/staff.routes.ts index 775fed2..2d090fc 100644 --- a/services/staff-service/src/modules/staff/staff.routes.ts +++ b/services/staff-service/src/modules/staff/staff.routes.ts @@ -5,16 +5,22 @@ const router = Router(); const controller = new StaffController(); router.get('/departments', controller.getDepartments.bind(controller)); + router.get('/doctors', controller.getDoctorsByDepartment.bind(controller)); + router.post('/availability', controller.setAvailability.bind(controller)); + router.get( '/availability/:doctorId/:dayOfWeek', controller.getAvailability.bind(controller) ); router.post('/', controller.createStaff.bind(controller)); -router.get('/:id', controller.getStaffById.bind(controller)); -router.put('/:id', controller.updateStaff.bind(controller)); -router.delete('/:id', controller.deactivateStaff.bind(controller)); + +router.get('/by-id/:id', controller.getStaffById.bind(controller)); + +router.put('/by-id/:id', controller.updateStaff.bind(controller)); + +router.delete('/by-id/:id', controller.deactivateStaff.bind(controller)); export default router; From b8ee6969737393440464ad45f7db74873824fb58 Mon Sep 17 00:00:00 2001 From: linasliyakath Date: Wed, 25 Mar 2026 11:38:35 +0530 Subject: [PATCH 2/7] staff service ui --- frontend/src/app/staff/dashboard/layout.tsx | 184 ++++++++++------- frontend/src/app/staff/dashboard/page.tsx | 135 ++++++++++--- .../src/app/staff/dashboard/queue/page.tsx | 189 ++++++++++++------ .../admissions/components/AdmissionQueue.tsx | 80 ++++---- .../components/DischargeRequests.tsx | 78 +++++--- .../admissions/components/DoctorPatients.tsx | 70 ++++--- .../features/beds/components/BedsBoard.tsx | 166 ++++++++++++--- .../admissions/admission.controller.ts | 2 +- .../admissions/admission.repository.ts | 47 +++-- .../modules/admissions/admission.service.ts | 89 ++++++++- .../appointments/appointment.repository.ts | 15 +- .../appointments/appointment.service.ts | 37 ++++ .../src/modules/beds/bed.controller.ts | 12 ++ .../src/modules/beds/bed.routes.ts | 5 + .../src/modules/beds/bed.service.ts | 23 +++ .../src/modules/staff/staff.controller.ts | 16 ++ .../src/modules/staff/staff.routes.ts | 2 + .../src/modules/staff/staff.service.ts | 17 ++ 18 files changed, 855 insertions(+), 312 deletions(-) diff --git a/frontend/src/app/staff/dashboard/layout.tsx b/frontend/src/app/staff/dashboard/layout.tsx index 407c2e3..1588122 100644 --- a/frontend/src/app/staff/dashboard/layout.tsx +++ b/frontend/src/app/staff/dashboard/layout.tsx @@ -4,6 +4,24 @@ import { ReactNode, useEffect } from 'react'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import { useStaffAuth } from '../../../staff/auth/staff.auth.provider'; +import { motion } from 'framer-motion'; +import { + LayoutDashboard, + Calendar, + User, + LogOut, + Activity, + Settings, + Bed, + ClipboardPlus, + FileMinus +} from 'lucide-react'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; export default function StaffLayout({ children, @@ -20,90 +38,102 @@ export default function StaffLayout({ } }, [auth.isRestoring, auth.accessToken, router]); - if (auth.isRestoring) return
Loading...
; + if (auth.isRestoring) { + return ( +
+
+
+
+
+ ); + } if (!auth.accessToken) return null; - function SidebarLink({ - href, - label, - active, -}: { - href: string; - label: string; - active: boolean; -}) { - return ( - - {label} - - ); -} - - return ( -
- + {/* Bottom Actions */} +
+ + + + + Settings + + + + + + + Sign out + +
+ -
- {children} -
-
+
+ {children} +
+ + ); } \ No newline at end of file diff --git a/frontend/src/app/staff/dashboard/page.tsx b/frontend/src/app/staff/dashboard/page.tsx index 01a7b48..2871518 100644 --- a/frontend/src/app/staff/dashboard/page.tsx +++ b/frontend/src/app/staff/dashboard/page.tsx @@ -4,6 +4,7 @@ import { useStaffAuth } from '../../../staff/auth/staff.auth.provider'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; import Link from 'next/link'; +import { LayoutDashboard, Calendar, FileText, User, Activity, ClipboardPlus, Bed } from 'lucide-react'; export default function StaffDashboardPage() { const { auth, logout } = useStaffAuth(); @@ -15,42 +16,124 @@ export default function StaffDashboardPage() { } }, [auth.accessToken, auth.isRestoring, router]); - if (auth.isRestoring) { - return ( -
-
+ if (auth.isRestoring) return ( +
+
+
- ); - } +
+ ); if (!auth.accessToken) return null; + const staff = auth.staff; + const isDoctor = staff?.role === 'DOCTOR'; + return ( -
- - {/* 🔥 LOGOUT BUTTON */} -
- +
+ + {/* Header */} +
+
+
+ {staff?.name?.charAt(0) || 'S'} +
+ +
+

+ Welcome back{staff?.name ? `, ${staff?.name}` : ''} +

+

+ {isDoctor ? "Manage your patients and queue for the day." : "Oversee hospital operations and admission requests."} +

+
+
+
+ + {/* Stats Cards */} +
+ +
+
+ +
+
+

Role

+

{staff?.role}

+
+
+ +
+
+ +
+
+

Department

+

{staff?.department}

+
+
+ +
+
+ +
+
+

Job Title

+

{staff?.job_title}

+
+
+
-

- Staff Dashboard -

+ {/* Quick Actions */} +
+

Quick Actions

-
-

Name: {auth.staff?.name}

-

Email: {auth.staff?.email}

-

Department: {auth.staff?.department}

-

Role: {auth.staff?.role}

-

Job Title: {auth.staff?.job_title}

+ {isDoctor ? ( +
+ +
+ +
+
+

Today's Queue

+

View your queued patients

+
+ + +
+ +
+
+

My Patients

+

View admitted patients

+
+ +
+ ) : ( +
+ +
+ +
+
+

Admission Requests

+

Acknowledge pending requests

+
+ + +
+ +
+
+

Bed Management

+

Manage ward allocations

+
+ +
+ )}
-
); } diff --git a/frontend/src/app/staff/dashboard/queue/page.tsx b/frontend/src/app/staff/dashboard/queue/page.tsx index 31d5b96..8841acf 100644 --- a/frontend/src/app/staff/dashboard/queue/page.tsx +++ b/frontend/src/app/staff/dashboard/queue/page.tsx @@ -10,17 +10,25 @@ import { } from '../../../../features/appointments/hooks/useStaffActions'; import { useEmergency } from '../../../../features/appointments/hooks/useEmergency'; import { useRequestAdmission } from '../../../../features/admissions/hooks/useRequestAdmission'; +import { Activity, Clock, Users, Timer, CheckCircle, AlertCircle } from 'lucide-react'; export default function QueuePage() { - const { auth } = useStaffAuth(); const doctorId = auth.staff?.id; const [showEmergency, setShowEmergency] = useState(false); const [patientId, setPatientId] = useState(''); - if (auth.isRestoring) return

Loading authentication...

; - if (!doctorId) return

Doctor information not available.

; + if (auth.isRestoring) return ( +
+
+
+ ); + if (!doctorId) return ( +
+ Doctor information not available. +
+ ); const { data, isLoading } = useDoctorQueue(doctorId); @@ -30,8 +38,16 @@ export default function QueuePage() { const emergencyMutation = useEmergency(doctorId); const admitMutation = useRequestAdmission(); - if (isLoading) return

Loading queue...

; - if (!data) return

No queue found.

; + if (isLoading) return ( +
+
+
+ ); + if (!data) return ( +
+ No queue found. +
+ ); const { queue, doctor_status } = data; const someoneInProgress = queue.some((q: any) => q.status === 'IN_PROGRESS'); @@ -39,54 +55,108 @@ export default function QueuePage() { return (
- {/* 🔴 Emergency Button */} -
-

Today's Queue

+ {/* 🔴 Header */} +
+

+
+ +
+ Today's Queue +

{/* Doctor Status Panel */} -
-

Total: {doctor_status.total_appointments}

-

Checked In: {doctor_status.checked_in_count}

-

Delay: {doctor_status.doctor_delay_minutes} min

-

Remaining: {doctor_status.remaining_queue_minutes} min

+
+ +
+
+
+

Total Appointments

+

{doctor_status.total_appointments}

+
+
+ +
+
+
+

Checked In

+

{doctor_status.checked_in_count}

+
+
+ +
+
+
+

Current Delay

+

{doctor_status.doctor_delay_minutes} min

+
+
+ +
+
+
+

Remaining Time

+

{doctor_status.remaining_queue_minutes} min

+
+
+
{/* Queue List */} -
- {queue.map((item: any) => ( +
+ {queue.length === 0 ? ( +
+ The queue is currently empty. +
+ ) : queue.map((item: any) => (
-

- Patient ID: {item.patient_id} -

- -

Status: {item.status}

-

Position: {item.position}

+
+
+ {item.patient_name?.charAt(0) || '?'} +
+
+

+ {item.patient_name} ({item.patient_id.split('-')[0]}...) +

+
+ + {item.status.replace('_', ' ')} + + + Pos: {item.position} + +
+
+
-
+
{/* Check In */} {item.status === 'SCHEDULED' && ( @@ -96,9 +166,9 @@ export default function QueuePage() { {item.status === 'CHECKED_IN' && !someoneInProgress && ( )} @@ -107,7 +177,7 @@ export default function QueuePage() { <> @@ -119,10 +189,10 @@ export default function QueuePage() { admitMutation.mutate({ patientId: item.patient_id, doctorId: doctorId, - departmentId: auth.staff?.department_id + departmentId: auth.staff?.department_id as string }) } - className="px-3 py-1 bg-red-600 text-white rounded text-sm disabled:opacity-50" + className="px-5 py-2.5 bg-red-600 hover:bg-red-700 text-white rounded-xl text-sm font-medium transition disabled:opacity-50 shadow-sm" > {admitMutation.isPending ? 'Admitting...' @@ -139,27 +209,33 @@ export default function QueuePage() { {/* 🔴 Emergency Modal */} {showEmergency && ( -
- -
- -

- Create Emergency Case -

- - setPatientId(e.target.value)} - className="w-full border rounded px-3 py-2" - /> +
+
+ +
+
+ +
+

+ Create Emergency Case +

+
-
+
+ + setPatientId(e.target.value)} + className="w-full border border-slate-300 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500 transition" + /> +
+
@@ -170,15 +246,12 @@ export default function QueuePage() { setShowEmergency(false); setPatientId(''); }} - className="px-4 py-2 bg-red-600 text-white rounded" + className="px-5 py-2.5 bg-red-600 hover:bg-red-700 text-white rounded-xl font-medium shadow-md transition" > - Confirm Emergency + Confirm Case -
-
-
)} diff --git a/frontend/src/features/admissions/components/AdmissionQueue.tsx b/frontend/src/features/admissions/components/AdmissionQueue.tsx index 2de8830..74de636 100644 --- a/frontend/src/features/admissions/components/AdmissionQueue.tsx +++ b/frontend/src/features/admissions/components/AdmissionQueue.tsx @@ -3,73 +3,85 @@ import { useState } from 'react' import { useAdmissions } from '../hooks/useAdmissions' import AssignBedModal from './AssignBedModal' +import { UserPlus, Stethoscope, Building2 } from 'lucide-react' export default function AdmissionQueue() { - const { data, isLoading } = useAdmissions() - - const [selectedAdmission,setSelectedAdmission] = useState(null) + const [selectedAdmission, setSelectedAdmission] = useState(null) if (isLoading) { - return
Loading admissions...
+ return ( +
+
+
+ ) } if (!data?.length) { return ( -
+
No pending admissions
) } return ( - <> -
- - {data.map((admission:any)=>( - +
+ {data.map((admission: any) => (
- -
- -
- Patient: {admission.patient_id} -
- -
- Doctor: {admission.doctor_id} +
+
+
+ +
+
+
+ {admission.patient_name} +
+
+ ID: {admission.patient_id.split('-')[0]}... +
+
-
- Department: {admission.department_id} +
+
+ +
+ Dr. {admission.doctor_name} + {admission.doctor_id && ({admission.doctor_id.split('-')[0]}...)} +
+
+ +
+ + {admission.department_name} +
-
- - +
+ +
- ))} -
{selectedAdmission && ( setSelectedAdmission(null)} + onClose={() => setSelectedAdmission(null)} /> )} - ) } \ No newline at end of file diff --git a/frontend/src/features/admissions/components/DischargeRequests.tsx b/frontend/src/features/admissions/components/DischargeRequests.tsx index 4977b35..9be9ca4 100644 --- a/frontend/src/features/admissions/components/DischargeRequests.tsx +++ b/frontend/src/features/admissions/components/DischargeRequests.tsx @@ -2,66 +2,78 @@ import { useDischargeRequests } from '../hooks/useDischargeRequests' import { useDischargePatient } from '../hooks/useDischargePatient' +import { LogOut, Building, BedDouble } from 'lucide-react' export default function DischargeRequests() { - const { data, isLoading } = useDischargeRequests() - const dischargeMutation = useDischargePatient() if (isLoading) { - return
Loading discharge requests...
+ return ( +
+
+
+ ) } if (!data?.length) { return ( -
+
No discharge requests
) } return ( - -
- - {data.map((req:any)=>( +
+ {data.map((req: any) => (
- -
- -
- Patient: {req.patient_id} +
+
+
+ +
+
+
+ {req.patient_name} +
+
+ ID: {req.patient_id.split('-')[0]}... +
+
-
- Ward: {req.ward} -
+
+
+ + {req.ward} +
-
- Bed: {req.bed_number} +
+ + Bed {req.bed_number} +
-
- - +
+ +
))} -
) } \ No newline at end of file diff --git a/frontend/src/features/admissions/components/DoctorPatients.tsx b/frontend/src/features/admissions/components/DoctorPatients.tsx index be0d8c4..9448a49 100644 --- a/frontend/src/features/admissions/components/DoctorPatients.tsx +++ b/frontend/src/features/admissions/components/DoctorPatients.tsx @@ -2,61 +2,75 @@ import { useDoctorAdmissions } from '../hooks/useDoctorAdmissions' import { useRequestDischarge } from '../hooks/useRequestDischarge' +import { User, Building, BedDouble } from 'lucide-react' export default function DoctorPatients() { - const { data, isLoading } = useDoctorAdmissions() - const dischargeMutation = useRequestDischarge() if (isLoading) { - return
Loading patients...
+ return ( +
+
+
+ ) } if (!data?.length) { return ( -
+
No admitted patients
) } return ( - -
- - {data.map((admission:any)=>( +
+ {data.map((admission: any) => (
- -
- -
- Patient: {admission.patient_id} +
+
+
+
+ +
+
+
+ {admission.patient_name} +
+
+ ID: {admission.patient_id.split('-')[0]}... +
+
+
-
- Ward: {admission.ward} -
+
+
+ + {admission.ward} +
-
- Bed: {admission.bed_number} +
+ + Bed {admission.bed_number} +
-
- - +
+ +
))} -
) } \ No newline at end of file diff --git a/frontend/src/features/beds/components/BedsBoard.tsx b/frontend/src/features/beds/components/BedsBoard.tsx index 355d037..290a0b3 100644 --- a/frontend/src/features/beds/components/BedsBoard.tsx +++ b/frontend/src/features/beds/components/BedsBoard.tsx @@ -1,40 +1,158 @@ 'use client' +import { useState, useMemo } from 'react' import { useBeds } from '../hooks/useBeds' +import { BedDouble, Building, Hash, Filter } from 'lucide-react' export default function BedsBoard() { const { data, isLoading } = useBeds() - if (isLoading) return

Loading beds...

+ const [selectedWard, setSelectedWard] = useState('All') + const [selectedRoom, setSelectedRoom] = useState('All') + + // Extract unique wards and rooms for the dropdowns + const { wards, rooms } = useMemo(() => { + if (!data) return { wards: [], rooms: [] } + + // Set object ensures uniqueness + const uniqueWards = Array.from(new Set(data.map((b: any) => b.ward))) as string[] + + // If a specific ward is selected, only show rooms for that ward + let filteredForRooms = data + if (selectedWard !== 'All') { + filteredForRooms = data.filter((b: any) => b.ward === selectedWard) + } + const uniqueRooms = Array.from(new Set(filteredForRooms.map((b: any) => b.room_number))) as string[] + + return { wards: uniqueWards.sort(), rooms: uniqueRooms.sort() } + }, [data, selectedWard]) + + // Handle ward change: reset room if the new ward doesn't have the selected room + const handleWardChange = (e: React.ChangeEvent) => { + const newWard = e.target.value + setSelectedWard(newWard) + setSelectedRoom('All') // Reset room selection when ward changes + } + + // Filter the actual bed data + const filteredBeds = useMemo(() => { + if (!data) return [] + return data.filter((bed: any) => { + const matchWard = selectedWard === 'All' || bed.ward === selectedWard + const matchRoom = selectedRoom === 'All' || bed.room_number === selectedRoom + return matchWard && matchRoom + }) + }, [data, selectedWard, selectedRoom]) + + if (isLoading) return ( +
+
+
+ ) + + if (!data?.length) { + return ( +
+ No beds configured yet. +
+ ) + } return ( -
- {data?.map((bed: any) => ( -
-
-

- Ward: {bed.ward} | Room: {bed.room_number} -

- -

Bed: {bed.bed_number}

+
+ {/* 🔴 Filter Controls */} +
+
+
+
+ Filter Beds +
- + + +
+ +
+ + +
+ +
+ Showing {filteredBeds.length} bed{filteredBeds.length !== 1 ? 's' : ''} +
+
+ + {/* 🛏️ Bed Grid */} + {filteredBeds.length === 0 ? ( +
+ No beds found matching the selected filters. +
+ ) : ( +
+ {filteredBeds.map((bed: any) => ( +
+
+
+
+ +
+
+

Bed {bed.bed_number}

+
+
+ + + {bed.status} + +
+ +
+
+ + Ward: {bed.ward} +
+
+ + Room: {bed.room_number} +
+
+
+ ))}
- ))} + )}
) } \ No newline at end of file diff --git a/services/patient-service/src/modules/admissions/admission.controller.ts b/services/patient-service/src/modules/admissions/admission.controller.ts index 6db22a0..2b822ce 100644 --- a/services/patient-service/src/modules/admissions/admission.controller.ts +++ b/services/patient-service/src/modules/admissions/admission.controller.ts @@ -19,7 +19,7 @@ export class AdmissionController { } async getPending(req: Request, res: Response) { - const admissions = await admissionRepository.getPendingAdmissions(); + const admissions = await admissionService.getPendingAdmissions(); res.json(admissions); } diff --git a/services/patient-service/src/modules/admissions/admission.repository.ts b/services/patient-service/src/modules/admissions/admission.repository.ts index c2e551b..ca32cc4 100644 --- a/services/patient-service/src/modules/admissions/admission.repository.ts +++ b/services/patient-service/src/modules/admissions/admission.repository.ts @@ -23,10 +23,11 @@ class AdmissionRepository { async getPendingAdmissions() { const result = await pool.query( ` - SELECT * - FROM admissions - WHERE status='REQUESTED' - ORDER BY created_at ASC + SELECT a.*, p.name AS patient_name + FROM admissions a + LEFT JOIN patients p ON p.id = a.patient_id + WHERE a.status='REQUESTED' + ORDER BY a.created_at ASC ` ); @@ -72,16 +73,18 @@ class AdmissionRepository { const result = await pool.query( ` SELECT - id, - patient_id, - doctor_id, - department_id, - status, - created_at - FROM admissions - WHERE doctor_id = $1 - AND status = 'ADMITTED' - ORDER BY created_at DESC + a.id, + a.patient_id, + p.name AS patient_name, + a.doctor_id, + a.department_id, + a.status, + a.created_at + FROM admissions a + LEFT JOIN patients p ON p.id = a.patient_id + WHERE a.doctor_id = $1 + AND a.status = 'ADMITTED' + ORDER BY a.created_at DESC `, [doctorId] ); @@ -92,13 +95,15 @@ class AdmissionRepository { const result = await pool.query( ` SELECT - id, - patient_id, - doctor_id - FROM admissions - WHERE discharge_requested = true - AND status = 'ADMITTED' - ORDER BY created_at ASC + a.id, + a.patient_id, + p.name AS patient_name, + a.doctor_id + FROM admissions a + LEFT JOIN patients p ON p.id = a.patient_id + WHERE a.discharge_requested = true + AND a.status = 'ADMITTED' + ORDER BY a.created_at ASC ` ); diff --git a/services/patient-service/src/modules/admissions/admission.service.ts b/services/patient-service/src/modules/admissions/admission.service.ts index 197efa3..7c554ad 100644 --- a/services/patient-service/src/modules/admissions/admission.service.ts +++ b/services/patient-service/src/modules/admissions/admission.service.ts @@ -1,4 +1,5 @@ import { admissionRepository } from './admission.repository'; +import axios from 'axios'; class AdmissionService { async requestAdmission(data: any) { @@ -6,7 +7,33 @@ class AdmissionService { } async getPendingAdmissions() { - return admissionRepository.getPendingAdmissions(); + const admissions = await admissionRepository.getPendingAdmissions(); + + if (!admissions || admissions.length === 0) return []; + + const doctorIds = admissions.map((a: any) => a.doctor_id); + + try { + const response = await axios.post( + `${process.env.STAFF_SERVICE_URL}/staff/bulk-basic-info`, + { + staffIds: doctorIds, + } + ); + const staffInfo = response.data || []; + + return admissions.map((admission: any) => { + const doc = staffInfo.find((s: any) => s.id === admission.doctor_id); + return { + ...admission, + doctor_name: doc?.doctor_name || 'Unknown Doctor', + department_name: doc?.department_name || 'Unknown Department', + }; + }); + } catch (err) { + console.error('Failed to fetch doctor names', err); + return admissions; + } } async admitPatient(admissionId: string) { @@ -22,10 +49,66 @@ class AdmissionService { } async getDoctorAdmissions(doctorId: string) { - return admissionRepository.getDoctorAdmissions(doctorId); + const admissions = await admissionRepository.getDoctorAdmissions(doctorId); + + if (!admissions || admissions.length === 0) return []; + + const patientIds = admissions.map((a: any) => a.patient_id); + + try { + const response = await axios.post( + `${process.env.STAFF_SERVICE_URL}/staff/beds/active-assignments`, + { + patientIds, + } + ); + const activeAssignments = response.data || []; + + return admissions.map((admission: any) => { + const assignment = activeAssignments.find( + (b: any) => b.patient_id === admission.patient_id + ); + return { + ...admission, + ward: assignment?.ward || 'Unassigned', + bed_number: assignment?.bed_number || 'Unassigned', + }; + }); + } catch (err) { + console.error('Failed to fetch bed assignments', err); + return admissions; + } } async getDischargeRequests() { - return admissionRepository.getDischargeRequests(); + const requests = await admissionRepository.getDischargeRequests(); + + if (!requests || requests.length === 0) return []; + + const patientIds = requests.map((r: any) => r.patient_id); + + try { + const response = await axios.post( + `${process.env.STAFF_SERVICE_URL}/staff/beds/active-assignments`, + { + patientIds, + } + ); + const activeAssignments = response.data || []; + + return requests.map((request: any) => { + const assignment = activeAssignments.find( + (b: any) => b.patient_id === request.patient_id + ); + return { + ...request, + ward: assignment?.ward || 'Unassigned', + bed_number: assignment?.bed_number || 'Unassigned', + }; + }); + } catch (err) { + console.error('Failed to fetch bed assignments', err); + return requests; + } } } diff --git a/services/patient-service/src/modules/appointments/appointment.repository.ts b/services/patient-service/src/modules/appointments/appointment.repository.ts index e505269..68ac3de 100644 --- a/services/patient-service/src/modules/appointments/appointment.repository.ts +++ b/services/patient-service/src/modules/appointments/appointment.repository.ts @@ -48,14 +48,15 @@ export class AppointmentRepository { async getDoctorAppointmentsForDay(doctorId: string, date: string) { const result = await pool.query( ` - SELECT * - FROM appointments - WHERE doctor_id = $1 - AND appointment_time::date = $2 - AND status IN ('SCHEDULED', 'CHECKED_IN', 'IN_PROGRESS') + SELECT a.*, p.name AS patient_name + FROM appointments a + LEFT JOIN patients p ON p.id = a.patient_id + WHERE a.doctor_id = $1 + AND a.appointment_time::date = $2 + AND a.status IN ('SCHEDULED', 'CHECKED_IN', 'IN_PROGRESS') ORDER BY - CASE WHEN priority = 'HIGH' THEN 0 ELSE 1 END, - appointment_time ASC + CASE WHEN a.priority = 'HIGH' THEN 0 ELSE 1 END, + a.appointment_time ASC `, [doctorId, date] ); diff --git a/services/patient-service/src/modules/appointments/appointment.service.ts b/services/patient-service/src/modules/appointments/appointment.service.ts index 8b6de8b..4e9bc45 100644 --- a/services/patient-service/src/modules/appointments/appointment.service.ts +++ b/services/patient-service/src/modules/appointments/appointment.service.ts @@ -341,6 +341,43 @@ export class AppointmentService { return appointment; } + async getMyStatus(patientId: string) { + const today = new Date().toLocaleDateString('en-CA'); + + const myAppointment = + await appointmentRepository.getPatientActiveAppointment(patientId, today); + + if (!myAppointment) { + return null; + } + + // get full doctor queue + const doctorQueue = await this.getDoctorQueueForDay( + myAppointment.doctor_id, + today + ); + + const myQueueItem = doctorQueue.queue.find( + (q) => q.id === myAppointment.id + ); + + if (!myQueueItem) { + return null; + } + + return { + appointment_id: myAppointment.id, + doctor_id: myAppointment.doctor_id, + status: myQueueItem.status, + position: myQueueItem.position, + patients_ahead: myQueueItem.patients_ahead, + estimated_start_time: myQueueItem.estimated_start_time, + estimated_end_time: myQueueItem.estimated_end_time, + delay_minutes: myQueueItem.delay_minutes, + doctor_delay_minutes: doctorQueue.doctor_status.doctor_delay_minutes, + doctor_current_patient: doctorQueue.doctor_status.current_patient, + }; + } } export const appointmentService = new AppointmentService(); diff --git a/services/staff-service/src/modules/beds/bed.controller.ts b/services/staff-service/src/modules/beds/bed.controller.ts index a31e3f6..0d20970 100644 --- a/services/staff-service/src/modules/beds/bed.controller.ts +++ b/services/staff-service/src/modules/beds/bed.controller.ts @@ -57,6 +57,18 @@ export class BedsController { }); } + async getActiveAssignments(req: Request, res: Response) { + const { patientIds } = req.body; + + if (!Array.isArray(patientIds) || patientIds.length === 0) { + res.json([]); + return; + } + + const assignments = await bedsService.getActiveAssignments(patientIds); + res.json(assignments); + } + async getWards(req: Request, res: Response) { const wards = await bedsService.getWards(); res.json(wards); diff --git a/services/staff-service/src/modules/beds/bed.routes.ts b/services/staff-service/src/modules/beds/bed.routes.ts index 1e01095..29b50e9 100644 --- a/services/staff-service/src/modules/beds/bed.routes.ts +++ b/services/staff-service/src/modules/beds/bed.routes.ts @@ -17,4 +17,9 @@ router.post('/assign', controller.assignBed.bind(controller)); router.post('/discharge', controller.dischargePatient.bind(controller)); +router.post( + '/active-assignments', + controller.getActiveAssignments.bind(controller) +); + export default router; diff --git a/services/staff-service/src/modules/beds/bed.service.ts b/services/staff-service/src/modules/beds/bed.service.ts index 23d302d..c30a351 100644 --- a/services/staff-service/src/modules/beds/bed.service.ts +++ b/services/staff-service/src/modules/beds/bed.service.ts @@ -132,6 +132,29 @@ class BedsService { ); } + async getActiveAssignments(patientIds: string[]) { + if (!patientIds || patientIds.length === 0) return []; + + const result = await pool.query( + ` + SELECT + ba.patient_id, + b.bed_number, + r.room_number, + w.name AS ward + FROM bed_assignments ba + JOIN beds b ON ba.bed_id = b.id + JOIN rooms r ON b.room_id = r.id + JOIN wards w ON r.ward_id = w.id + WHERE ba.patient_id = ANY($1::uuid[]) + AND ba.discharged_at IS NULL + `, + [patientIds] + ); + + return result.rows; + } + async getWards() { const result = await pool.query(` SELECT id, name diff --git a/services/staff-service/src/modules/staff/staff.controller.ts b/services/staff-service/src/modules/staff/staff.controller.ts index 884f080..ee2165a 100644 --- a/services/staff-service/src/modules/staff/staff.controller.ts +++ b/services/staff-service/src/modules/staff/staff.controller.ts @@ -38,6 +38,22 @@ export class StaffController { } } + async getBulkBasicInfo(req: Request, res: Response) { + try { + const { staffIds } = req.body; + if (!Array.isArray(staffIds) || staffIds.length === 0) { + res.json([]); + return; + } + const data = await staffService.getBulkBasicInfo(staffIds); + res.json(data); + return; + } catch (err) { + res.status(500).json({ error: 'Failed to fetch bulk staff info' }); + return; + } + } + async updateStaff(req: Request, res: Response) { try { const id = getIdParam(req); diff --git a/services/staff-service/src/modules/staff/staff.routes.ts b/services/staff-service/src/modules/staff/staff.routes.ts index 2d090fc..aa8a05e 100644 --- a/services/staff-service/src/modules/staff/staff.routes.ts +++ b/services/staff-service/src/modules/staff/staff.routes.ts @@ -19,6 +19,8 @@ router.post('/', controller.createStaff.bind(controller)); router.get('/by-id/:id', controller.getStaffById.bind(controller)); +router.post('/bulk-basic-info', controller.getBulkBasicInfo.bind(controller)); + router.put('/by-id/:id', controller.updateStaff.bind(controller)); router.delete('/by-id/:id', controller.deactivateStaff.bind(controller)); diff --git a/services/staff-service/src/modules/staff/staff.service.ts b/services/staff-service/src/modules/staff/staff.service.ts index dbb984d..819ee87 100644 --- a/services/staff-service/src/modules/staff/staff.service.ts +++ b/services/staff-service/src/modules/staff/staff.service.ts @@ -106,6 +106,23 @@ class StaffService { return result.rows[0] || null; } + async getBulkBasicInfo(staffIds: string[]) { + if (!staffIds || staffIds.length === 0) return []; + const result = await pool.query( + ` + SELECT + s.id, + s.name AS doctor_name, + d.name AS department_name + FROM staff s + LEFT JOIN departments d ON s.department_id = d.id + WHERE s.id = ANY($1::uuid[]) + `, + [staffIds] + ); + return result.rows; + } + async updateStaff(id: string, data: any) { const result = await pool.query( ` From ea6c6555d0931742c005b4a188721999454c733b Mon Sep 17 00:00:00 2001 From: linasliyakath Date: Wed, 25 Mar 2026 17:31:37 +0530 Subject: [PATCH 3/7] Inventory management --- .../app/staff/dashboard/inventory/page.tsx | 86 +++++++++++ frontend/src/app/staff/dashboard/layout.tsx | 12 +- .../src/app/staff/dashboard/pharmacy/page.tsx | 86 +++++++++++ .../src/app/staff/dashboard/queue/page.tsx | 68 ++++++--- .../admissions/components/DoctorPatients.tsx | 31 +++- .../admissions/hooks/useRequestAdmission.ts | 6 +- .../admissions/hooks/useRequestDischarge.ts | 6 +- .../features/inventory/api/inventory.api.ts | 29 ++++ .../inventory/components/AddStockModal.tsx | 125 ++++++++++++++++ .../inventory/components/UseItemModal.tsx | 83 +++++++++++ .../features/inventory/hooks/useInventory.ts | 9 ++ .../inventory/hooks/useInventoryActions.ts | 32 +++++ .../src/features/pharmacy/api/pharmacy.api.ts | 20 +++ .../pharmacy/components/PrescribeModal.tsx | 136 ++++++++++++++++++ .../features/pharmacy/hooks/usePharmacy.ts | 9 ++ .../pharmacy/hooks/usePharmacyActions.ts | 25 ++++ .../admissions/admission.repository.ts | 9 +- .../appointments/appointment.repository.ts | 9 +- services/staff-service/inventory_audit.json | 47 ++++++ services/staff-service/scripts/init-db.ts | 78 +++++++++- services/staff-service/src/app.ts | 4 + .../modules/inventory/inventory.controller.ts | 59 ++++++++ .../modules/inventory/inventory.repository.ts | 77 ++++++++++ .../src/modules/inventory/inventory.routes.ts | 13 ++ .../modules/inventory/inventory.service.ts | 68 +++++++++ .../modules/pharmacy/pharmacy.controller.ts | 40 ++++++ .../modules/pharmacy/pharmacy.repository.ts | 89 ++++++++++++ .../src/modules/pharmacy/pharmacy.routes.ts | 11 ++ .../src/modules/pharmacy/pharmacy.service.ts | 50 +++++++ 29 files changed, 1283 insertions(+), 34 deletions(-) create mode 100644 frontend/src/app/staff/dashboard/inventory/page.tsx create mode 100644 frontend/src/app/staff/dashboard/pharmacy/page.tsx create mode 100644 frontend/src/features/inventory/api/inventory.api.ts create mode 100644 frontend/src/features/inventory/components/AddStockModal.tsx create mode 100644 frontend/src/features/inventory/components/UseItemModal.tsx create mode 100644 frontend/src/features/inventory/hooks/useInventory.ts create mode 100644 frontend/src/features/inventory/hooks/useInventoryActions.ts create mode 100644 frontend/src/features/pharmacy/api/pharmacy.api.ts create mode 100644 frontend/src/features/pharmacy/components/PrescribeModal.tsx create mode 100644 frontend/src/features/pharmacy/hooks/usePharmacy.ts create mode 100644 frontend/src/features/pharmacy/hooks/usePharmacyActions.ts create mode 100644 services/staff-service/inventory_audit.json create mode 100644 services/staff-service/src/modules/inventory/inventory.controller.ts create mode 100644 services/staff-service/src/modules/inventory/inventory.repository.ts create mode 100644 services/staff-service/src/modules/inventory/inventory.routes.ts create mode 100644 services/staff-service/src/modules/inventory/inventory.service.ts create mode 100644 services/staff-service/src/modules/pharmacy/pharmacy.controller.ts create mode 100644 services/staff-service/src/modules/pharmacy/pharmacy.repository.ts create mode 100644 services/staff-service/src/modules/pharmacy/pharmacy.routes.ts create mode 100644 services/staff-service/src/modules/pharmacy/pharmacy.service.ts diff --git a/frontend/src/app/staff/dashboard/inventory/page.tsx b/frontend/src/app/staff/dashboard/inventory/page.tsx new file mode 100644 index 0000000..b4abb43 --- /dev/null +++ b/frontend/src/app/staff/dashboard/inventory/page.tsx @@ -0,0 +1,86 @@ +'use client' + +import { useState } from 'react' +import { Package, Plus, Search } from 'lucide-react' +import { useInventory } from '../../../../features/inventory/hooks/useInventory' +import { AddStockModal } from '../../../../features/inventory/components/AddStockModal' + +export default function InventoryPage() { + const { data: items, isLoading } = useInventory(); + const [showAddModal, setShowAddModal] = useState(false); + const [search, setSearch] = useState(''); + + if (isLoading) return ( +
+
+
+ ); + + const filteredItems = items?.filter((item: any) => + item.name.toLowerCase().includes(search.toLowerCase()) || + item.category.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+ {/* 🔴 Header */} +
+

+
+ +
+ Inventory Management +

+ + +
+ + {/* Search Bar */} +
+ + setSearch(e.target.value)} + /> +
+ + {/* Grid */} + {filteredItems?.length === 0 ? ( +
+ No inventory items found. Add stock to begin. +
+ ) : ( +
+ {filteredItems?.map((item: any) => ( +
+
+ + {item.category} + +

{item.name}

+
+
+
+

Stock

+

+ {item.quantity} +

+
+
+
+ ))} +
+ )} + + {/* Modal */} + {showAddModal && setShowAddModal(false)} items={items || []} />} +
+ ) +} diff --git a/frontend/src/app/staff/dashboard/layout.tsx b/frontend/src/app/staff/dashboard/layout.tsx index 1588122..220a8fe 100644 --- a/frontend/src/app/staff/dashboard/layout.tsx +++ b/frontend/src/app/staff/dashboard/layout.tsx @@ -14,7 +14,9 @@ import { Settings, Bed, ClipboardPlus, - FileMinus + FileMinus, + Package, + Pill } from 'lucide-react'; import { Tooltip, @@ -56,11 +58,15 @@ export default function StaffLayout({ { label: "Today's Queue", icon: Activity, path: '/staff/dashboard/queue' }, { label: 'Manage Availability', icon: Calendar, path: '/staff/dashboard/availability' }, { label: 'Patients', icon: User, path: '/staff/dashboard/patients' }, + { label: 'Pharmacy', icon: Pill, path: '/staff/dashboard/pharmacy' }, + { label: 'Inventory', icon: Package, path: '/staff/dashboard/inventory' }, ] : [ { label: 'Dashboard', icon: LayoutDashboard, path: '/staff/dashboard' }, { label: 'Beds', icon: Bed, path: '/staff/dashboard/beds' }, { label: 'Admissions', icon: ClipboardPlus, path: '/staff/dashboard/admissions' }, { label: 'Discharge', icon: FileMinus, path: '/staff/dashboard/discharge' }, + { label: 'Pharmacy', icon: Pill, path: '/staff/dashboard/pharmacy' }, + { label: 'Inventory', icon: Package, path: '/staff/dashboard/inventory' }, ]; return ( @@ -90,8 +96,8 @@ export default function StaffLayout({ diff --git a/frontend/src/app/staff/dashboard/pharmacy/page.tsx b/frontend/src/app/staff/dashboard/pharmacy/page.tsx new file mode 100644 index 0000000..35f15aa --- /dev/null +++ b/frontend/src/app/staff/dashboard/pharmacy/page.tsx @@ -0,0 +1,86 @@ +'use client' + +import { Pill, CheckCircle } from 'lucide-react' +import { usePendingPrescriptions } from '../../../../features/pharmacy/hooks/usePharmacy' +import { useDispensePrescription } from '../../../../features/pharmacy/hooks/usePharmacyActions' + +export default function PharmacyPage() { + const { data: prescriptions, isLoading } = usePendingPrescriptions(); + const dispenseMutation = useDispensePrescription(); + + if (isLoading) return ( +
+
+
+ ); + + return ( +
+
+

+
+ +
+ Pharmacy & Dispensing +

+
+ + {prescriptions?.length === 0 ? ( +
+ No pending prescriptions right now. You're all caught up! +
+ ) : ( +
+ {prescriptions?.map((prescription: any) => { + const canDispense = prescription.items.every((item: any) => item.stock_available >= item.quantity); + + return ( +
+
+
+
+

{prescription.patient_name}

+

Dr. {prescription.doctor_name}

+
+ + {prescription.status} + +
+ +
+

Prescribed Medicines

+ {prescription.items.map((item: any) => ( +
+
+

{item.item_name}

+ {item.instructions &&

{item.instructions}

} +

+ Stock: {item.stock_available} +

+
+
+ x{item.quantity} +
+
+ ))} +
+
+ +
+ +
+
+ ); + })} +
+ )} +
+ ) +} diff --git a/frontend/src/app/staff/dashboard/queue/page.tsx b/frontend/src/app/staff/dashboard/queue/page.tsx index 8841acf..294d7c1 100644 --- a/frontend/src/app/staff/dashboard/queue/page.tsx +++ b/frontend/src/app/staff/dashboard/queue/page.tsx @@ -11,6 +11,7 @@ import { import { useEmergency } from '../../../../features/appointments/hooks/useEmergency'; import { useRequestAdmission } from '../../../../features/admissions/hooks/useRequestAdmission'; import { Activity, Clock, Users, Timer, CheckCircle, AlertCircle } from 'lucide-react'; +import { PrescribeModal } from '../../../../features/pharmacy/components/PrescribeModal'; export default function QueuePage() { const { auth } = useStaffAuth(); @@ -18,6 +19,7 @@ export default function QueuePage() { const [showEmergency, setShowEmergency] = useState(false); const [patientId, setPatientId] = useState(''); + const [prescribePatient, setPrescribePatient] = useState<{ id: string, name: string } | null>(null); if (auth.isRestoring) return (
@@ -120,10 +122,10 @@ export default function QueuePage() {
@@ -137,9 +139,9 @@ export default function QueuePage() {

{item.status.replace('_', ' ')} @@ -175,6 +177,13 @@ export default function QueuePage() { {/* Complete Consultation */} {item.status === 'IN_PROGRESS' && ( <> + + {/* 🏥 Admit Patient */} - + {item.admission_requested ? ( + + Request Sent + + ) : ( + + )} )} @@ -255,6 +270,15 @@ export default function QueuePage() {
)} + {/* 🔴 Prescribe Modal */} + {prescribePatient && ( + setPrescribePatient(null)} + /> + )} +
); } \ No newline at end of file diff --git a/frontend/src/features/admissions/components/DoctorPatients.tsx b/frontend/src/features/admissions/components/DoctorPatients.tsx index 9448a49..83be06b 100644 --- a/frontend/src/features/admissions/components/DoctorPatients.tsx +++ b/frontend/src/features/admissions/components/DoctorPatients.tsx @@ -2,11 +2,14 @@ import { useDoctorAdmissions } from '../hooks/useDoctorAdmissions' import { useRequestDischarge } from '../hooks/useRequestDischarge' -import { User, Building, BedDouble } from 'lucide-react' +import { User, Building, BedDouble, Plus } from 'lucide-react' +import { UseItemModal } from '../../inventory/components/UseItemModal' +import { useState } from 'react' export default function DoctorPatients() { const { data, isLoading } = useDoctorAdmissions() const dischargeMutation = useRequestDischarge() + const [useItemPatientId, setUseItemPatientId] = useState(null) if (isLoading) { return ( @@ -63,14 +66,34 @@ export default function DoctorPatients() {
+ {admission.discharge_requested ? ( + + Request Sent + + ) : ( + + )}
))} + + {useItemPatientId && ( + setUseItemPatientId(null)} + patientId={useItemPatientId} + /> + )}
) } \ No newline at end of file diff --git a/frontend/src/features/admissions/hooks/useRequestAdmission.ts b/frontend/src/features/admissions/hooks/useRequestAdmission.ts index f20b763..e0b20dd 100644 --- a/frontend/src/features/admissions/hooks/useRequestAdmission.ts +++ b/frontend/src/features/admissions/hooks/useRequestAdmission.ts @@ -1,8 +1,12 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { requestAdmission } from '../api/requestAdmission.api'; export function useRequestAdmission() { + const queryClient = useQueryClient(); return useMutation({ mutationFn: requestAdmission, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['doctorQueue'] }); + }, }); } diff --git a/frontend/src/features/admissions/hooks/useRequestDischarge.ts b/frontend/src/features/admissions/hooks/useRequestDischarge.ts index 82ee569..e05b63a 100644 --- a/frontend/src/features/admissions/hooks/useRequestDischarge.ts +++ b/frontend/src/features/admissions/hooks/useRequestDischarge.ts @@ -1,8 +1,12 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { requestDischarge } from '../api/requestDischarge.api'; export const useRequestDischarge = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: requestDischarge, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['doctorAdmissions'] }); + }, }); }; diff --git a/frontend/src/features/inventory/api/inventory.api.ts b/frontend/src/features/inventory/api/inventory.api.ts new file mode 100644 index 0000000..3529d02 --- /dev/null +++ b/frontend/src/features/inventory/api/inventory.api.ts @@ -0,0 +1,29 @@ +import { api } from '../../../lib/api'; + +export const getInventoryItems = async () => { + const res = await api.get('/staff/inventory/items'); + return res.data; +}; + +export const addStock = async (data: { itemId: string; quantity: number }) => { + const res = await api.post('/staff/inventory/stock', data); + return res.data; +}; + +export const useItem = async (data: { + itemId: string; + quantity: number; + patientId?: string; +}) => { + const res = await api.post('/staff/inventory/use', data); + return res.data; +}; + +export const createItem = async (data: { + name: string; + category: string; + quantity: number; +}) => { + const res = await api.post('/staff/inventory/items', data); + return res.data; +}; diff --git a/frontend/src/features/inventory/components/AddStockModal.tsx b/frontend/src/features/inventory/components/AddStockModal.tsx new file mode 100644 index 0000000..d0a8188 --- /dev/null +++ b/frontend/src/features/inventory/components/AddStockModal.tsx @@ -0,0 +1,125 @@ +import { useState } from 'react'; +import { useAddStock, useCreateItem } from '../hooks/useInventoryActions'; +import { Package, X } from 'lucide-react'; + +export function AddStockModal({ onClose, items }: { onClose: () => void, items: any[] }) { + const [tab, setTab] = useState<'EXISTING' | 'NEW'>('EXISTING'); + + const [selectedItemId, setSelectedItemId] = useState(items?.[0]?.id || ''); + const [quantity, setQuantity] = useState(1); + const [name, setName] = useState(''); + const [category, setCategory] = useState('MEDICINE'); + + const addStockMutation = useAddStock(); + const createItemMutation = useCreateItem(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (tab === 'EXISTING') { + addStockMutation.mutate({ itemId: selectedItemId || items[0].id, quantity }, { + onSuccess: () => onClose() + }); + } else { + createItemMutation.mutate({ name, category, quantity }, { + onSuccess: () => onClose() + }); + } + }; + + return ( +
+
+
+
+
+

Add Stock

+
+ +
+ +
+ + +
+ +
+ {tab === 'EXISTING' ? ( +
+ + +
+ ) : ( + <> +
+ + setName(e.target.value)} + placeholder="e.g. Paracetamol 500mg" + className="w-full border border-slate-300 rounded-xl px-4 py-3 bg-slate-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition" + required + /> +
+
+ + +
+ + )} + +
+ + setQuantity(parseInt(e.target.value) || 1)} + className="w-full border border-slate-300 rounded-xl px-4 py-3 bg-slate-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition" + required + /> +
+ + +
+
+
+ ); +} diff --git a/frontend/src/features/inventory/components/UseItemModal.tsx b/frontend/src/features/inventory/components/UseItemModal.tsx new file mode 100644 index 0000000..8478e38 --- /dev/null +++ b/frontend/src/features/inventory/components/UseItemModal.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import { useConsumeItem } from '../hooks/useInventoryActions'; +import { useInventory } from '../hooks/useInventory'; +import { PackageMinus, X } from 'lucide-react'; + +export function UseItemModal({ onClose, patientId, preselectedCategory }: { onClose: () => void, patientId?: string, preselectedCategory?: string }) { + const { data: items } = useInventory(); + + const availableItems = items?.filter((i: any) => i.quantity > 0 && (!preselectedCategory || i.category === preselectedCategory)) || []; + + const [selectedItemId, setSelectedItemId] = useState(availableItems[0]?.id || ''); + const [quantity, setQuantity] = useState(1); + const consumeMutation = useConsumeItem(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const resolvedId = selectedItemId || availableItems[0]?.id; + if (!resolvedId) return; + + consumeMutation.mutate({ itemId: resolvedId, quantity, patientId }, { + onSuccess: () => onClose() + }); + }; + + return ( +
+
+
+
+
+

Use Inventory

+
+ +
+ + {availableItems.length === 0 ? ( +
+ No matching items available in stock. +
+ ) : ( +
+
+ + +
+ +
+ + i.id === (selectedItemId || availableItems[0]?.id))?.quantity || 1} + value={quantity} + onChange={e => setQuantity(parseInt(e.target.value) || 1)} + className="w-full border border-slate-300 rounded-xl px-4 py-3 bg-slate-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-rose-500 transition" + required + /> +
+ + +
+ )} +
+
+ ); +} diff --git a/frontend/src/features/inventory/hooks/useInventory.ts b/frontend/src/features/inventory/hooks/useInventory.ts new file mode 100644 index 0000000..8086971 --- /dev/null +++ b/frontend/src/features/inventory/hooks/useInventory.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; +import { getInventoryItems } from '../api/inventory.api'; + +export function useInventory() { + return useQuery({ + queryKey: ['inventory'], + queryFn: getInventoryItems, + }); +} diff --git a/frontend/src/features/inventory/hooks/useInventoryActions.ts b/frontend/src/features/inventory/hooks/useInventoryActions.ts new file mode 100644 index 0000000..9b68691 --- /dev/null +++ b/frontend/src/features/inventory/hooks/useInventoryActions.ts @@ -0,0 +1,32 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { addStock, useItem, createItem } from '../api/inventory.api'; + +export function useAddStock() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: addStock, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['inventory'] }); + }, + }); +} + +export function useConsumeItem() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: useItem, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['inventory'] }); + }, + }); +} + +export function useCreateItem() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createItem, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['inventory'] }); + }, + }); +} diff --git a/frontend/src/features/pharmacy/api/pharmacy.api.ts b/frontend/src/features/pharmacy/api/pharmacy.api.ts new file mode 100644 index 0000000..1aeff81 --- /dev/null +++ b/frontend/src/features/pharmacy/api/pharmacy.api.ts @@ -0,0 +1,20 @@ +import { api } from '../../../lib/api'; + +export const createPrescription = async (data: { + patientId: string; + patientName: string; + items: { itemId: string; quantity: number; instructions?: string }[]; +}) => { + const res = await api.post('/staff/pharmacy', data); + return res.data; +}; + +export const getPendingPrescriptions = async () => { + const res = await api.get('/staff/pharmacy/pending'); + return res.data; +}; + +export const dispensePrescription = async (prescriptionId: string) => { + const res = await api.post(`/staff/pharmacy/${prescriptionId}/dispense`); + return res.data; +}; diff --git a/frontend/src/features/pharmacy/components/PrescribeModal.tsx b/frontend/src/features/pharmacy/components/PrescribeModal.tsx new file mode 100644 index 0000000..8738f51 --- /dev/null +++ b/frontend/src/features/pharmacy/components/PrescribeModal.tsx @@ -0,0 +1,136 @@ +import { useState, useEffect } from 'react'; +import { useCreatePrescription } from '../hooks/usePharmacyActions'; +import { useInventory } from '../../inventory/hooks/useInventory'; +import { Pill, X, Plus, Trash2 } from 'lucide-react'; + +export function PrescribeModal({ onClose, patientId, patientName }: { onClose: () => void, patientId: string, patientName: string }) { + const { data: inventory } = useInventory(); + const medicines = inventory?.filter((i: any) => i.category === 'MEDICINE') || []; + + const [items, setItems] = useState<{ itemId: string; quantity: number | string; instructions: string }[]>([]); + const prescribeMutation = useCreatePrescription(); + + // Auto-populate first row if medicines exist + useEffect(() => { + if (medicines.length > 0 && items.length === 0) { + setItems([{ itemId: medicines[0].id, quantity: 1, instructions: '' }]); + } + }, [medicines, items.length]); + + const handleAddItem = () => { + if (medicines.length === 0) return; + setItems([...items, { itemId: medicines[0].id, quantity: 1, instructions: '' }]); + }; + + const handleUpdateItem = (index: number, field: string, value: any) => { + const newItems = [...items]; + newItems[index] = { ...newItems[index], [field]: value }; + setItems(newItems); + }; + + const handleRemoveItem = (index: number) => { + setItems(items.filter((_, i) => i !== index)); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (items.length === 0) return; + + const payloadItems = items.map(item => ({ + itemId: item.itemId, + quantity: typeof item.quantity === 'string' ? parseInt(item.quantity) || 1 : item.quantity, + instructions: item.instructions + })); + + prescribeMutation.mutate({ patientId, patientName, items: payloadItems }, { + onSuccess: () => onClose() + }); + }; + + return ( +
+
+
+
+
+

Write Prescription

+
+ +
+ +
+
+

Patient Name

+

{patientName}

+
+
+ +
+ {items.map((item, index) => ( +
+
+
+ +
+
+ handleUpdateItem(index, 'quantity', e.target.value)} + className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-center font-bold focus:outline-none focus:ring-2 focus:ring-indigo-500" + required + placeholder="Qty" + /> +
+ +
+
+ handleUpdateItem(index, 'instructions', e.target.value)} + placeholder="Doctor instructions (e.g. 1-0-1 for 5 days after food)" + className="w-full bg-slate-50 border border-slate-200 rounded-lg px-4 py-3 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500 transition" + /> +
+
+ ))} + + {medicines.length > 0 ? ( + + ) : ( +
No unassigned medicines available in the hospital inventory.
+ )} + + +
+
+
+ ); +} diff --git a/frontend/src/features/pharmacy/hooks/usePharmacy.ts b/frontend/src/features/pharmacy/hooks/usePharmacy.ts new file mode 100644 index 0000000..eca4e43 --- /dev/null +++ b/frontend/src/features/pharmacy/hooks/usePharmacy.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; +import { getPendingPrescriptions } from '../api/pharmacy.api'; + +export function usePendingPrescriptions() { + return useQuery({ + queryKey: ['pendingPrescriptions'], + queryFn: getPendingPrescriptions, + }); +} diff --git a/frontend/src/features/pharmacy/hooks/usePharmacyActions.ts b/frontend/src/features/pharmacy/hooks/usePharmacyActions.ts new file mode 100644 index 0000000..64f68d0 --- /dev/null +++ b/frontend/src/features/pharmacy/hooks/usePharmacyActions.ts @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { createPrescription, dispensePrescription } from '../api/pharmacy.api'; + +export function useCreatePrescription() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createPrescription, + onSuccess: () => { + // We don't necessarily need to invalidate here since Doctors don't see the Pharmacy list, but good practice + queryClient.invalidateQueries({ queryKey: ['pendingPrescriptions'] }); + }, + }); +} + +export function useDispensePrescription() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: dispensePrescription, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['pendingPrescriptions'] }); + // Dispensing uses inventory stock, so we invalidate inventory too + queryClient.invalidateQueries({ queryKey: ['inventory'] }); + }, + }); +} diff --git a/services/patient-service/src/modules/admissions/admission.repository.ts b/services/patient-service/src/modules/admissions/admission.repository.ts index ca32cc4..8895dbb 100644 --- a/services/patient-service/src/modules/admissions/admission.repository.ts +++ b/services/patient-service/src/modules/admissions/admission.repository.ts @@ -7,6 +7,12 @@ class AdmissionRepository { doctorId: string; departmentId: string; }) { + const existing = await pool.query( + `SELECT * FROM admissions WHERE patient_id = $1 AND status != 'DISCHARGED'`, + [data.patientId] + ); + if (existing.rows.length > 0) return existing.rows[0]; + const result = await pool.query( ` INSERT INTO admissions @@ -79,7 +85,8 @@ class AdmissionRepository { a.doctor_id, a.department_id, a.status, - a.created_at + a.created_at, + a.discharge_requested FROM admissions a LEFT JOIN patients p ON p.id = a.patient_id WHERE a.doctor_id = $1 diff --git a/services/patient-service/src/modules/appointments/appointment.repository.ts b/services/patient-service/src/modules/appointments/appointment.repository.ts index 68ac3de..7b2b722 100644 --- a/services/patient-service/src/modules/appointments/appointment.repository.ts +++ b/services/patient-service/src/modules/appointments/appointment.repository.ts @@ -48,7 +48,14 @@ export class AppointmentRepository { async getDoctorAppointmentsForDay(doctorId: string, date: string) { const result = await pool.query( ` - SELECT a.*, p.name AS patient_name + SELECT + a.*, + p.name AS patient_name, + EXISTS ( + SELECT 1 FROM admissions adm + WHERE adm.patient_id = a.patient_id + AND adm.status != 'DISCHARGED' + ) AS admission_requested FROM appointments a LEFT JOIN patients p ON p.id = a.patient_id WHERE a.doctor_id = $1 diff --git a/services/staff-service/inventory_audit.json b/services/staff-service/inventory_audit.json new file mode 100644 index 0000000..b27077c --- /dev/null +++ b/services/staff-service/inventory_audit.json @@ -0,0 +1,47 @@ +[ + { + "name": "Dolo 250mg", + "category": "CONSUMABLE", + "quantity": 10 + }, + { + "name": "Paracetamol 500mg", + "category": "CONSUMABLE", + "quantity": 8 + }, + { + "name": "Ibuprofen 400mg", + "category": "MEDICINE", + "quantity": 300 + }, + { + "name": "Pantoprazole 40mg", + "category": "MEDICINE", + "quantity": 150 + }, + { + "name": "Syringes 5ml", + "category": "CONSUMABLE", + "quantity": 1000 + }, + { + "name": "Medical Gauze", + "category": "CONSUMABLE", + "quantity": 2000 + }, + { + "name": "Cetirizine 10mg", + "category": "MEDICINE", + "quantity": 99 + }, + { + "name": "Amoxicillin 250mg", + "category": "MEDICINE", + "quantity": 200 + }, + { + "name": "new", + "category": "CONSUMABLE", + "quantity": 300 + } +] diff --git a/services/staff-service/scripts/init-db.ts b/services/staff-service/scripts/init-db.ts index 479c442..50e4103 100644 --- a/services/staff-service/scripts/init-db.ts +++ b/services/staff-service/scripts/init-db.ts @@ -133,8 +133,84 @@ async function init() { WHERE discharged_at IS NULL; `); + /** + * 9️⃣ Inventory Tables + */ + await pool.query(` + CREATE TABLE IF NOT EXISTS inventory_items ( + id UUID PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + category TEXT NOT NULL, + quantity INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT now() + ); + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS inventory_transactions ( + id UUID PRIMARY KEY, + item_id UUID REFERENCES inventory_items(id) ON DELETE CASCADE, + type TEXT NOT NULL, + quantity INT NOT NULL, + staff_id UUID REFERENCES staff(id) ON DELETE SET NULL, + patient_id UUID, + timestamp TIMESTAMP DEFAULT now() + ); + `); + + /** + * 10️⃣ Pharmacy Tables + */ + await pool.query(` + CREATE TABLE IF NOT EXISTS prescriptions ( + id UUID PRIMARY KEY, + patient_id UUID NOT NULL, + patient_name TEXT, + doctor_id UUID REFERENCES staff(id) ON DELETE CASCADE, + status TEXT DEFAULT 'PENDING', + created_at TIMESTAMP DEFAULT now() + ); + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS prescription_items ( + id UUID PRIMARY KEY, + prescription_id UUID REFERENCES prescriptions(id) ON DELETE CASCADE, + item_id UUID REFERENCES inventory_items(id) ON DELETE CASCADE, + quantity INT NOT NULL, + instructions TEXT + ); + `); + + // Ensure instructions column exists if table was created previously + await pool.query(` + ALTER TABLE prescription_items ADD COLUMN IF NOT EXISTS instructions TEXT; + `); + + /** + * Seed default medicines + */ + const defaultMedicines = [ + { name: 'Paracetamol 500mg', category: 'MEDICINE', quantity: 500 }, + { name: 'Amoxicillin 250mg', category: 'MEDICINE', quantity: 200 }, + { name: 'Ibuprofen 400mg', category: 'MEDICINE', quantity: 300 }, + { name: 'Cetirizine 10mg', category: 'MEDICINE', quantity: 100 }, + { name: 'Pantoprazole 40mg', category: 'MEDICINE', quantity: 150 }, + { name: 'Syringes 5ml', category: 'CONSUMABLE', quantity: 1000 }, + { name: 'Medical Gauze', category: 'CONSUMABLE', quantity: 2000 }, + ]; + + for (const med of defaultMedicines) { + await pool.query( + `INSERT INTO inventory_items (id, name, category, quantity) + VALUES ($1, $2, $3, $4) + ON CONFLICT (name) DO NOTHING`, + [randomUUID(), med.name, med.category, med.quantity] + ); + } + console.log( - '✅ departments + staff + doctor_availability + bed management tables ready' + '✅ departments + staff + doctor_availability + bed management + inventory + pharmacy tables + seeds ready' ); process.exit(0); diff --git a/services/staff-service/src/app.ts b/services/staff-service/src/app.ts index 977a9a5..396bc7c 100644 --- a/services/staff-service/src/app.ts +++ b/services/staff-service/src/app.ts @@ -5,6 +5,8 @@ import authRouter from './modules/auth/auth.routes'; import staffRouter from './modules/staff/staff.routes'; import bedRoutes from './modules/beds/bed.routes'; import adminBedRoutes from './modules/beds/admin.bed.routes'; +import inventoryRouter from './modules/inventory/inventory.routes'; +import pharmacyRouter from './modules/pharmacy/pharmacy.routes'; // import departmentRoutes from './modules/staff/department.routes' const app = express(); @@ -16,6 +18,8 @@ app.use('/auth', authRouter); app.use('/staff/beds', bedRoutes); app.use('/admin/beds', adminBedRoutes); +app.use('/staff/inventory', inventoryRouter); +app.use('/staff/pharmacy', pharmacyRouter); app.use('/staff', staffRouter); // app.use('/departments',departmentRoutes); diff --git a/services/staff-service/src/modules/inventory/inventory.controller.ts b/services/staff-service/src/modules/inventory/inventory.controller.ts new file mode 100644 index 0000000..3441359 --- /dev/null +++ b/services/staff-service/src/modules/inventory/inventory.controller.ts @@ -0,0 +1,59 @@ +import { Request, Response } from 'express'; +import { inventoryService } from './inventory.service'; + +export class InventoryController { + async getItems(req: Request, res: Response) { + try { + const items = await inventoryService.getItems(); + res.json(items); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } + } + + async createItem(req: Request, res: Response) { + try { + const staffId = req.headers['x-user-id'] as string; + const item = await inventoryService.createItem(req.body, staffId); + res.json(item); + } catch (err: any) { + res.status(400).json({ error: err.message }); + } + } + + async addStock(req: Request, res: Response) { + try { + const staffId = req.headers['x-user-id'] as string; + const { itemId, quantity } = req.body; + const item = await inventoryService.addStock(itemId, quantity, staffId); + res.json(item); + } catch (err: any) { + res.status(400).json({ error: err.message }); + } + } + + async useItem(req: Request, res: Response) { + try { + const staffId = req.headers['x-user-id'] as string; + const { itemId, quantity, patientId } = req.body; + const item = await inventoryService.useItem( + itemId, + quantity, + staffId, + patientId + ); + res.json(item); + } catch (err: any) { + res.status(400).json({ error: err.message }); + } + } + + async getTransactions(req: Request, res: Response) { + try { + const transactions = await inventoryService.getTransactions(); + res.json(transactions); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } + } +} diff --git a/services/staff-service/src/modules/inventory/inventory.repository.ts b/services/staff-service/src/modules/inventory/inventory.repository.ts new file mode 100644 index 0000000..4da6794 --- /dev/null +++ b/services/staff-service/src/modules/inventory/inventory.repository.ts @@ -0,0 +1,77 @@ +import { pool } from '../../db'; +import { randomUUID } from 'crypto'; + +class InventoryRepository { + async getItems() { + const result = await pool.query( + `SELECT * FROM inventory_items ORDER BY name ASC` + ); + return result.rows; + } + + async createItem(data: { name: string; category: string; quantity: number }) { + const result = await pool.query( + ` + INSERT INTO inventory_items (id, name, category, quantity) + VALUES ($1, $2, $3, $4) + RETURNING * + `, + [randomUUID(), data.name, data.category, data.quantity] + ); + return result.rows[0]; + } + + async adjustQuantity(itemId: string, amount: number) { + const result = await pool.query( + ` + UPDATE inventory_items + SET quantity = quantity + $2 + WHERE id = $1 + RETURNING * + `, + [itemId, amount] + ); + return result.rows[0]; + } + + async logTransaction(data: { + itemId: string; + type: 'IN' | 'OUT'; + quantity: number; + staffId: string; + patientId?: string; + }) { + const result = await pool.query( + ` + INSERT INTO inventory_transactions (id, item_id, type, quantity, staff_id, patient_id) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING * + `, + [ + randomUUID(), + data.itemId, + data.type, + data.quantity, + data.staffId, + data.patientId || null, + ] + ); + return result.rows[0]; + } + + async getTransactions() { + const result = await pool.query( + ` + SELECT t.*, i.name as item_name, s.name as staff_name + FROM inventory_transactions t + JOIN inventory_items i ON t.item_id = i.id + LEFT JOIN staff s ON t.staff_id = s.id + ORDER BY t.timestamp DESC + LIMIT 100 + ` + ); + return result.rows; + } +} + +export const inventoryRepository = new InventoryRepository(); diff --git a/services/staff-service/src/modules/inventory/inventory.routes.ts b/services/staff-service/src/modules/inventory/inventory.routes.ts new file mode 100644 index 0000000..aea0402 --- /dev/null +++ b/services/staff-service/src/modules/inventory/inventory.routes.ts @@ -0,0 +1,13 @@ +import { Router } from 'express'; +import { InventoryController } from './inventory.controller'; + +const router = Router(); +const controller = new InventoryController(); + +router.get('/items', controller.getItems.bind(controller)); +router.post('/items', controller.createItem.bind(controller)); +router.post('/stock', controller.addStock.bind(controller)); +router.post('/use', controller.useItem.bind(controller)); +router.get('/transactions', controller.getTransactions.bind(controller)); + +export default router; diff --git a/services/staff-service/src/modules/inventory/inventory.service.ts b/services/staff-service/src/modules/inventory/inventory.service.ts new file mode 100644 index 0000000..5479abd --- /dev/null +++ b/services/staff-service/src/modules/inventory/inventory.service.ts @@ -0,0 +1,68 @@ +import { inventoryRepository } from './inventory.repository'; + +class InventoryService { + async getItems() { + return inventoryRepository.getItems(); + } + + async createItem( + data: { name: string; category: string; quantity: number }, + staffId: string + ) { + const item = await inventoryRepository.createItem(data); + if (data.quantity > 0) { + await inventoryRepository.logTransaction({ + itemId: item.id, + type: 'IN', + quantity: data.quantity, + staffId, + }); + } + return item; + } + + async addStock(itemId: string, quantity: number, staffId: string) { + if (quantity <= 0) throw new Error('Quantity must be greater than zero'); + + const updated = await inventoryRepository.adjustQuantity(itemId, quantity); + await inventoryRepository.logTransaction({ + itemId, + type: 'IN', + quantity, + staffId, + }); + + return updated; + } + + async useItem( + itemId: string, + quantity: number, + staffId: string, + patientId?: string + ) { + if (quantity <= 0) throw new Error('Quantity must be greater than zero'); + + const items = await inventoryRepository.getItems(); + const item = items.find((i: any) => i.id === itemId); + if (!item) throw new Error('Item not found'); + if (item.quantity < quantity) throw new Error('Insufficient stock'); + + const updated = await inventoryRepository.adjustQuantity(itemId, -quantity); + await inventoryRepository.logTransaction({ + itemId, + type: 'OUT', + quantity, + staffId, + patientId, + }); + + return updated; + } + + async getTransactions() { + return inventoryRepository.getTransactions(); + } +} + +export const inventoryService = new InventoryService(); diff --git a/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts b/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts new file mode 100644 index 0000000..c85b64d --- /dev/null +++ b/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts @@ -0,0 +1,40 @@ +import { Request, Response } from 'express'; +import { pharmacyService } from './pharmacy.service'; + +export class PharmacyController { + async createPrescription(req: Request, res: Response) { + try { + const doctorId = req.headers['x-user-id'] as string; + const { patientId, patientName, items } = req.body; + const result = await pharmacyService.createPrescription({ + patientId, + patientName, + doctorId, + items, + }); + res.json(result); + } catch (e: any) { + res.status(400).json({ error: e.message }); + } + } + + async getPending(req: Request, res: Response) { + try { + const result = await pharmacyService.getPendingPrescriptions(); + res.json(result); + } catch (e: any) { + res.status(500).json({ error: e.message }); + } + } + + async dispense(req: Request, res: Response) { + try { + const staffId = req.headers['x-user-id'] as string; + const { id } = req.params; + const result = await pharmacyService.dispense(id, staffId); + res.json(result); + } catch (e: any) { + res.status(400).json({ error: e.message }); + } + } +} diff --git a/services/staff-service/src/modules/pharmacy/pharmacy.repository.ts b/services/staff-service/src/modules/pharmacy/pharmacy.repository.ts new file mode 100644 index 0000000..e36d375 --- /dev/null +++ b/services/staff-service/src/modules/pharmacy/pharmacy.repository.ts @@ -0,0 +1,89 @@ +import { pool } from '../../db'; +import { randomUUID } from 'crypto'; + +class PharmacyRepository { + async createPrescription(data: { + patientId: string; + patientName: string; + doctorId: string; + items: { itemId: string; quantity: number; instructions?: string }[]; + }) { + const client = await pool.connect(); + try { + await client.query('BEGIN'); + const prescriptionId = randomUUID(); + + const pRes = await client.query( + `INSERT INTO prescriptions (id, patient_id, patient_name, doctor_id, status) + VALUES ($1, $2, $3, $4, 'PENDING') RETURNING *`, + [prescriptionId, data.patientId, data.patientName, data.doctorId] + ); + + for (const item of data.items) { + await client.query( + `INSERT INTO prescription_items (id, prescription_id, item_id, quantity, instructions) + VALUES ($1, $2, $3, $4, $5)`, + [ + randomUUID(), + prescriptionId, + item.itemId, + item.quantity, + item.instructions || '', + ] + ); + } + + await client.query('COMMIT'); + return pRes.rows[0]; + } catch (e) { + await client.query('ROLLBACK'); + throw e; + } finally { + client.release(); + } + } + + async getPendingPrescriptions() { + const result = await pool.query( + ` + SELECT + p.*, + s.name as doctor_name, + json_agg(json_build_object( + 'id', pi.id, + 'item_id', pi.item_id, + 'quantity', pi.quantity, + 'instructions', pi.instructions, + 'item_name', i.name, + 'category', i.category, + 'stock_available', COALESCE(i.quantity, 0) + )) as items + FROM prescriptions p + LEFT JOIN staff s ON p.doctor_id = s.id + LEFT JOIN prescription_items pi ON p.id = pi.prescription_id + LEFT JOIN inventory_items i ON pi.item_id = i.id + WHERE p.status = 'PENDING' + GROUP BY p.id, s.name + ORDER BY p.created_at ASC + ` + ); + return result.rows; + } + + async markDispensed(prescriptionId: string) { + const result = await pool.query( + `UPDATE prescriptions SET status = 'DISPENSED' WHERE id = $1 RETURNING *`, + [prescriptionId] + ); + return result.rows[0]; + } + + async getPrescriptionItems(prescriptionId: string) { + const result = await pool.query( + `SELECT * FROM prescription_items WHERE prescription_id = $1`, + [prescriptionId] + ); + return result.rows; + } +} +export const pharmacyRepository = new PharmacyRepository(); diff --git a/services/staff-service/src/modules/pharmacy/pharmacy.routes.ts b/services/staff-service/src/modules/pharmacy/pharmacy.routes.ts new file mode 100644 index 0000000..2670c8e --- /dev/null +++ b/services/staff-service/src/modules/pharmacy/pharmacy.routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { PharmacyController } from './pharmacy.controller'; + +const router = Router(); +const controller = new PharmacyController(); + +router.post('/', controller.createPrescription.bind(controller)); +router.get('/pending', controller.getPending.bind(controller)); +router.post('/:id/dispense', controller.dispense.bind(controller)); + +export default router; diff --git a/services/staff-service/src/modules/pharmacy/pharmacy.service.ts b/services/staff-service/src/modules/pharmacy/pharmacy.service.ts new file mode 100644 index 0000000..d4ef0e4 --- /dev/null +++ b/services/staff-service/src/modules/pharmacy/pharmacy.service.ts @@ -0,0 +1,50 @@ +import { pharmacyRepository } from './pharmacy.repository'; +import { inventoryService } from '../inventory/inventory.service'; + +class PharmacyService { + async createPrescription(data: { + patientId: string; + patientName: string; + doctorId: string; + items: { itemId: string; quantity: number; instructions?: string }[]; + }) { + if (!data.items || data.items.length === 0) + throw new Error('Prescription must have at least one item'); + return pharmacyRepository.createPrescription(data); + } + + async getPendingPrescriptions() { + const prescriptions = await pharmacyRepository.getPendingPrescriptions(); + // Clean up any NULL entries from the aggregations if LEFT JOIN had no matches + return prescriptions.map((p: any) => ({ + ...p, + items: p.items.filter((item: any) => item.id !== null), + })); + } + + async dispense(prescriptionId: string, staffId: string) { + // 1. Validate the items + const items = await pharmacyRepository.getPrescriptionItems(prescriptionId); + if (!items.length) throw new Error('No items found on this prescription'); + + // We need patient mapping info, let's grab it directly + const pendingList = await pharmacyRepository.getPendingPrescriptions(); + const prescription = pendingList.find((p: any) => p.id === prescriptionId); + if (!prescription) + throw new Error('Prescription not found or already dispensed'); + + // 2. Iterate and useItem (this seamlessly checks constraints & reduces stock) + for (const item of items) { + await inventoryService.useItem( + item.item_id, + item.quantity, + staffId, + prescription.patient_id + ); + } + + // 3. Complete fulfillment + return pharmacyRepository.markDispensed(prescriptionId); + } +} +export const pharmacyService = new PharmacyService(); From aa16ecd5ab2f4dd9eebc9724c349fb8b987c1f6e Mon Sep 17 00:00:00 2001 From: linasliyakath Date: Wed, 25 Mar 2026 20:15:32 +0530 Subject: [PATCH 4/7] Ci fix --- .../src/modules/pharmacy/pharmacy.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts b/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts index c85b64d..a5faf06 100644 --- a/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts +++ b/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts @@ -4,7 +4,7 @@ import { pharmacyService } from './pharmacy.service'; export class PharmacyController { async createPrescription(req: Request, res: Response) { try { - const doctorId = req.headers['x-user-id'] as string; + const doctorId = String(req.headers['x-user-id']); const { patientId, patientName, items } = req.body; const result = await pharmacyService.createPrescription({ patientId, @@ -29,8 +29,8 @@ export class PharmacyController { async dispense(req: Request, res: Response) { try { - const staffId = req.headers['x-user-id'] as string; - const { id } = req.params; + const staffId = String(req.headers['x-user-id']); + const id = String(req.params.id); const result = await pharmacyService.dispense(id, staffId); res.json(result); } catch (e: any) { From 1b63b4929ba7d300ace48750f25dac7fec1ea376 Mon Sep 17 00:00:00 2001 From: linasliyakath Date: Wed, 25 Mar 2026 20:19:28 +0530 Subject: [PATCH 5/7] fix --- .../appointments/appointment.repository.ts | 28 +++++ .../appointments/appointment.service.ts | 115 ++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/services/patient-service/src/modules/appointments/appointment.repository.ts b/services/patient-service/src/modules/appointments/appointment.repository.ts index 7b2b722..427f894 100644 --- a/services/patient-service/src/modules/appointments/appointment.repository.ts +++ b/services/patient-service/src/modules/appointments/appointment.repository.ts @@ -154,6 +154,34 @@ export class AppointmentRepository { return `${hours}:${minutes}`; }); } + + async updateAppointmentTime( + appointmentId: string, + newTime: Date, + newDuration: number + ) { + const result = await pool.query( + ` + UPDATE appointments + SET appointment_time = $2, + duration_minutes = $3, + updated_at = now() + WHERE id = $1 + RETURNING *; + `, + [appointmentId, newTime, newDuration] + ); + + return result.rows[0]; + } + + async getAppointmentById(id: string) { + const result = await pool.query( + `SELECT * FROM appointments WHERE id = $1`, + [id] + ); + return result.rows[0] || null; + } } export const appointmentRepository = new AppointmentRepository(); diff --git a/services/patient-service/src/modules/appointments/appointment.service.ts b/services/patient-service/src/modules/appointments/appointment.service.ts index 4e9bc45..5b46104 100644 --- a/services/patient-service/src/modules/appointments/appointment.service.ts +++ b/services/patient-service/src/modules/appointments/appointment.service.ts @@ -378,6 +378,121 @@ export class AppointmentService { doctor_current_patient: doctorQueue.doctor_status.current_patient, }; } + + async setPriority(appointmentId: string, priority: 'NORMAL' | 'HIGH') { + const appointment = + await appointmentRepository.getAppointmentById(appointmentId); + + if (!appointment) throw new Error('NOT_FOUND'); + + return appointmentRepository.updatePriority(appointmentId, priority); + } + + async cancelAppointment(appointmentId: string, patientId: string) { + const appointment = + await appointmentRepository.getAppointmentById(appointmentId); + + if (!appointment) throw new Error('NOT_FOUND'); + + if (appointment.patient_id !== patientId) throw new Error('FORBIDDEN'); + + if (appointment.status !== 'SCHEDULED') throw new Error('CANNOT_CANCEL'); + + // Check if it's too late (e.g. less than 1 hour before) + const now = new Date(); + const apptTime = new Date(appointment.appointment_time); + const diffHours = (apptTime.getTime() - now.getTime()) / 3600000; + + if (diffHours < 1) throw new Error('TOO_LATE_TO_CANCEL'); + + return this.updateStatus(appointmentId, 'CANCELLED'); + } + + async rescheduleAppointment( + appointmentId: string, + patientId: string, + newTimeStr: string + ) { + const appointment = + await appointmentRepository.getAppointmentById(appointmentId); + + if (!appointment) throw new Error('NOT_FOUND'); + + if (appointment.patient_id !== patientId) throw new Error('FORBIDDEN'); + + if (appointment.status !== 'SCHEDULED') + throw new Error('CANNOT_RESCHEDULE'); + + const now = new Date(); + const newTime = new Date(newTimeStr); + + if (newTime <= now) { + throw new Error('PAST_TIME_NOT_ALLOWED'); + } + + // Check if too late to change current one + const apptTime = new Date(appointment.appointment_time); + const diffHours = (apptTime.getTime() - now.getTime()) / 3600000; + + if (diffHours < 1) throw new Error('TOO_LATE_TO_RESCHEDULE'); + + // Validate new slot + const dayOfWeek = newTime.getDay(); + const availabilityResponse = await axios.get( + `${process.env.STAFF_SERVICE_URL}/staff/availability/${appointment.doctor_id}/${dayOfWeek}` + ); + + const availability = availabilityResponse.data; + if (!availability) throw new Error('DOCTOR_NOT_AVAILABLE'); + + const { start_time, end_time, slot_duration } = availability; + + const start = new Date(newTime); + start.setHours( + Number(start_time.split(':')[0]), + Number(start_time.split(':')[1]), + 0, + 0 + ); + + const end = new Date(newTime); + end.setHours( + Number(end_time.split(':')[0]), + Number(end_time.split(':')[1]), + 0, + 0 + ); + + if (newTime < start || newTime >= end) { + throw new Error('OUTSIDE_WORKING_HOURS'); + } + + const diffMinutes = (newTime.getTime() - start.getTime()) / 60000; + if (diffMinutes % slot_duration !== 0) { + throw new Error('INVALID_SLOT_TIME'); + } + + // Check if slot taken + const dateStr = newTime.toISOString().split('T')[0]; + const booked = await appointmentRepository.getBookedSlots( + appointment.doctor_id, + dateStr + ); + + const hours = newTime.getHours().toString().padStart(2, '0'); + const minutes = newTime.getMinutes().toString().padStart(2, '0'); + const timeStr = `${hours}:${minutes}`; + + if (booked.includes(timeStr)) { + throw new Error('SLOT_TAKEN'); + } + + return appointmentRepository.updateAppointmentTime( + appointmentId, + newTime, + slot_duration + ); + } } export const appointmentService = new AppointmentService(); From b1582eed77a4b227c17803c544e44d2d40788395 Mon Sep 17 00:00:00 2001 From: linasliyakath Date: Thu, 26 Mar 2026 19:49:25 +0530 Subject: [PATCH 6/7] Inventory-Management --- api-gateway/src/app.ts | 29 +++- .../src/app/(patient)/dashboard/layout.tsx | 21 ++- frontend/src/app/(patient)/dashboard/page.tsx | 8 +- .../app/staff/dashboard/inventory/page.tsx | 10 ++ frontend/src/app/staff/dashboard/layout.tsx | 2 - .../src/app/staff/dashboard/pharmacy/page.tsx | 19 ++- .../components/AppointmentCard.tsx | 161 ++++++++++++++---- .../features/beds/components/BedsBoard.tsx | 25 ++- .../patient/api/getCurrentAdmission.ts | 6 + .../features/patient/api/getPrescriptions.ts | 6 + .../patient/components/AdmissionStatus.tsx | 117 +++++++++++++ services/patient-service/scripts/init-db.ts | 3 + .../admissions/admission.controller.ts | 27 +++ .../admissions/admission.repository.ts | 34 ++++ .../modules/admissions/admission.routes.ts | 2 + .../modules/admissions/admission.service.ts | 51 ++++++ .../appointments/appointment.service.ts | 2 + .../modules/patients/patient.controller.ts | 15 +- .../src/modules/patients/patient.routes.ts | 5 + .../src/modules/patients/patient.service.ts | 9 + .../src/modules/beds/bed.service.ts | 65 ++++++- .../modules/pharmacy/pharmacy.controller.ts | 16 ++ .../modules/pharmacy/pharmacy.repository.ts | 38 ++++- .../src/modules/pharmacy/pharmacy.routes.ts | 1 + .../src/modules/pharmacy/pharmacy.service.ts | 9 + 25 files changed, 615 insertions(+), 66 deletions(-) create mode 100644 frontend/src/features/patient/api/getCurrentAdmission.ts create mode 100644 frontend/src/features/patient/api/getPrescriptions.ts create mode 100644 frontend/src/features/patient/components/AdmissionStatus.tsx diff --git a/api-gateway/src/app.ts b/api-gateway/src/app.ts index 29363fa..c22a196 100644 --- a/api-gateway/src/app.ts +++ b/api-gateway/src/app.ts @@ -41,9 +41,18 @@ app.use( // STAFF auth app.use('/staff/public', staffAuthProxy); -app.use('/staff', authenticate, staffDataProxy); +const injectStaffHeaders = (req: any, _res: any, next: any) => { + if (req.user) { + if (req.user.sub) req.headers['x-user-id'] = String(req.user.sub); + if (req.user.role) req.headers['x-user-role'] = String(req.user.role); + if (req.user.type) req.headers['x-user-type'] = String(req.user.type); + } + next(); +}; -app.use('/admin', authenticate, staffDataProxy); +app.use('/staff', authenticate, injectStaffHeaders, staffDataProxy); + +app.use('/admin', authenticate, injectStaffHeaders, staffDataProxy); app.use( '/admissions', @@ -86,4 +95,20 @@ app.use( }) ); +app.use( + '/prescriptions', + authenticate, + (req: any, _res, next) => { + if (req.user?.sub) { + req.headers['x-user-id'] = String(req.user.sub); + } + next(); + }, + createProxyMiddleware({ + target: process.env.STAFF_SERVICE_URL, + changeOrigin: true, + pathRewrite: (path) => `/staff/pharmacy${path}`, + }) +); + export default app; diff --git a/frontend/src/app/(patient)/dashboard/layout.tsx b/frontend/src/app/(patient)/dashboard/layout.tsx index 13f2679..16ab33c 100644 --- a/frontend/src/app/(patient)/dashboard/layout.tsx +++ b/frontend/src/app/(patient)/dashboard/layout.tsx @@ -29,8 +29,8 @@ const navItems = [ { label: 'Dashboard', icon: LayoutDashboard, path: '/dashboard' }, { label: 'Appointments', icon: Calendar, path: '/dashboard/appointments' }, { label: 'Book Appointment', icon: PlusCircle, path: '/dashboard/book' }, - { label: 'Profile', icon: User, path: '/dashboard/profile' }, { label: 'Documents', icon: FileText, path: '/dashboard/documents' }, + { label: 'Profile', icon: User, path: '/dashboard/profile' }, ]; export default function PatientDashboardLayout({ @@ -48,11 +48,11 @@ export default function PatientDashboardLayout({ } }, [auth.isRestoring, auth.accessToken, router]); - if (auth.isRestoring) return
-
-
-
-
; + if (auth.isRestoring) return
+
+
+
+
; if (!auth.accessToken) return null; // Safely extract initials @@ -85,11 +85,10 @@ export default function PatientDashboardLayout({ diff --git a/frontend/src/app/(patient)/dashboard/page.tsx b/frontend/src/app/(patient)/dashboard/page.tsx index 708e969..9362739 100644 --- a/frontend/src/app/(patient)/dashboard/page.tsx +++ b/frontend/src/app/(patient)/dashboard/page.tsx @@ -11,6 +11,7 @@ import { getPatientDocuments } from '@/src/features/patient/api/getDocument'; import { getProfile } from '@/src/features/patient/api/getProfile'; import { Calendar, FileText, CalendarPlus, Upload, CloudCog } from 'lucide-react'; +import AdmissionStatus from '@/src/features/patient/components/AdmissionStatus'; export default function DashboardHome() { @@ -32,8 +33,8 @@ export default function DashboardHome() { const docs = await getPatientDocuments(); const profileData = await getProfile(); - console.log('profiledata',profileData); - + console.log('profiledata', profileData); + setProfile(profileData); setPatientName(profileData.name); @@ -105,6 +106,9 @@ export default function DashboardHome() {
+ {/* Admission Status */} + + {/* Quick Actions */}
diff --git a/frontend/src/app/staff/dashboard/inventory/page.tsx b/frontend/src/app/staff/dashboard/inventory/page.tsx index b4abb43..7a82348 100644 --- a/frontend/src/app/staff/dashboard/inventory/page.tsx +++ b/frontend/src/app/staff/dashboard/inventory/page.tsx @@ -4,8 +4,18 @@ import { useState } from 'react' import { Package, Plus, Search } from 'lucide-react' import { useInventory } from '../../../../features/inventory/hooks/useInventory' import { AddStockModal } from '../../../../features/inventory/components/AddStockModal' +import { useStaffAuth } from '../../../../staff/auth/staff.auth.provider' +import { useRouter } from 'next/navigation' export default function InventoryPage() { + const { auth } = useStaffAuth(); + const router = useRouter(); + + if (auth.staff?.role === 'DOCTOR') { + router.replace('/staff/dashboard'); + return null; + } + const { data: items, isLoading } = useInventory(); const [showAddModal, setShowAddModal] = useState(false); const [search, setSearch] = useState(''); diff --git a/frontend/src/app/staff/dashboard/layout.tsx b/frontend/src/app/staff/dashboard/layout.tsx index 220a8fe..5ad4edf 100644 --- a/frontend/src/app/staff/dashboard/layout.tsx +++ b/frontend/src/app/staff/dashboard/layout.tsx @@ -58,8 +58,6 @@ export default function StaffLayout({ { label: "Today's Queue", icon: Activity, path: '/staff/dashboard/queue' }, { label: 'Manage Availability', icon: Calendar, path: '/staff/dashboard/availability' }, { label: 'Patients', icon: User, path: '/staff/dashboard/patients' }, - { label: 'Pharmacy', icon: Pill, path: '/staff/dashboard/pharmacy' }, - { label: 'Inventory', icon: Package, path: '/staff/dashboard/inventory' }, ] : [ { label: 'Dashboard', icon: LayoutDashboard, path: '/staff/dashboard' }, { label: 'Beds', icon: Bed, path: '/staff/dashboard/beds' }, diff --git a/frontend/src/app/staff/dashboard/pharmacy/page.tsx b/frontend/src/app/staff/dashboard/pharmacy/page.tsx index 35f15aa..3055a8e 100644 --- a/frontend/src/app/staff/dashboard/pharmacy/page.tsx +++ b/frontend/src/app/staff/dashboard/pharmacy/page.tsx @@ -2,9 +2,19 @@ import { Pill, CheckCircle } from 'lucide-react' import { usePendingPrescriptions } from '../../../../features/pharmacy/hooks/usePharmacy' +import { useStaffAuth } from '../../../../staff/auth/staff.auth.provider' import { useDispensePrescription } from '../../../../features/pharmacy/hooks/usePharmacyActions' +import { useRouter } from 'next/navigation' export default function PharmacyPage() { + const { auth } = useStaffAuth(); + const router = useRouter(); + + if (auth.staff?.role === 'DOCTOR') { + router.replace('/staff/dashboard'); + return null; + } + const { data: prescriptions, isLoading } = usePendingPrescriptions(); const dispenseMutation = useDispensePrescription(); @@ -38,9 +48,12 @@ export default function PharmacyPage() {
-
-

{prescription.patient_name}

-

Dr. {prescription.doctor_name}

+
+

Patient

+

{prescription.patient_name || 'Anonymous Patient'}

+

+ Prescribed by: Dr. {prescription.doctor_name} +

{prescription.status} diff --git a/frontend/src/features/appointments/components/AppointmentCard.tsx b/frontend/src/features/appointments/components/AppointmentCard.tsx index 8474153..29056c4 100644 --- a/frontend/src/features/appointments/components/AppointmentCard.tsx +++ b/frontend/src/features/appointments/components/AppointmentCard.tsx @@ -1,9 +1,12 @@ 'use client'; import { useState } from 'react'; -import { ChevronDown, ChevronUp } from 'lucide-react'; +import { ChevronDown, ChevronUp, MapPin, Bed, User, Clock, Pill } from 'lucide-react'; import { useCancelAppointment } from '../hooks/useCancelAppointment'; import RescheduleModal from './RescheduleModal'; +import { getCurrentAdmission } from '../../patient/api/getCurrentAdmission'; +import { getPrescriptions } from '../../patient/api/getPrescriptions'; +import { useEffect } from 'react'; interface Props { appointment: any; @@ -24,6 +27,32 @@ export default function AppointmentCard({ appointment }: Props) { const [expanded, setExpanded] = useState(false); const [showReschedule, setShowReschedule] = useState(false); + const [admission, setAdmission] = useState(null); + const [prescriptions, setPrescriptions] = useState([]); + const [isLoadingExtra, setIsLoadingExtra] = useState(false); + + useEffect(() => { + if (expanded && !admission && prescriptions.length === 0) { + const loadData = async () => { + setIsLoadingExtra(true); + try { + const [adm, pres] = await Promise.all([ + getCurrentAdmission(), + getPrescriptions() + ]); + setAdmission(adm); + // Show all prescriptions for now, or filter by doctor if needed + setPrescriptions(pres || []); + } catch (err) { + console.error("Failed to load extra appointment data", err); + } finally { + setIsLoadingExtra(false); + } + }; + loadData(); + } + }, [expanded]); + const appointmentDate = new Date(appointment.appointment_time); const now = new Date(); @@ -81,47 +110,107 @@ export default function AppointmentCard({ appointment }: Props) {
{/* Status */} -
- - {appointment.status.replace("_", " ")} - -
+
+ + {appointment.status.replace("_", " ")} + +
{/* Expanded section */} {expanded && ( -
- - {appointment.status === 'SCHEDULED' && !isPast && ( - +
+ + {isLoadingExtra ? ( +
+
+
+ ) : ( + <> + {/* Admission Status */} +
+

+ + Admission Status +

+ {admission ? ( +
+
+
+

Ward

+

{admission.ward}

+
+
+

Bed

+

{admission.bed_number}

+
+
+
+ ) : ( +

Not currently admitted

+ )} +
+ + {/* Prescriptions */} +
+

+ + Prescribed Medicines +

+ {prescriptions.length > 0 ? ( +
+ {prescriptions.map((pres: any) => ( +
+

Prescription from Dr. {pres.doctor_name}

+
    + {pres.items.map((item: any) => ( +
  • + {item.item_name} × {item.quantity} + {item.instructions} +
  • + ))} +
+
+ ))} +
+ ) : ( +

No medicines prescribed recently

+ )} +
+ )} - {canReschedule && ( - - )} - - {appointment.status === 'CANCELLED' && ( - - Appointment Cancelled - - )} +
+ {appointment.status === 'SCHEDULED' && !isPast && ( + + )} + + {canReschedule && ( + + )} + + {appointment.status === 'CANCELLED' && ( + + Appointment Cancelled + + )} +
diff --git a/frontend/src/features/beds/components/BedsBoard.tsx b/frontend/src/features/beds/components/BedsBoard.tsx index 290a0b3..91477f2 100644 --- a/frontend/src/features/beds/components/BedsBoard.tsx +++ b/frontend/src/features/beds/components/BedsBoard.tsx @@ -2,7 +2,7 @@ import { useState, useMemo } from 'react' import { useBeds } from '../hooks/useBeds' -import { BedDouble, Building, Hash, Filter } from 'lucide-react' +import { BedDouble, Building, Hash, Filter, User } from 'lucide-react' export default function BedsBoard() { const { data, isLoading } = useBeds() @@ -118,7 +118,7 @@ export default function BedsBoard() {
@@ -129,10 +129,10 @@ export default function BedsBoard() { {bed.status} @@ -149,6 +149,19 @@ export default function BedsBoard() { Room: {bed.room_number}
+ + {bed.status === 'OCCUPIED' && ( +
+
+ + Occupied By: {bed.patient_name} +
+
+ + Assigned By: Dr. {bed.doctor_name} +
+
+ )}
))}
diff --git a/frontend/src/features/patient/api/getCurrentAdmission.ts b/frontend/src/features/patient/api/getCurrentAdmission.ts new file mode 100644 index 0000000..573fd94 --- /dev/null +++ b/frontend/src/features/patient/api/getCurrentAdmission.ts @@ -0,0 +1,6 @@ +import { api } from '@/src/lib/api'; + +export async function getCurrentAdmission() { + const res = await api.get('/admissions/current'); + return res.data; +} diff --git a/frontend/src/features/patient/api/getPrescriptions.ts b/frontend/src/features/patient/api/getPrescriptions.ts new file mode 100644 index 0000000..ec20a67 --- /dev/null +++ b/frontend/src/features/patient/api/getPrescriptions.ts @@ -0,0 +1,6 @@ +import { api } from '@/src/lib/api'; + +export async function getPrescriptions() { + const res = await api.get('/prescriptions/me'); + return res.data; +} diff --git a/frontend/src/features/patient/components/AdmissionStatus.tsx b/frontend/src/features/patient/components/AdmissionStatus.tsx new file mode 100644 index 0000000..60df217 --- /dev/null +++ b/frontend/src/features/patient/components/AdmissionStatus.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { getCurrentAdmission } from '../api/getCurrentAdmission'; +import { Bed, User, Clock, MapPin, AlertCircle } from 'lucide-react'; + +export default function AdmissionStatus() { + const [admission, setAdmission] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchStatus() { + try { + const data = await getCurrentAdmission(); + setAdmission(data); + } catch (err) { + console.error('Failed to fetch admission status', err); + } finally { + setLoading(false); + } + } + fetchStatus(); + }, []); + + if (loading) { + return ( +
+
+
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+
+ ); + } + + if (!admission) { + return ( +
+
+ +
+
+

No Active Admission

+

You are currently not admitted to any ward.

+
+
+ ); + } + + const admissionDate = new Date(admission.admitted_at || admission.created_at); + const formattedDate = admissionDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + const formattedTime = admissionDate.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + }); + + return ( +
+
+

+ Current Admission Status + + {admission.status} + +

+
+ +
+
+
+ +
+
+

Ward / Department

+

{admission.ward || 'General'}

+
+
+ +
+
+ +
+
+

Bed Number

+

{admission.bed_number || 'TBD'}

+
+
+ +
+
+ +
+
+

Admitting Doctor

+

Dr. {admission.doctor_name || 'Assigned Doctor'}

+
+
+ +
+
+ +
+
+

Admitted On

+

{formattedDate} at {formattedTime}

+
+
+
+
+ ); +} diff --git a/services/patient-service/scripts/init-db.ts b/services/patient-service/scripts/init-db.ts index 711f558..bc61120 100644 --- a/services/patient-service/scripts/init-db.ts +++ b/services/patient-service/scripts/init-db.ts @@ -31,6 +31,9 @@ async function init() { status TEXT NOT NULL DEFAULT 'REQUESTED', + discharge_requested BOOLEAN DEFAULT false, + discharge_requested_at TIMESTAMP, + created_at TIMESTAMP DEFAULT now(), admitted_at TIMESTAMP, discharged_at TIMESTAMP diff --git a/services/patient-service/src/modules/admissions/admission.controller.ts b/services/patient-service/src/modules/admissions/admission.controller.ts index 2b822ce..b1a3082 100644 --- a/services/patient-service/src/modules/admissions/admission.controller.ts +++ b/services/patient-service/src/modules/admissions/admission.controller.ts @@ -71,4 +71,31 @@ export class AdmissionController { res.json(requests); } + + async getCurrentAdmission(req: Request, res: Response) { + const patientId = req.headers['x-user-id'] as string; + + if (!patientId) { + return res.status(401).json({ + error: 'Unauthorized', + }); + } + + const admission = await admissionService.getCurrentAdmission(patientId); + + res.json(admission); + } + + async getBulkCurrent(req: Request, res: Response) { + try { + const { patientIds } = req.body; + if (!Array.isArray(patientIds)) { + return res.status(400).json({ error: 'patientIds array required' }); + } + const data = await admissionService.getBulkCurrent(patientIds); + return res.json(data); + } catch (e: any) { + return res.status(500).json({ error: e.message }); + } + } } diff --git a/services/patient-service/src/modules/admissions/admission.repository.ts b/services/patient-service/src/modules/admissions/admission.repository.ts index 8895dbb..4af1959 100644 --- a/services/patient-service/src/modules/admissions/admission.repository.ts +++ b/services/patient-service/src/modules/admissions/admission.repository.ts @@ -116,6 +116,40 @@ class AdmissionRepository { return result.rows; } + + async getCurrentAdmission(patientId: string) { + const result = await pool.query( + ` + SELECT * FROM admissions + WHERE patient_id = $1 + AND status != 'DISCHARGED' + LIMIT 1 + `, + [patientId] + ); + + return result.rows[0] || null; + } + + async getAdmissionsByIds(ids: string[]) { + if (!ids || ids.length === 0) return []; + const result = await pool.query( + `SELECT id, patient_id, doctor_id FROM admissions WHERE id = ANY($1::uuid[])`, + [ids] + ); + return result.rows; + } + + async getBulkCurrent(patientIds: string[]) { + if (!patientIds || patientIds.length === 0) return []; + const result = await pool.query( + `SELECT id as admission_id, patient_id, doctor_id + FROM admissions + WHERE patient_id = ANY($1::uuid[]) AND status != 'DISCHARGED'`, + [patientIds] + ); + return result.rows; + } } export const admissionRepository = new AdmissionRepository(); diff --git a/services/patient-service/src/modules/admissions/admission.routes.ts b/services/patient-service/src/modules/admissions/admission.routes.ts index 28fed7d..1a90908 100644 --- a/services/patient-service/src/modules/admissions/admission.routes.ts +++ b/services/patient-service/src/modules/admissions/admission.routes.ts @@ -19,4 +19,6 @@ router.post( controller.requestDischarge.bind(controller) ); router.post('/:id/discharged', controller.completeDischarge.bind(controller)); +router.get('/current', controller.getCurrentAdmission.bind(controller)); +router.post('/bulk-current', controller.getBulkCurrent.bind(controller)); export default router; diff --git a/services/patient-service/src/modules/admissions/admission.service.ts b/services/patient-service/src/modules/admissions/admission.service.ts index 7c554ad..b0b9d22 100644 --- a/services/patient-service/src/modules/admissions/admission.service.ts +++ b/services/patient-service/src/modules/admissions/admission.service.ts @@ -110,6 +110,57 @@ class AdmissionService { return requests; } } + + async getCurrentAdmission(patientId: string) { + const admission = await admissionRepository.getCurrentAdmission(patientId); + + if (!admission) return null; + + // Fetch doctor info + let doctor_name = 'Unknown Doctor'; + try { + const response = await axios.post( + `${process.env.STAFF_SERVICE_URL}/staff/bulk-basic-info`, + { + staffIds: [admission.doctor_id], + } + ); + const staffInfo = response.data || []; + doctor_name = staffInfo[0]?.doctor_name || 'Unknown Doctor'; + } catch (err) { + console.error('Failed to fetch doctor info', err); + } + + // Fetch bed assignment + let assignmentInfo = { ward: 'N/A', bed_number: 'N/A' }; + try { + const response = await axios.post( + `${process.env.STAFF_SERVICE_URL}/staff/beds/active-assignments`, + { + patientIds: [patientId], + } + ); + const activeAssignments = response.data || []; + if (activeAssignments.length > 0) { + assignmentInfo = { + ward: activeAssignments[0].ward, + bed_number: activeAssignments[0].bed_number, + }; + } + } catch (err) { + console.error('Failed to fetch bed assignment', err); + } + + return { + ...admission, + doctor_name, + ...assignmentInfo, + }; + } + + async getBulkCurrent(patientIds: string[]) { + return admissionRepository.getBulkCurrent(patientIds); + } } export const admissionService = new AdmissionService(); diff --git a/services/patient-service/src/modules/appointments/appointment.service.ts b/services/patient-service/src/modules/appointments/appointment.service.ts index 5b46104..9ba4575 100644 --- a/services/patient-service/src/modules/appointments/appointment.service.ts +++ b/services/patient-service/src/modules/appointments/appointment.service.ts @@ -186,6 +186,8 @@ export class AppointmentService { return { id: appt.id, patient_id: appt.patient_id, + patient_name: appt.patient_name, + admission_requested: appt.admission_requested, status: appt.status, priority: appt.priority, planned_time: appt.appointment_time, diff --git a/services/patient-service/src/modules/patients/patient.controller.ts b/services/patient-service/src/modules/patients/patient.controller.ts index a7e774e..6147f1b 100644 --- a/services/patient-service/src/modules/patients/patient.controller.ts +++ b/services/patient-service/src/modules/patients/patient.controller.ts @@ -163,9 +163,22 @@ class PatientController { const { file_key } = req.body; await deletePatientDocument(patientId, file_key); - return res.json({ success: true }); } + + async getBulkInfo(req: Request, res: Response) { + try { + const { ids } = req.body; + if (!Array.isArray(ids)) { + return res.status(400).json({ error: 'ids array required' }); + } + const patients = await patientService.getPatientsByIds(ids); + return res.json(patients); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Failed' }); + } + } } export const patientController = new PatientController(); diff --git a/services/patient-service/src/modules/patients/patient.routes.ts b/services/patient-service/src/modules/patients/patient.routes.ts index 351fa88..df7ae01 100644 --- a/services/patient-service/src/modules/patients/patient.routes.ts +++ b/services/patient-service/src/modules/patients/patient.routes.ts @@ -34,4 +34,9 @@ router.delete('/:id', (req, res) => patientController.deactivatePatient(req, res) ); +router.post( + '/bulk-info', + patientController.getBulkInfo.bind(patientController) +); + export default router; diff --git a/services/patient-service/src/modules/patients/patient.service.ts b/services/patient-service/src/modules/patients/patient.service.ts index b12df86..9d462fd 100644 --- a/services/patient-service/src/modules/patients/patient.service.ts +++ b/services/patient-service/src/modules/patients/patient.service.ts @@ -265,6 +265,15 @@ class PatientService { return result.rows[0] || null; } + async getPatientsByIds(ids: string[]) { + if (!ids || ids.length === 0) return []; + const result = await pool.query( + `SELECT id, name FROM patients WHERE id = ANY($1::uuid[])`, + [ids] + ); + return result.rows; + } + /** * UPDATE PATIENT PROFILE */ diff --git a/services/staff-service/src/modules/beds/bed.service.ts b/services/staff-service/src/modules/beds/bed.service.ts index c30a351..7137563 100644 --- a/services/staff-service/src/modules/beds/bed.service.ts +++ b/services/staff-service/src/modules/beds/bed.service.ts @@ -50,14 +50,75 @@ class BedsService { b.bed_number, b.status, r.room_number, - w.name AS ward + w.name AS ward, + ba.patient_id FROM beds b JOIN rooms r ON b.room_id = r.id JOIN wards w ON r.ward_id = w.id + LEFT JOIN bed_assignments ba ON b.id = ba.bed_id AND ba.discharged_at IS NULL ` ); - return result.rows; + const beds = result.rows; + const activePatientIds = beds + .filter((b) => b.status === 'OCCUPIED' && b.patient_id) + .map((b) => b.patient_id); + + if (activePatientIds.length === 0) return beds; + + try { + // 1. Fetch names and current admissions from patient-service + const [patientsRes, admissionsRes] = await Promise.all([ + axios.post(`${process.env.PATIENT_SERVICE_URL}/patients/bulk-info`, { + ids: activePatientIds, + }), + axios.post( + `${process.env.PATIENT_SERVICE_URL}/admissions/bulk-current`, + { + patientIds: activePatientIds, + } + ), + ]); + + const patientMap = new Map( + patientsRes.data.map((p: any) => [p.id, p.name]) + ); + const admissionMap = new Map( + admissionsRes.data.map((a: any) => [a.patient_id, a.doctor_id]) + ); + + const doctorIds = Array.from( + new Set(admissionsRes.data.map((a: any) => a.doctor_id)) as Set + ); + + // 2. Fetch doctor names from local staff table + let doctorMap = new Map(); + if (doctorIds.length > 0) { + const staffRes = await pool.query( + `SELECT id, name FROM staff WHERE id = ANY($1::uuid[])`, + [doctorIds] + ); + doctorMap = new Map(staffRes.rows.map((s: any) => [s.id, s.name])); + } + + // 3. Map back to beds + return beds.map((bed) => { + if (bed.status === 'OCCUPIED' && bed.patient_id) { + const doctorId = admissionMap.get(bed.patient_id); + return { + ...bed, + patient_name: patientMap.get(bed.patient_id) || 'Unknown Patient', + doctor_name: doctorId + ? doctorMap.get(doctorId) || 'Unknown Doctor' + : 'Unknown Doctor', + }; + } + return bed; + }); + } catch (e) { + console.error('Enrichment failed:', e); + return beds; + } } async assignBed(bedId: string, patientId: string, admissionId: string) { diff --git a/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts b/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts index a5faf06..dc24378 100644 --- a/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts +++ b/services/staff-service/src/modules/pharmacy/pharmacy.controller.ts @@ -5,6 +5,9 @@ export class PharmacyController { async createPrescription(req: Request, res: Response) { try { const doctorId = String(req.headers['x-user-id']); + if (!doctorId || doctorId === 'undefined') { + return res.status(401).json({ error: 'User identity missing' }); + } const { patientId, patientName, items } = req.body; const result = await pharmacyService.createPrescription({ patientId, @@ -37,4 +40,17 @@ export class PharmacyController { res.status(400).json({ error: e.message }); } } + + async getMyPrescriptions(req: Request, res: Response) { + try { + const patientId = String(req.headers['x-user-id']); + if (!patientId || patientId === 'undefined') { + return res.status(401).json({ error: 'User identity missing' }); + } + const result = await pharmacyService.getPatientPrescriptions(patientId); + res.json(result); + } catch (e: any) { + res.status(400).json({ error: e.message }); + } + } } diff --git a/services/staff-service/src/modules/pharmacy/pharmacy.repository.ts b/services/staff-service/src/modules/pharmacy/pharmacy.repository.ts index e36d375..09a4fb9 100644 --- a/services/staff-service/src/modules/pharmacy/pharmacy.repository.ts +++ b/services/staff-service/src/modules/pharmacy/pharmacy.repository.ts @@ -47,7 +47,11 @@ class PharmacyRepository { const result = await pool.query( ` SELECT - p.*, + p.id, + p.patient_id, + p.patient_name, + p.status, + p.created_at, s.name as doctor_name, json_agg(json_build_object( 'id', pi.id, @@ -85,5 +89,37 @@ class PharmacyRepository { ); return result.rows; } + + async getPatientPrescriptions(patientId: string) { + const result = await pool.query( + ` + SELECT + p.id, + p.patient_id, + p.patient_name, + p.status, + p.created_at, + s.name as doctor_name, + json_agg(json_build_object( + 'id', pi.id, + 'item_id', pi.item_id, + 'quantity', pi.quantity, + 'instructions', pi.instructions, + 'item_name', i.name, + 'category', i.category, + 'stock_available', COALESCE(i.quantity, 0) + )) as items + FROM prescriptions p + LEFT JOIN staff s ON p.doctor_id = s.id + LEFT JOIN prescription_items pi ON p.id = pi.prescription_id + LEFT JOIN inventory_items i ON pi.item_id = i.id + WHERE p.patient_id = $1 + GROUP BY p.id, s.name + ORDER BY p.created_at DESC + `, + [patientId] + ); + return result.rows; + } } export const pharmacyRepository = new PharmacyRepository(); diff --git a/services/staff-service/src/modules/pharmacy/pharmacy.routes.ts b/services/staff-service/src/modules/pharmacy/pharmacy.routes.ts index 2670c8e..2ffe1df 100644 --- a/services/staff-service/src/modules/pharmacy/pharmacy.routes.ts +++ b/services/staff-service/src/modules/pharmacy/pharmacy.routes.ts @@ -7,5 +7,6 @@ const controller = new PharmacyController(); router.post('/', controller.createPrescription.bind(controller)); router.get('/pending', controller.getPending.bind(controller)); router.post('/:id/dispense', controller.dispense.bind(controller)); +router.get('/me', controller.getMyPrescriptions.bind(controller)); export default router; diff --git a/services/staff-service/src/modules/pharmacy/pharmacy.service.ts b/services/staff-service/src/modules/pharmacy/pharmacy.service.ts index d4ef0e4..68a8c98 100644 --- a/services/staff-service/src/modules/pharmacy/pharmacy.service.ts +++ b/services/staff-service/src/modules/pharmacy/pharmacy.service.ts @@ -46,5 +46,14 @@ class PharmacyService { // 3. Complete fulfillment return pharmacyRepository.markDispensed(prescriptionId); } + + async getPatientPrescriptions(patientId: string) { + const prescriptions = + await pharmacyRepository.getPatientPrescriptions(patientId); + return prescriptions.map((p: any) => ({ + ...p, + items: (p.items || []).filter((item: any) => item.id !== null), + })); + } } export const pharmacyService = new PharmacyService(); From a8b2b66b6b574b35ed487aaf1fcfb23f6e77514f Mon Sep 17 00:00:00 2001 From: linasliyakath Date: Fri, 27 Mar 2026 12:19:43 +0530 Subject: [PATCH 7/7] Staff-CheckIN-Patient --- frontend/src/app/staff/dashboard/layout.tsx | 1 + .../src/app/staff/dashboard/queue/page.tsx | 121 ++++++++++++++---- .../appointments/api/getDoctorQueue.ts | 7 +- .../appointments/hooks/useDoctorQueue.ts | 6 +- frontend/src/features/staff/api/staff.api.ts | 13 ++ .../src/features/staff/hooks/useStaffData.ts | 19 +++ .../appointments/appointment.controller.ts | 14 +- .../appointments/appointment.repository.ts | 10 +- .../appointments/appointment.service.ts | 12 +- 9 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 frontend/src/features/staff/api/staff.api.ts create mode 100644 frontend/src/features/staff/hooks/useStaffData.ts diff --git a/frontend/src/app/staff/dashboard/layout.tsx b/frontend/src/app/staff/dashboard/layout.tsx index 5ad4edf..68eec89 100644 --- a/frontend/src/app/staff/dashboard/layout.tsx +++ b/frontend/src/app/staff/dashboard/layout.tsx @@ -60,6 +60,7 @@ export default function StaffLayout({ { label: 'Patients', icon: User, path: '/staff/dashboard/patients' }, ] : [ { label: 'Dashboard', icon: LayoutDashboard, path: '/staff/dashboard' }, + { label: "Today's Queue", icon: Activity, path: '/staff/dashboard/queue' }, { label: 'Beds', icon: Bed, path: '/staff/dashboard/beds' }, { label: 'Admissions', icon: ClipboardPlus, path: '/staff/dashboard/admissions' }, { label: 'Discharge', icon: FileMinus, path: '/staff/dashboard/discharge' }, diff --git a/frontend/src/app/staff/dashboard/queue/page.tsx b/frontend/src/app/staff/dashboard/queue/page.tsx index 294d7c1..2996d4a 100644 --- a/frontend/src/app/staff/dashboard/queue/page.tsx +++ b/frontend/src/app/staff/dashboard/queue/page.tsx @@ -12,42 +12,99 @@ import { useEmergency } from '../../../../features/appointments/hooks/useEmergen import { useRequestAdmission } from '../../../../features/admissions/hooks/useRequestAdmission'; import { Activity, Clock, Users, Timer, CheckCircle, AlertCircle } from 'lucide-react'; import { PrescribeModal } from '../../../../features/pharmacy/components/PrescribeModal'; +import { useDepartments, useDoctorsByDepartment } from '../../../../features/staff/hooks/useStaffData'; export default function QueuePage() { const { auth } = useStaffAuth(); - const doctorId = auth.staff?.id; + const isDoctor = auth.staff?.role === 'DOCTOR'; + const [selectedDeptId, setSelectedDeptId] = useState(''); + const [selectedDoctorId, setSelectedDoctorId] = useState(isDoctor ? auth.staff?.id : ''); const [showEmergency, setShowEmergency] = useState(false); const [patientId, setPatientId] = useState(''); const [prescribePatient, setPrescribePatient] = useState<{ id: string, name: string } | null>(null); + const { data: departments } = useDepartments(); + const { data: doctorsInDept, isLoading: isLoadingDoctors } = useDoctorsByDepartment(selectedDeptId); + + const statuses = isDoctor ? ['CHECKED_IN', 'IN_PROGRESS'] : ['SCHEDULED', 'CHECKED_IN', 'IN_PROGRESS']; + const { data, isLoading } = useDoctorQueue(selectedDoctorId, statuses); + + const startMutation = useStartAppointment(); + const completeMutation = useCompleteAppointment(); + const checkInMutation = useCheckInAppointment(); + const emergencyMutation = useEmergency(selectedDoctorId || ''); + const admitMutation = useRequestAdmission(); + if (auth.isRestoring) return (
); - if (!doctorId) return ( -
- Doctor information not available. -
- ); - const { data, isLoading } = useDoctorQueue(doctorId); + if (!isDoctor && !selectedDoctorId) { + return ( +
+
+
+ +

Staff Check-in Panel

+

Select a department and doctor to manage their queue.

+
- const startMutation = useStartAppointment(); - const completeMutation = useCompleteAppointment(); - const checkInMutation = useCheckInAppointment(); - const emergencyMutation = useEmergency(doctorId); - const admitMutation = useRequestAdmission(); +
+
+ + +
+ +
+ + +
+
+
+
+ ); + } + + const doctorId = selectedDoctorId; if (isLoading) return (
); + if (!data) return (
- No queue found. + No queue found for this doctor. + {!isDoctor && ( + + )}
); @@ -59,12 +116,22 @@ export default function QueuePage() { {/* 🔴 Header */}
-

-
- -
- Today's Queue -

+
+

+
+ +
+ {isDoctor ? "Today's Queue" : "Staff Check-in"} +

+ {!isDoctor && ( + + )} +
)} - {/* Start Consultation */} - {item.status === 'CHECKED_IN' && !someoneInProgress && ( + {/* Start Consultation - DOCTOR ONLY */} + {isDoctor && item.status === 'CHECKED_IN' && !someoneInProgress && ( )} - {/* Complete Consultation */} - {item.status === 'IN_PROGRESS' && ( + {/* Complete Consultation - DOCTOR ONLY */} + {isDoctor && item.status === 'IN_PROGRESS' && ( <>