feat: implement core hospital workflow system with API and UI#2
feat: implement core hospital workflow system with API and UI#2
Conversation
Full-stack monorepo with pnpm workspaces + Turborepo for 3 parallel developer workstreams: - Developer A: Patient flow & auth (Express + JWT + Prisma) - Developer B: Bed management & task board (Socket.IO real-time) - Developer C: Staff scheduling & department coordination Includes: Next.js 14 frontend, Express API with 7 modules, PostgreSQL schema, shared Zod types, Docker setup, and seed data. https://claude.ai/code/session_01LXmx7H3ihtz3eyWBia2dTU
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bf05d3da23
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }); | ||
|
|
||
| // GET /api/departments/:id | ||
| router.get("/:id", async (req: Request, res: Response) => { |
There was a problem hiding this comment.
Move transfer list route before
/:id matcher
Because router.get("/:id") is registered before router.get("/transfers"), a request to GET /api/departments/transfers is consumed by the /:id handler (id = "transfers") and never reaches the transfer-list logic. In practice this makes the list endpoint unreachable and returns department-not-found behavior instead of transfer data.
Useful? React with 👍 / 👎.
| const notification = await prisma.notification.update({ | ||
| where: { id: req.params.id }, | ||
| data: { isRead: true }, |
There was a problem hiding this comment.
Enforce recipient ownership when marking notifications read
This update only filters by notification id, so any authenticated user who can obtain another user's notification ID can mark it as read. The read-all endpoint already scopes by recipientId, so this single-item endpoint should apply the same ownership check to prevent cross-user state changes.
Useful? React with 👍 / 👎.
| const staff = await prisma.staff.findUnique({ where: { userId: req.user!.userId } }); | ||
| const transfer = await prisma.transferRequest.update({ | ||
| where: { id: req.params.id }, | ||
| data: { status: "REJECTED", approvedById: staff?.id, resolvedAt: new Date() }, |
There was a problem hiding this comment.
Require staff authorization for transfer rejection
The reject path looks up the caller's staff record but never enforces that one exists before updating the transfer, unlike the approve path which returns 403 for non-staff users. As written, any authenticated account can reject transfer requests, which breaks the intended permission boundary for transfer decisions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR bootstraps a full-stack “Smart Hospital Workflow Optimization” monorepo, introducing core backend (Express + Prisma + Socket.IO), frontend (Next.js + Tailwind), and a shared types/validation package to establish an end-to-end foundation.
Changes:
- Added pnpm workspace + Turborepo configuration and shared TS/ESLint configs.
- Implemented initial API (auth, patients, beds, tasks, staff, departments/transfers, notifications) backed by a new Prisma schema + seed data.
- Added initial Next.js UI shell and feature pages, plus a shared types package with Zod schemas/enums.
Reviewed changes
Copilot reviewed 59 out of 60 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| turbo.json | Turborepo task pipeline configuration. |
| pnpm-workspace.yaml | Defines workspace package locations. |
| package.json | Root scripts to run/build/lint/test monorepo. |
| docker-compose.yml | Local orchestration for Postgres + API + web. |
| .gitignore | Ignores node/build/env artifacts. |
| .env.example | Environment variable template for local/dev. |
| README.md | Project documentation and setup instructions. |
| CLAUDE.md | Project conventions and developer guidance. |
| packages/eslint-config/package.json | Shared ESLint config package manifest. |
| packages/eslint-config/index.js | Shared ESLint rules for TS packages. |
| packages/tsconfig/package.json | Shared TS config package manifest. |
| packages/tsconfig/base.json | Base strict TS compiler settings. |
| packages/tsconfig/node.json | Node-focused TS compiler settings. |
| packages/tsconfig/nextjs.json | Next.js-focused TS compiler settings. |
| packages/shared-types/package.json | Shared API contract package manifest. |
| packages/shared-types/tsconfig.json | Build config for shared-types package. |
| packages/shared-types/src/enums.ts | Domain enums for roles/statuses/types. |
| packages/shared-types/src/schemas.ts | Zod schemas for request/DTO validation. |
| packages/shared-types/src/index.ts | Re-exports schemas/enums and inferred types. |
| apps/api/package.json | API dependencies and scripts. |
| apps/api/tsconfig.json | API TypeScript build configuration. |
| apps/api/Dockerfile | Container build for API service. |
| apps/api/src/config/env.ts | API environment configuration. |
| apps/api/src/config/db.ts | Prisma client initialization. |
| apps/api/src/config/socket.ts | Socket.IO server setup and accessor. |
| apps/api/src/middleware/auth.ts | JWT auth + role-based authorization middleware. |
| apps/api/src/middleware/validate.ts | Zod request-body validation middleware. |
| apps/api/src/middleware/errorHandler.ts | Global error handler middleware. |
| apps/api/src/modules/auth/auth.routes.ts | Auth routes (register/login/refresh/me). |
| apps/api/src/modules/patients/patient.routes.ts | Patient CRUD + admit/discharge workflows. |
| apps/api/src/modules/beds/bed.routes.ts | Bed CRUD + assign/release + stats endpoint. |
| apps/api/src/modules/tasks/task.routes.ts | Task CRUD + escalation + Socket.IO events. |
| apps/api/src/modules/staff/staff.routes.ts | Staff CRUD + schedule + shift endpoints. |
| apps/api/src/modules/departments/department.routes.ts | Department CRUD + transfer request endpoints. |
| apps/api/src/modules/notifications/notification.routes.ts | Notifications list + mark-read endpoints. |
| apps/api/src/app.ts | Express app wiring (middleware + module routers). |
| apps/api/src/server.ts | HTTP server bootstrap + Socket.IO init. |
| apps/api/prisma/schema.prisma | Core database schema (users/patients/beds/tasks/etc.). |
| apps/api/prisma/seed.ts | Seed script with demo departments/users/staff/patients/beds. |
| apps/web/package.json | Web app dependencies and scripts. |
| apps/web/tsconfig.json | Web app TypeScript configuration. |
| apps/web/next.config.js | Next config (transpile workspace package). |
| apps/web/tailwind.config.ts | Tailwind configuration + theme colors. |
| apps/web/postcss.config.js | PostCSS config for Tailwind pipeline. |
| apps/web/Dockerfile | Container build for web service. |
| apps/web/lib/cn.ts | Utility to merge Tailwind class names. |
| apps/web/lib/api.ts | Fetch wrapper for API calls with token support. |
| apps/web/components/ui/Card.tsx | UI card component. |
| apps/web/components/ui/StatusBadge.tsx | Status badge with color mapping. |
| apps/web/components/Sidebar.tsx | Sidebar navigation shell. |
| apps/web/components/Header.tsx | Header shell with notification/user stub. |
| apps/web/app/globals.css | Global Tailwind styles. |
| apps/web/app/layout.tsx | Root layout wiring sidebar/header and main content. |
| apps/web/app/page.tsx | Redirects home to dashboard. |
| apps/web/app/dashboard/page.tsx | Dashboard page scaffold with summary cards. |
| apps/web/app/patients/page.tsx | Patients page scaffold (mock data). |
| apps/web/app/beds/page.tsx | Beds page scaffold (mock data). |
| apps/web/app/tasks/page.tsx | Tasks page scaffold (mock data). |
| apps/web/app/staff/page.tsx | Staff page scaffold (mock data). |
| apps/web/app/departments/page.tsx | Departments page scaffold (mock data). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "private": true, | ||
| "main": "./src/index.ts", | ||
| "types": "./src/index.ts", |
There was a problem hiding this comment.
@smart-hospital/shared-types points main/types at ./src/index.ts (TypeScript) and doesn’t define a build step. Since the API runs compiled JS (node dist/server.js), Node will try to load TS from this package at runtime and crash. Emit JS/types to dist/ and update main/types (and optionally exports) to reference the built output; make API build depend on it.
| const notification = await prisma.notification.update({ | ||
| where: { id: req.params.id }, | ||
| data: { isRead: true }, | ||
| }); |
There was a problem hiding this comment.
PATCH /api/notifications/:id/read updates by notification id only, without scoping to recipientId = req.user.userId. Any authenticated user who can guess a UUID could mark someone else’s notification as read. Update with a where clause including recipientId (or fetch+verify ownership) and return 404/403 when it doesn’t belong to the caller.
| const notification = await prisma.notification.update({ | |
| where: { id: req.params.id }, | |
| data: { isRead: true }, | |
| }); | |
| const result = await prisma.notification.updateMany({ | |
| where: { id: req.params.id, recipientId: req.user!.userId }, | |
| data: { isRead: true }, | |
| }); | |
| if (result.count === 0) { | |
| return res.status(404).json({ success: false, error: "Notification not found" }); | |
| } | |
| const notification = await prisma.notification.findFirst({ | |
| where: { id: req.params.id, recipientId: req.user!.userId }, | |
| }); |
| environment: | ||
| POSTGRES_USER: postgres | ||
| POSTGRES_PASSWORD: postgres | ||
| POSTGRES_DB: smart_hospital |
There was a problem hiding this comment.
The compose file hardcodes database credentials (POSTGRES_USER/POSTGRES_PASSWORD). Even for local dev, prefer .env-driven configuration (or env var passthrough) to reduce accidental reuse/leakage and make overrides easier across environments.
| environment: | ||
| DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/smart_hospital?schema=public" | ||
| JWT_SECRET: "change-this-in-production" | ||
| JWT_REFRESH_SECRET: "change-this-refresh-in-production" | ||
| API_PORT: "4000" |
There was a problem hiding this comment.
JWT secrets are hardcoded in docker-compose.yml (JWT_SECRET, JWT_REFRESH_SECRET). If someone forgets to override these, it’s easy to accidentally deploy with known secrets. Prefer reading them from .env/external env vars and avoid committing defaults that look usable.
| if (bedId) { | ||
| await tx.bed.update({ | ||
| where: { id: bedId }, | ||
| data: { status: "OCCUPIED", currentPatientId: patientId }, | ||
| }); |
There was a problem hiding this comment.
Admitting a patient updates the bed to OCCUPIED without validating that the bed exists, is AVAILABLE, and belongs to the target departmentId. This can overwrite an existing assignment and produce inconsistent state. Add checks (ideally inside the transaction) and return a 409/400 when the bed is invalid/unavailable.
| const transfer = await prisma.transferRequest.update({ | ||
| where: { id: req.params.id }, | ||
| data: { status: "REJECTED", approvedById: staff?.id, resolvedAt: new Date() }, |
There was a problem hiding this comment.
PATCH /api/departments/transfers/:id/reject doesn’t enforce that the caller is staff: if no staff record exists, approvedById becomes null and the update still succeeds. This allows any authenticated user to reject transfers. Require a staff profile (and ideally appropriate roles) the same way the approve endpoint does.
| const transfer = await prisma.transferRequest.update({ | |
| where: { id: req.params.id }, | |
| data: { status: "REJECTED", approvedById: staff?.id, resolvedAt: new Date() }, | |
| if (!staff) { | |
| res.status(403).json({ success: false, error: "Only staff can reject transfers" }); | |
| return; | |
| } | |
| const transfer = await prisma.transferRequest.update({ | |
| where: { id: req.params.id }, | |
| data: { status: "REJECTED", approvedById: staff.id, resolvedAt: new Date() }, |
| "@smart-hospital/tsconfig": "workspace:*", | ||
| "@types/bcryptjs": "^2.4.6", | ||
| "@types/cors": "^2.8.17", | ||
| "@types/express": "^5.0.0", |
There was a problem hiding this comment.
@types/express is ^5.0.0 while express is ^4.21.0. Express v5 typings are not compatible with Express 4 in many cases and can cause incorrect types/build errors. Align these by using @types/express@^4.x (or upgrade Express to v5 consistently).
| "@types/express": "^5.0.0", | |
| "@types/express": "^4.17.21", |
| const { email, password, firstName, lastName, role } = req.body; | ||
|
|
There was a problem hiding this comment.
RegisterSchema requires firstName/lastName, but the register handler doesn’t persist them anywhere (they’re only used in the response message). This makes the request contract misleading. Either store these fields (e.g., on User or by creating the Staff profile here) or remove them from the register schema.
| export const env = { | ||
| PORT: parseInt(process.env.API_PORT || "4000", 10), | ||
| DATABASE_URL: process.env.DATABASE_URL!, | ||
| JWT_SECRET: process.env.JWT_SECRET || "dev-secret", | ||
| JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || "dev-refresh-secret", |
There was a problem hiding this comment.
DATABASE_URL is asserted non-null and JWT secrets fall back to weak defaults (dev-secret, dev-refresh-secret). Consider validating required env vars at startup and failing fast with clear errors; for JWT secrets, require explicit values outside local dev to avoid accidentally running with insecure defaults.
| export const env = { | |
| PORT: parseInt(process.env.API_PORT || "4000", 10), | |
| DATABASE_URL: process.env.DATABASE_URL!, | |
| JWT_SECRET: process.env.JWT_SECRET || "dev-secret", | |
| JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || "dev-refresh-secret", | |
| const isLocalDevelopment = | |
| !process.env.NODE_ENV || | |
| process.env.NODE_ENV === "development" || | |
| process.env.NODE_ENV === "dev"; | |
| function getRequiredEnvVar(name: string): string { | |
| const value = process.env[name]?.trim(); | |
| if (!value) { | |
| throw new Error(`Missing required environment variable: ${name}`); | |
| } | |
| return value; | |
| } | |
| function getJwtSecret(name: string, localDefault: string): string { | |
| const value = process.env[name]?.trim(); | |
| if (value) { | |
| return value; | |
| } | |
| if (isLocalDevelopment) { | |
| return localDefault; | |
| } | |
| throw new Error( | |
| `Missing required environment variable: ${name}. Weak development defaults are only allowed in local development.` | |
| ); | |
| } | |
| export const env = { | |
| PORT: parseInt(process.env.API_PORT || "4000", 10), | |
| DATABASE_URL: getRequiredEnvVar("DATABASE_URL"), | |
| JWT_SECRET: getJwtSecret("JWT_SECRET", "dev-secret"), | |
| JWT_REFRESH_SECRET: getJwtSecret("JWT_REFRESH_SECRET", "dev-refresh-secret"), |
| include: { beds: true }, | ||
| }); | ||
|
|
||
| const stats = departments.map((dept) => ({ | ||
| departmentId: dept.id, | ||
| departmentName: dept.name, | ||
| total: dept.beds.length, | ||
| available: dept.beds.filter((b) => b.status === "AVAILABLE").length, | ||
| occupied: dept.beds.filter((b) => b.status === "OCCUPIED").length, | ||
| reserved: dept.beds.filter((b) => b.status === "RESERVED").length, | ||
| maintenance: dept.beds.filter((b) => b.status === "MAINTENANCE").length, | ||
| occupancyRate: dept.beds.length > 0 | ||
| ? Math.round((dept.beds.filter((b) => b.status === "OCCUPIED").length / dept.beds.length) * 100) | ||
| : 0, | ||
| })); | ||
|
|
There was a problem hiding this comment.
GET /api/beds/stats fetches all active departments including full beds arrays and then computes counts in memory. This scales poorly as bed counts grow. Prefer Prisma aggregations/groupBy (or count queries by status) so you only transfer the computed counts.
| include: { beds: true }, | |
| }); | |
| const stats = departments.map((dept) => ({ | |
| departmentId: dept.id, | |
| departmentName: dept.name, | |
| total: dept.beds.length, | |
| available: dept.beds.filter((b) => b.status === "AVAILABLE").length, | |
| occupied: dept.beds.filter((b) => b.status === "OCCUPIED").length, | |
| reserved: dept.beds.filter((b) => b.status === "RESERVED").length, | |
| maintenance: dept.beds.filter((b) => b.status === "MAINTENANCE").length, | |
| occupancyRate: dept.beds.length > 0 | |
| ? Math.round((dept.beds.filter((b) => b.status === "OCCUPIED").length / dept.beds.length) * 100) | |
| : 0, | |
| })); | |
| select: { | |
| id: true, | |
| name: true, | |
| _count: { | |
| select: { beds: true }, | |
| }, | |
| }, | |
| }); | |
| const departmentIds = departments.map((dept) => dept.id); | |
| const bedCountsByStatus = departmentIds.length > 0 | |
| ? await prisma.bed.groupBy({ | |
| by: ["departmentId", "status"], | |
| where: { | |
| departmentId: { in: departmentIds }, | |
| }, | |
| _count: { | |
| _all: true, | |
| }, | |
| }) | |
| : []; | |
| const countsByDepartment = bedCountsByStatus.reduce<Record<string, { | |
| AVAILABLE: number; | |
| OCCUPIED: number; | |
| RESERVED: number; | |
| MAINTENANCE: number; | |
| }>>((acc, row) => { | |
| if (!acc[row.departmentId]) { | |
| acc[row.departmentId] = { | |
| AVAILABLE: 0, | |
| OCCUPIED: 0, | |
| RESERVED: 0, | |
| MAINTENANCE: 0, | |
| }; | |
| } | |
| if (row.status === "AVAILABLE" || row.status === "OCCUPIED" || row.status === "RESERVED" || row.status === "MAINTENANCE") { | |
| acc[row.departmentId][row.status] = row._count._all; | |
| } | |
| return acc; | |
| }, {}); | |
| const stats = departments.map((dept) => { | |
| const counts = countsByDepartment[dept.id] ?? { | |
| AVAILABLE: 0, | |
| OCCUPIED: 0, | |
| RESERVED: 0, | |
| MAINTENANCE: 0, | |
| }; | |
| const total = dept._count.beds; | |
| const occupied = counts.OCCUPIED; | |
| return { | |
| departmentId: dept.id, | |
| departmentName: dept.name, | |
| total, | |
| available: counts.AVAILABLE, | |
| occupied, | |
| reserved: counts.RESERVED, | |
| maintenance: counts.MAINTENANCE, | |
| occupancyRate: total > 0 ? Math.round((occupied / total) * 100) : 0, | |
| }; | |
| }); |
Summary
This PR establishes the foundational architecture for the Smart Hospital Workflow Optimization platform (Package 1), implementing a complete full-stack system with database schema, REST API, frontend UI, and shared type definitions.
Key Changes
Database & ORM
apps/api/prisma/schema.prisma) with 11 models covering:apps/api/prisma/seed.ts) with sample departments, users, staff, and patientsBackend API (Express.js)
auth.routes.ts): JWT-based login/register with bcrypt password hashingpatient.routes.ts): CRUD operations with filtering, search, and paginationbed.routes.ts): Bed assignment, status management, and occupancy statisticstask.routes.ts): Task creation, assignment, and status updates with Socket.IO eventsstaff.routes.ts): Staff management and shift schedulingdepartment.routes.ts): Department CRUD and patient transfer coordinationnotification.routes.ts): Real-time notification deliveryFrontend (Next.js 14)
Shared Types & Validation
packages/shared-types/src/schemas.ts) for:Infrastructure & Configuration
.env.example)Documentation
Notable Implementation Details
https://claude.ai/code/session_01LXmx7H3ihtz3eyWBia2dTU