Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A comprehensive Hospital Information System (HIS) built with **Next.js 15**, **React 19**, and **TypeScript**, featuring real-time analytics, role-based access control, and modern Apple HCI-inspired UI/UX.



## ✨ Current Features

### Core Capabilities
Expand Down
60 changes: 60 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,36 @@ model Doctor {
userId String @unique
specialization String
licenseNumber String @unique
seniority SeniorityLevel @default(JUNIOR)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
appointments Appointment[]
shifts DoctorShift[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
prescriptions Prescription[]
referralsSent Referral[] @relation("ReferralsFrom")
referralsReceived Referral[] @relation("ReferralsTo")
surgeries Surgery[]
}

model DoctorShift {
id String @id @default(cuid())
doctorId String
shiftType ShiftType
dayOfWeek Int // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
startTime String // Format: "HH:mm" (e.g., "09:00")
endTime String // Format: "HH:mm" (e.g., "17:00")
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
doctor Doctor @relation(fields: [doctorId], references: [id], onDelete: Cascade)

@@unique([doctorId, shiftType, dayOfWeek])
@@index([doctorId])
@@index([shiftType])
@@index([dayOfWeek])
}

model Nurse {
id String @id @default(cuid())
userId String @unique
Expand Down Expand Up @@ -134,6 +154,11 @@ model Appointment {
reason String
status AppointmentStatus @default(SCHEDULED)
notes String?
// Smart Scheduling Fields
severity SeverityLevel?
problemCategory ProblemCategory?
symptoms String[]
wasAutoAssigned Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
referralId String? @unique
Expand Down Expand Up @@ -503,6 +528,41 @@ enum ReferralUrgency {
EMERGENCY
}

// Smart Scheduling Enums
enum SeverityLevel {
LOW
MODERATE
HIGH
}

enum ProblemCategory {
HEART
BONE_JOINT
EAR_NOSE_THROAT
EYE
SKIN
DIGESTIVE
RESPIRATORY
NEUROLOGICAL
DENTAL
GENERAL
PEDIATRIC
GYNECOLOGY
UROLOGY
MENTAL_HEALTH
}

enum SeniorityLevel {
HOD
SENIOR
JUNIOR
}

enum ShiftType {
MORNING
AFTERNOON
EVENING
NIGHT
enum InsurancePolicyStatus {
ACTIVE
EXPIRED
Expand Down
6 changes: 6 additions & 0 deletions src/app/api/appointments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { requirePermission } from "@/lib/authorization";
import { prisma } from "@/lib/prisma";
import { createAudit } from "@/services/audit.service";
import { SeverityLevel, ProblemCategory } from "@/types/scheduling";

export async function GET(req: Request) {
try {
Expand Down Expand Up @@ -82,6 +83,11 @@ export async function POST(req: Request) {
reason: body.reason,
notes: body.notes || null,
status: "SCHEDULED",
// Smart scheduling fields (optional)
severity: body.severity as SeverityLevel | undefined,
problemCategory: body.problemCategory as ProblemCategory | undefined,
symptoms: body.symptoms || [],
wasAutoAssigned: body.wasAutoAssigned || false,
},
include: {
patient: true,
Expand Down
188 changes: 188 additions & 0 deletions src/app/api/appointments/smart/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { findBestAvailableDoctor } from '@/services/scheduling.service';
import {
SeverityLevel,
ProblemCategory,
} from '@/types/scheduling';

/**
* POST /api/appointments/smart
* Create an appointment with automatic doctor assignment based on severity and problem category
*/
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const body = await request.json();
const {
patientId,
severity,
problemCategory,
symptoms,
preferredDate,
preferredTime,
reason,
notes,
} = body;

// Validate required fields
if (!patientId || !severity || !problemCategory || !preferredDate) {
return NextResponse.json(
{ error: 'Missing required fields: patientId, severity, problemCategory, preferredDate' },
{ status: 400 },
);
}

// Validate severity
if (!['LOW', 'MODERATE', 'HIGH'].includes(severity)) {
return NextResponse.json(
{ error: 'Invalid severity. Must be LOW, MODERATE, or HIGH' },
{ status: 400 },
);
}

// Build requested datetime
const dateTime = new Date(`${preferredDate}T${preferredTime || '09:00'}:00`);
if (isNaN(dateTime.getTime())) {
return NextResponse.json(
{ error: 'Invalid date/time format' },
{ status: 400 },
);
}

// Find best available doctor
const recommendation = await findBestAvailableDoctor(
problemCategory as ProblemCategory,
severity as SeverityLevel,
dateTime,
);

if (!recommendation.success || !recommendation.doctor) {
return NextResponse.json(
{
error: 'No suitable doctor available',
details: recommendation.reason,
alternatives: recommendation.alternativeDoctors,
},
{ status: 404 },
);
}

// Get receptionist ID if user is a receptionist
let receptionistId: string | null = null;
if (session.user.role === 'RECEPTIONIST') {
const receptionist = await prisma.receptionist.findUnique({
where: { userId: session.user.id },
});
receptionistId = receptionist?.id || null;
}

// Create the appointment with smart scheduling data
const appointment = await prisma.appointment.create({
data: {
patientId,
doctorId: recommendation.doctor.id,
receptionistId,
dateTime,
reason: reason || `${problemCategory.replace('_', ' ')} - ${severity} severity`,
notes,
severity: severity as SeverityLevel,
problemCategory: problemCategory as ProblemCategory,
symptoms: symptoms || [],
wasAutoAssigned: true,
status: 'SCHEDULED',
},
include: {
patient: {
select: {
id: true,
firstName: true,
lastName: true,
phone: true,
},
},
doctor: {
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
},
},
});

return NextResponse.json({
success: true,
appointment,
assignmentDetails: {
doctor: recommendation.doctor,
reason: recommendation.reason,
alternatives: recommendation.alternativeDoctors,
},
});
} catch (error) {
console.error('Smart appointment creation error:', error);
return NextResponse.json(
{ error: 'Failed to create appointment' },
{ status: 500 },
);
}
}

/**
* GET /api/appointments/smart
* Get doctor recommendations without creating an appointment
*/
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { searchParams } = new URL(request.url);
const severity = searchParams.get('severity') as SeverityLevel;
const problemCategory = searchParams.get('problemCategory') as ProblemCategory;
const date = searchParams.get('date');
const time = searchParams.get('time') || '09:00';

if (!severity || !problemCategory || !date) {
return NextResponse.json(
{ error: 'Missing required params: severity, problemCategory, date' },
{ status: 400 },
);
}

const dateTime = new Date(`${date}T${time}:00`);
if (isNaN(dateTime.getTime())) {
return NextResponse.json(
{ error: 'Invalid date/time format' },
{ status: 400 },
);
}

const recommendation = await findBestAvailableDoctor(
problemCategory,
severity,
dateTime,
);

return NextResponse.json(recommendation);
} catch (error) {
console.error('Doctor recommendation error:', error);
return NextResponse.json(
{ error: 'Failed to get recommendations' },
{ status: 500 },
);
}
}
Loading