Skip to content

Commit 4429f46

Browse files
committed
create nurse routes and tests
1 parent bc06b81 commit 4429f46

17 files changed

Lines changed: 573 additions & 1 deletion

File tree

scripts/smoke-nurse-routes.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/* smoke-nurse-routes.ts — quick runtime checks for nurse route handlers
2+
- Mocks `requirePermission` and `prisma` methods used by the handlers
3+
- Executes each handler and asserts expected shape/status
4+
*/
5+
import assert from 'assert';
6+
7+
// import modules under test
8+
import * as bedsRoute from '@/app/api/nurse/beds/route';
9+
import * as bedsAvailableRoute from '@/app/api/nurse/beds/available/route';
10+
import * as bedStatusRoute from '@/app/api/nurse/beds/[bedId]/status/route';
11+
import * as assignmentsRoute from '@/app/api/nurse/bed-assignments/route';
12+
import * as dischargeRoute from '@/app/api/nurse/bed-assignments/[id]/discharge/route';
13+
import * as patientsRoute from '@/app/api/nurse/patients/[patientId]/route';
14+
import * as patientBedRoute from '@/app/api/nurse/patients/[patientId]/bed/route';
15+
import * as emrRoute from '@/app/api/nurse/patients/[patientId]/emr/route';
16+
import * as labRoute from '@/app/api/nurse/patients/[patientId]/lab-tests/route';
17+
import * as nursingRecordsRoute from '@/app/api/nurse/nursing-records/route';
18+
import * as wardAssignmentsRoute from '@/app/api/nurse/ward/assignments/route';
19+
import * as auditLogsRoute from '@/app/api/nurse/audit-logs/route';
20+
21+
import * as authLib from '@/lib/authorization';
22+
import { prisma } from '@/lib/prisma';
23+
24+
// helpers
25+
const jsonRequest = (body?: any) => new Request('http://localhost', { method: body ? 'POST' : 'GET', body: body ? JSON.stringify(body) : undefined });
26+
27+
async function run() {
28+
// stub authorization to an authorized nurse by default
29+
(authLib as any).requirePermission = async (req: any, perm: string) => {
30+
if (perm === 'beds.update' || perm === 'nursing.create') {
31+
return { session: { user: { id: 'u-nurse' } } } as any;
32+
}
33+
return {} as any;
34+
};
35+
36+
// ---- beds list ----
37+
(prisma.bed as any).findMany = async (opts: any) => [ { id: 'b1', bedNumber: '1', ward: 'A', bedType: 'standard', status: 'AVAILABLE' } ];
38+
let res: any = await bedsRoute.GET(jsonRequest());
39+
let j = await res.json();
40+
assert(Array.isArray(j) && j.length > 0 && j[0].bedNumber === '1');
41+
42+
// ---- available beds ----
43+
(prisma.bed as any).findMany = async (opts: any) => [ { id: 'b1', bedNumber: '1', ward: 'A', bedType: 'standard', status: 'AVAILABLE' } ];
44+
res = await bedsAvailableRoute.GET(jsonRequest());
45+
j = await res.json();
46+
assert(j.every((x: any) => x.status === 'AVAILABLE'));
47+
48+
// ---- update bed status (PATCH) ----
49+
(prisma.bed as any).findUnique = async (q: any) => ({ id: 'b1', status: 'AVAILABLE' });
50+
(prisma.bed as any).update = async (q: any) => ({ id: 'b1', status: q.data.status });
51+
res = await bedStatusRoute.PATCH(new Request('http://localhost', { method: 'PATCH', body: JSON.stringify({ status: 'MAINTENANCE' }) }) as any, { params: { bedId: 'b1' } } as any);
52+
j = await res.json();
53+
assert(j.status === 'MAINTENANCE');
54+
55+
// ---- create bed-assignment (POST) ----
56+
(prisma.bed as any).findUnique = async (q: any) => ({ id: 'b1', status: 'AVAILABLE' });
57+
(prisma.nurse as any).findUnique = async (q: any) => ({ id: 'n1' });
58+
const fakeAssign = { id: 'a1', bedId: 'b1', patientId: 'p1', nurseId: 'n1' };
59+
(prisma as any).$transaction = async (ops: any[]) => [ fakeAssign, { id: 'b1', status: 'OCCUPIED' } ];
60+
61+
res = await assignmentsRoute.POST(jsonRequest({ bedId: 'b1', patientId: 'p1' }) as any);
62+
j = await res.json();
63+
assert(j.id === 'a1');
64+
65+
// ---- list active assignments ----
66+
(prisma.bedAssignment as any).findMany = async (opts: any) => [ fakeAssign ];
67+
res = await assignmentsRoute.GET(jsonRequest());
68+
j = await res.json();
69+
assert(Array.isArray(j));
70+
71+
// ---- discharge assignment ----
72+
const existing = { id: 'a2', bedId: 'b2', dischargedAt: null, bed: { id: 'b2', status: 'OCCUPIED' } };
73+
(prisma.bedAssignment as any).findUnique = async (q: any) => existing;
74+
(prisma as any).$transaction = async (ops: any[]) => [ { ...existing, dischargedAt: new Date() }, { id: 'b2', status: 'AVAILABLE' } ];
75+
res = await dischargeRoute.PATCH(jsonRequest() as any, { params: { id: 'a2' } } as any);
76+
j = await res.json();
77+
assert(j.assignment.dischargedAt);
78+
assert(j.bed.status === 'AVAILABLE');
79+
80+
// ---- patient basic info ----
81+
(prisma.patient as any).findUnique = async (q: any) => ({ id: 'p1', firstName: 'A', lastName: 'B', dateOfBirth: new Date(), gender: 'F', phone: 'x' });
82+
res = await patientsRoute.GET(jsonRequest() as any, { params: { patientId: 'p1' } } as any);
83+
j = await res.json();
84+
assert(j.id === 'p1');
85+
86+
// ---- patient bed ----
87+
(prisma.bedAssignment as any).findFirst = async (q: any) => ({ id: 'a1', bedId: 'b1', patientId: 'p1', bed: { id: 'b1', bedNumber: '1' } });
88+
res = await patientBedRoute.GET(jsonRequest() as any, { params: { patientId: 'p1' } } as any);
89+
j = await res.json();
90+
assert(j.bed && j.patientId === 'p1');
91+
92+
// ---- EMR and lab-tests (read-only) ----
93+
(prisma.eMR as any).findMany = async (q: any) => [{ id: 'e1', diagnosis: 'X' }];
94+
res = await emrRoute.GET(jsonRequest() as any, { params: { patientId: 'p1' } } as any);
95+
j = await res.json();
96+
assert(Array.isArray(j));
97+
98+
(prisma.labTest as any).findMany = async (q: any) => [{ id: 'lt1', testType: 'CBC' }];
99+
res = await labRoute.GET(jsonRequest() as any, { params: { patientId: 'p1' } } as any);
100+
j = await res.json();
101+
assert(Array.isArray(j));
102+
103+
// ---- nursing records GET/POST ----
104+
(prisma.nursingRecord as any).findMany = async (q: any) => [{ id: 'nr1', nurseId: 'n1', vitals: {} }];
105+
res = await nursingRecordsRoute.GET(new Request('http://localhost?patientName=foo') as any);
106+
j = await res.json();
107+
assert(Array.isArray(j));
108+
109+
(prisma.nursingRecord as any).create = async (q: any) => ({ id: 'nr2', ...q.data });
110+
res = await nursingRecordsRoute.POST(jsonRequest({ nurseId: 'n1', patientName: 'p1', vitals: { bp: '120/80' } }) as any);
111+
j = await res.json();
112+
assert(j.id === 'nr2');
113+
114+
// ---- ward assignments ----
115+
(prisma.nurse as any).findUnique = async (q: any) => ({ id: 'n1', department: 'Ward A' });
116+
(prisma.bedAssignment as any).findMany = async (q: any) => [ { id: 'a1', bed: { ward: 'Ward A' }, patient: { id: 'p1' } } ];
117+
res = await wardAssignmentsRoute.GET(new Request('http://localhost') as any);
118+
j = await res.json();
119+
assert(Array.isArray(j));
120+
121+
// ---- audit logs ----
122+
(prisma.auditLog as any).findMany = async (q: any) => [ { id: 'al1', action: 'bed.assign' } ];
123+
res = await auditLogsRoute.GET(new Request('http://localhost') as any);
124+
j = await res.json();
125+
assert(Array.isArray(j));
126+
127+
console.log('SMOKE: all nurse route handlers returned expected responses ✅');
128+
}
129+
130+
run().catch(err => {
131+
console.error('SMOKE FAILED —', err);
132+
process.exitCode = 2;
133+
});

src/app/(dashboard)/receptionist/appointments/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ export default function AppointmentBooking() {
382382
string,
383383
"default" | "success" | "warning" | "destructive"
384384
> = {
385-
SCHEDULED: "info",
385+
SCHEDULED: "default",
386386
COMPLETED: "success",
387387
CANCELLED: "destructive",
388388
NO_SHOW: "warning",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
5+
export async function GET(req: Request) {
6+
try {
7+
await requirePermission(req, 'audit.read');
8+
} catch (err) {
9+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
10+
}
11+
12+
const url = new URL(req.url);
13+
const resource = url.searchParams.get('resource');
14+
const actorId = url.searchParams.get('actorId');
15+
16+
const where: any = {};
17+
if (resource) where.resource = resource;
18+
if (actorId) where.actorId = actorId;
19+
20+
// limit and ordering consistent with other audit endpoints
21+
const rows = await prisma.auditLog.findMany({ where, orderBy: { createdAt: 'desc' }, take: 200 });
22+
return NextResponse.json(rows);
23+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
7+
try {
8+
const res = await requirePermission(req, 'beds.update');
9+
const actorId = res.session?.user?.id ?? null;
10+
11+
const existing = await prisma.bedAssignment.findUnique({ where: { id: params.id }, include: { bed: true } });
12+
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
13+
if (existing.dischargedAt) return NextResponse.json({ error: 'Already discharged' }, { status: 409 });
14+
15+
const dischargedAt = new Date();
16+
const [updatedAssignment, updatedBed] = await prisma.$transaction([
17+
prisma.bedAssignment.update({ where: { id: params.id }, data: { dischargedAt } }),
18+
prisma.bed.update({ where: { id: existing.bedId }, data: { status: 'AVAILABLE' } })
19+
]);
20+
21+
await createAudit({ actorId, action: 'bed.discharge', resource: 'BedAssignment', resourceId: params.id, before: existing, after: updatedAssignment });
22+
23+
return NextResponse.json({ assignment: updatedAssignment, bed: updatedBed });
24+
} catch (err) {
25+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
26+
}
27+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
export async function GET(req: Request) {
7+
try {
8+
await requirePermission(req, 'beds.read');
9+
} catch (err) {
10+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
11+
}
12+
13+
const rows = await prisma.bedAssignment.findMany({ where: { dischargedAt: null }, include: { bed: true, patient: true, nurse: { select: { id: true, department: true } } }, orderBy: { assignedAt: 'desc' } });
14+
return NextResponse.json(rows);
15+
}
16+
17+
export async function POST(req: Request) {
18+
try {
19+
const res = await requirePermission(req, 'beds.update');
20+
const actorId = res.session?.user?.id ?? null;
21+
const body = await req.json();
22+
const { bedId, patientId } = body || {};
23+
if (!bedId || !patientId) return NextResponse.json({ error: 'bedId and patientId required' }, { status: 400 });
24+
25+
// ensure bed exists and is available
26+
const bed = await prisma.bed.findUnique({ where: { id: bedId } });
27+
if (!bed) return NextResponse.json({ error: 'bed not found' }, { status: 404 });
28+
if (bed.status !== 'AVAILABLE') return NextResponse.json({ error: 'bed not available' }, { status: 409 });
29+
30+
// validate patient exists and is not already admitted
31+
const patient = await prisma.patient.findUnique({ where: { id: patientId } });
32+
if (!patient) return NextResponse.json({ error: 'patient not found' }, { status: 404 });
33+
const existingAssignment = await prisma.bedAssignment.findFirst({ where: { patientId, dischargedAt: null } });
34+
if (existingAssignment) return NextResponse.json({ error: 'patient already assigned to a bed' }, { status: 409 });
35+
36+
// resolve nurse entity from session if present
37+
const userId = res.session?.user?.id ?? null;
38+
const nurse = userId ? await prisma.nurse.findUnique({ where: { userId } }) : null;
39+
40+
const [assignment] = await prisma.$transaction([
41+
prisma.bedAssignment.create({ data: { bedId, patientId, nurseId: nurse?.id ?? undefined } }),
42+
prisma.bed.update({ where: { id: bedId }, data: { status: 'OCCUPIED' } })
43+
]);
44+
45+
await createAudit({ actorId, action: 'bed.assign', resource: 'BedAssignment', resourceId: assignment.id, after: assignment });
46+
47+
return NextResponse.json(assignment, { status: 201 });
48+
} catch (err) {
49+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
50+
}
51+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
export async function PATCH(req: Request, { params }: { params: { bedId: string } }) {
7+
try {
8+
const res = await requirePermission(req, 'beds.update');
9+
const actorId = res.session?.user?.id ?? null;
10+
const { status } = await req.json();
11+
if (!['AVAILABLE', 'OCCUPIED', 'MAINTENANCE'].includes(status)) {
12+
return NextResponse.json({ error: 'invalid status' }, { status: 400 });
13+
}
14+
15+
const before = await prisma.bed.findUnique({ where: { id: params.bedId } });
16+
if (!before) return NextResponse.json({ error: 'Not found' }, { status: 404 });
17+
18+
// don't allow marking AVAILABLE if there's an active assignment for this bed
19+
if (status === 'AVAILABLE') {
20+
const active = await prisma.bedAssignment.findFirst({ where: { bedId: params.bedId, dischargedAt: null } });
21+
if (active) return NextResponse.json({ error: 'bed has active assignment' }, { status: 409 });
22+
}
23+
24+
const updated = await prisma.bed.update({ where: { id: params.bedId }, data: { status } });
25+
26+
await createAudit({ actorId, action: 'bed.status.update', resource: 'Bed', resourceId: params.bedId, before, after: updated });
27+
28+
return NextResponse.json(updated);
29+
} catch (err) {
30+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
31+
}
32+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
5+
export async function GET(req: Request) {
6+
try {
7+
await requirePermission(req, 'beds.read');
8+
} catch (err) {
9+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
10+
}
11+
12+
const rows = await prisma.bed.findMany({
13+
where: { status: 'AVAILABLE' },
14+
orderBy: { bedNumber: 'asc' },
15+
select: { id: true, bedNumber: true, ward: true, bedType: true, status: true }
16+
});
17+
18+
return NextResponse.json(rows);
19+
}

src/app/api/nurse/beds/route.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
5+
export async function GET(req: Request) {
6+
try {
7+
await requirePermission(req, 'beds.read');
8+
} catch (err) {
9+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
10+
}
11+
12+
const rows = await prisma.bed.findMany({
13+
take: 1000,
14+
orderBy: { bedNumber: 'asc' },
15+
select: { id: true, bedNumber: true, ward: true, bedType: true, status: true, updatedAt: true }
16+
});
17+
18+
return NextResponse.json(rows);
19+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
export async function GET(req: Request, { params }: { params: { id: string } }) {
7+
try {
8+
await requirePermission(req, 'nursing.read');
9+
} catch (err) {
10+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
11+
}
12+
13+
const rec = await prisma.nursingRecord.findUnique({ where: { id: params.id } });
14+
if (!rec) return NextResponse.json({ error: 'Not found' }, { status: 404 });
15+
return NextResponse.json(rec);
16+
}
17+
18+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
19+
try {
20+
const res = await requirePermission(req, 'nursing.update');
21+
const actorId = res.session?.user?.id ?? null;
22+
const body = await req.json();
23+
const before = await prisma.nursingRecord.findUnique({ where: { id: params.id } });
24+
if (!before) return NextResponse.json({ error: 'Not found' }, { status: 404 });
25+
26+
const allowed: any = {};
27+
if (body.vitals) allowed.vitals = body.vitals;
28+
if (typeof body.notes === 'string') allowed.notes = body.notes;
29+
30+
const updated = await prisma.nursingRecord.update({ where: { id: params.id }, data: allowed });
31+
await createAudit({ actorId, action: 'nursing.record.update', resource: 'NursingRecord', resourceId: params.id, before, after: updated });
32+
return NextResponse.json(updated);
33+
} catch (err) {
34+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
35+
}
36+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { Prisma } from '@prisma/client';
5+
import { createAudit } from '@/services/audit.service';
6+
7+
export async function GET(req: Request) {
8+
try {
9+
await requirePermission(req, 'nursing.read');
10+
} catch (err) {
11+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
12+
}
13+
14+
const url = new URL(req.url);
15+
const patientName = url.searchParams.get('patientName');
16+
const where: Prisma.NursingRecordWhereInput = patientName
17+
? { patientName: { contains: patientName, mode: Prisma.QueryMode.insensitive } }
18+
: {};
19+
20+
const rows = await prisma.nursingRecord.findMany({ where, orderBy: { createdAt: 'desc' }, take: 200 });
21+
return NextResponse.json(rows);
22+
}
23+
24+
export async function POST(req: Request) {
25+
try {
26+
const res = await requirePermission(req, 'nursing.create');
27+
const actorId = res.session?.user?.id ?? null;
28+
const body = await req.json();
29+
if (!body.vitals || !body.nurseId) return NextResponse.json({ error: 'nurseId and vitals required' }, { status: 400 });
30+
31+
const rec = await prisma.nursingRecord.create({ data: { nurseId: body.nurseId, patientName: body.patientName ?? '', vitals: body.vitals ?? {}, notes: body.notes ?? null } });
32+
33+
await createAudit({ actorId, action: 'nursing.record.create', resource: 'NursingRecord', resourceId: rec.id, after: rec });
34+
35+
return NextResponse.json(rec, { status: 201 });
36+
} catch (err) {
37+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
38+
}
39+
}

0 commit comments

Comments
 (0)