Spaces:
Sleeping
Sleeping
Commit ·
5266fc5
1
Parent(s): cd5e33d
first
Browse files- db/mongo.js +114 -49
- middleware/auth.js +5 -3
- models/Appointment.js +281 -0
- models/DeleteRequest.js +136 -0
- models/Patient.js +189 -0
- models/User.js +112 -0
- models/Warning.js +232 -0
- models/index.js +28 -0
- routes/appointments.js +130 -15
- routes/auth.js +145 -16
- routes/patients.js +347 -87
- routes/warnings.js +2 -7
- services/appointmentService.js +99 -210
- services/authService.js +61 -34
- services/timelineService.js +8 -11
- services/vectorService.js +4 -4
- services/warningService.js +36 -146
db/mongo.js
CHANGED
|
@@ -1,73 +1,138 @@
|
|
| 1 |
-
import
|
| 2 |
import dotenv from 'dotenv';
|
| 3 |
|
| 4 |
dotenv.config();
|
| 5 |
|
| 6 |
const MONGO_URI = process.env.MONGO_URI;
|
| 7 |
|
| 8 |
-
|
| 9 |
-
let
|
| 10 |
|
| 11 |
-
// Collections
|
| 12 |
-
let patientsCollection;
|
| 13 |
-
let usersCollection;
|
| 14 |
-
let appointmentsCollection;
|
| 15 |
-
let warningsCollection;
|
| 16 |
|
| 17 |
export const connectDB = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
try {
|
| 19 |
-
|
| 20 |
-
|
|
|
|
| 21 |
});
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
-
await client.db('admin').command({ ping: 1 });
|
| 25 |
-
console.log('✓ Connected to MongoDB Atlas successfully!');
|
| 26 |
-
|
| 27 |
-
db = client.db('patient_info');
|
| 28 |
-
|
| 29 |
-
// Initialize collections
|
| 30 |
-
patientsCollection = db.collection('patients');
|
| 31 |
-
usersCollection = db.collection('users');
|
| 32 |
-
appointmentsCollection = db.collection('appointments');
|
| 33 |
-
warningsCollection = db.collection('clinical_warnings');
|
| 34 |
-
|
| 35 |
-
return db;
|
| 36 |
} catch (error) {
|
| 37 |
-
console.log(`
|
| 38 |
-
console.log('The server will start, but database operations may fail.');
|
| 39 |
-
console.log('Please check:');
|
| 40 |
-
console.log(' 1. Your MongoDB Atlas IP whitelist (add your current IP)');
|
| 41 |
-
console.log(' 2. Your internet connection');
|
| 42 |
-
console.log(' 3. If your Atlas cluster is paused');
|
| 43 |
-
|
| 44 |
-
// Create client anyway for later retry
|
| 45 |
-
client = new MongoClient(MONGO_URI);
|
| 46 |
-
db = client.db('patient_info');
|
| 47 |
-
|
| 48 |
-
patientsCollection = db.collection('patients');
|
| 49 |
-
usersCollection = db.collection('users');
|
| 50 |
-
appointmentsCollection = db.collection('appointments');
|
| 51 |
-
warningsCollection = db.collection('clinical_warnings');
|
| 52 |
-
|
| 53 |
-
return db;
|
| 54 |
}
|
| 55 |
};
|
| 56 |
|
|
|
|
|
|
|
|
|
|
| 57 |
export const ensureIndexes = async () => {
|
| 58 |
try {
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
} catch (error) {
|
| 62 |
-
console.log(`
|
| 63 |
}
|
| 64 |
};
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
export const
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
export
|
|
|
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mongoose from 'mongoose';
|
| 2 |
import dotenv from 'dotenv';
|
| 3 |
|
| 4 |
dotenv.config();
|
| 5 |
|
| 6 |
const MONGO_URI = process.env.MONGO_URI;
|
| 7 |
|
| 8 |
+
// Connection state
|
| 9 |
+
let isConnected = false;
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
export const connectDB = async () => {
|
| 13 |
+
if (isConnected) {
|
| 14 |
+
console.log('[OK] Using existing database connection');
|
| 15 |
+
return mongoose.connection;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
try {
|
| 19 |
+
const conn = await mongoose.connect(MONGO_URI, {
|
| 20 |
+
dbName: 'patient_info',
|
| 21 |
+
serverSelectionTimeoutMS: 5000,
|
| 22 |
});
|
| 23 |
+
|
| 24 |
+
isConnected = true;
|
| 25 |
+
console.log(`[OK] Database: ${conn.connection.name}`);
|
| 26 |
|
| 27 |
+
return conn.connection;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
} catch (error) {
|
| 29 |
+
console.log(`[ERROR] Failed to connect to MongoDB: ${error.message}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
};
|
| 32 |
|
| 33 |
+
/**
|
| 34 |
+
* Ensure indexes are created for all models
|
| 35 |
+
*/
|
| 36 |
export const ensureIndexes = async () => {
|
| 37 |
try {
|
| 38 |
+
// Mongoose will create indexes automatically when models are used
|
| 39 |
+
// But we can explicitly sync indexes here
|
| 40 |
+
const { User, Patient, Appointment, Warning, DeleteRequest } = await import('../models/index.js');
|
| 41 |
+
|
| 42 |
+
await Promise.all([
|
| 43 |
+
User.syncIndexes(),
|
| 44 |
+
Patient.syncIndexes(),
|
| 45 |
+
Appointment.syncIndexes(),
|
| 46 |
+
Warning.syncIndexes(),
|
| 47 |
+
DeleteRequest.syncIndexes()
|
| 48 |
+
]);
|
| 49 |
+
|
| 50 |
+
console.log('[OK] Database indexes synchronized');
|
| 51 |
} catch (error) {
|
| 52 |
+
console.log(`[WARNING] Could not sync indexes: ${error.message}`);
|
| 53 |
}
|
| 54 |
};
|
| 55 |
|
| 56 |
+
/**
|
| 57 |
+
* Get the database connection
|
| 58 |
+
*/
|
| 59 |
+
export const getDB = () => mongoose.connection.db;
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* Get mongoose connection object
|
| 63 |
+
*/
|
| 64 |
+
export const getConnection = () => mongoose.connection;
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Check if database is connected
|
| 68 |
+
*/
|
| 69 |
+
export const isDBConnected = () => isConnected && mongoose.connection.readyState === 1;
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* Disconnect from database
|
| 73 |
+
*/
|
| 74 |
+
export const disconnectDB = async () => {
|
| 75 |
+
if (isConnected) {
|
| 76 |
+
await mongoose.disconnect();
|
| 77 |
+
isConnected = false;
|
| 78 |
+
console.log('[OK] Disconnected from MongoDB');
|
| 79 |
+
}
|
| 80 |
+
};
|
| 81 |
|
| 82 |
+
// Re-export ObjectId from mongoose for backward compatibility
|
| 83 |
+
export const ObjectId = mongoose.Types.ObjectId;
|
| 84 |
|
| 85 |
+
export const getPatientsCollection = async () => {
|
| 86 |
+
const { Patient } = await import('../models/index.js');
|
| 87 |
+
return Patient;
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
export const getUsersCollection = async () => {
|
| 91 |
+
const { User } = await import('../models/index.js');
|
| 92 |
+
return User;
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
export const getAppointmentsCollection = async () => {
|
| 96 |
+
const { Appointment } = await import('../models/index.js');
|
| 97 |
+
return Appointment;
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
export const getWarningsCollection = async () => {
|
| 101 |
+
const { Warning } = await import('../models/index.js');
|
| 102 |
+
return Warning;
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
export const getDeleteRequestsCollection = async () => {
|
| 106 |
+
const { DeleteRequest } = await import('../models/index.js');
|
| 107 |
+
return DeleteRequest;
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
// Handle connection events
|
| 111 |
+
mongoose.connection.on('connected', () => {
|
| 112 |
+
console.log('[OK] Mongoose connected to database');
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
mongoose.connection.on('error', (err) => {
|
| 116 |
+
console.log(`[ERROR] Mongoose connection error: ${err.message}`);
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
mongoose.connection.on('disconnected', () => {
|
| 120 |
+
isConnected = false;
|
| 121 |
+
console.log('[WARNING] Mongoose disconnected from database');
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
// Handle process termination
|
| 125 |
+
process.on('SIGINT', async () => {
|
| 126 |
+
await disconnectDB();
|
| 127 |
+
process.exit(0);
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
export default {
|
| 131 |
+
connectDB,
|
| 132 |
+
ensureIndexes,
|
| 133 |
+
getDB,
|
| 134 |
+
getConnection,
|
| 135 |
+
isDBConnected,
|
| 136 |
+
disconnectDB,
|
| 137 |
+
ObjectId
|
| 138 |
+
};
|
middleware/auth.js
CHANGED
|
@@ -2,6 +2,7 @@ import { authService } from '../services/authService.js';
|
|
| 2 |
|
| 3 |
// User roles enum
|
| 4 |
export const UserRole = {
|
|
|
|
| 5 |
DOCTOR: 'doctor',
|
| 6 |
PATIENT: 'patient'
|
| 7 |
};
|
|
@@ -87,8 +88,10 @@ export const requireRoles = (allowedRoles) => {
|
|
| 87 |
};
|
| 88 |
|
| 89 |
// Pre-built role middlewares for common use cases
|
| 90 |
-
export const
|
| 91 |
-
export const
|
|
|
|
|
|
|
| 92 |
|
| 93 |
/**
|
| 94 |
* Optional authentication - doesn't fail if no token provided
|
|
@@ -118,4 +121,3 @@ export const optionalAuth = async (req, res, next) => {
|
|
| 118 |
next();
|
| 119 |
}
|
| 120 |
};
|
| 121 |
-
|
|
|
|
| 2 |
|
| 3 |
// User roles enum
|
| 4 |
export const UserRole = {
|
| 5 |
+
ADMIN: 'admin',
|
| 6 |
DOCTOR: 'doctor',
|
| 7 |
PATIENT: 'patient'
|
| 8 |
};
|
|
|
|
| 88 |
};
|
| 89 |
|
| 90 |
// Pre-built role middlewares for common use cases
|
| 91 |
+
export const requireAdmin = requireRoles([UserRole.ADMIN]);
|
| 92 |
+
export const requireDoctor = requireRoles([UserRole.DOCTOR, UserRole.ADMIN]);
|
| 93 |
+
export const requireStaff = requireRoles([UserRole.DOCTOR, UserRole.ADMIN]);
|
| 94 |
+
export const requireAdminOrDoctor = requireRoles([UserRole.ADMIN, UserRole.DOCTOR]);
|
| 95 |
|
| 96 |
/**
|
| 97 |
* Optional authentication - doesn't fail if no token provided
|
|
|
|
| 121 |
next();
|
| 122 |
}
|
| 123 |
};
|
|
|
models/Appointment.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mongoose from 'mongoose';
|
| 2 |
+
|
| 3 |
+
// Appointment status enum
|
| 4 |
+
export const AppointmentStatus = {
|
| 5 |
+
SCHEDULED: 'scheduled',
|
| 6 |
+
CONFIRMED: 'confirmed',
|
| 7 |
+
IN_PROGRESS: 'in_progress',
|
| 8 |
+
COMPLETED: 'completed',
|
| 9 |
+
CANCELLED: 'cancelled',
|
| 10 |
+
NO_SHOW: 'no_show'
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
// Appointment type enum
|
| 14 |
+
export const AppointmentType = {
|
| 15 |
+
CHECKUP: 'checkup',
|
| 16 |
+
FOLLOW_UP: 'follow_up',
|
| 17 |
+
CONSULTATION: 'consultation',
|
| 18 |
+
EMERGENCY: 'emergency',
|
| 19 |
+
PROCEDURE: 'procedure',
|
| 20 |
+
LAB_WORK: 'lab_work',
|
| 21 |
+
OTHER: 'other'
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
const AppointmentSchema = new mongoose.Schema({
|
| 25 |
+
patient_id: {
|
| 26 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 27 |
+
required: [true, 'Patient ID is required'],
|
| 28 |
+
index: true,
|
| 29 |
+
ref: 'Patient'
|
| 30 |
+
},
|
| 31 |
+
doctor_id: {
|
| 32 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 33 |
+
ref: 'User',
|
| 34 |
+
index: true
|
| 35 |
+
},
|
| 36 |
+
date_time: {
|
| 37 |
+
type: String,
|
| 38 |
+
required: [true, 'Date and time is required']
|
| 39 |
+
},
|
| 40 |
+
duration_minutes: {
|
| 41 |
+
type: Number,
|
| 42 |
+
default: 30,
|
| 43 |
+
min: [5, 'Duration must be at least 5 minutes'],
|
| 44 |
+
max: [480, 'Duration cannot exceed 8 hours']
|
| 45 |
+
},
|
| 46 |
+
appointment_type: {
|
| 47 |
+
type: String,
|
| 48 |
+
enum: {
|
| 49 |
+
values: Object.values(AppointmentType),
|
| 50 |
+
message: '{VALUE} is not a valid appointment type'
|
| 51 |
+
},
|
| 52 |
+
default: AppointmentType.CHECKUP
|
| 53 |
+
},
|
| 54 |
+
purpose: {
|
| 55 |
+
type: String,
|
| 56 |
+
required: [true, 'Purpose is required'],
|
| 57 |
+
trim: true
|
| 58 |
+
},
|
| 59 |
+
notes: {
|
| 60 |
+
type: String,
|
| 61 |
+
trim: true
|
| 62 |
+
},
|
| 63 |
+
location: {
|
| 64 |
+
type: String,
|
| 65 |
+
trim: true
|
| 66 |
+
},
|
| 67 |
+
status: {
|
| 68 |
+
type: String,
|
| 69 |
+
enum: {
|
| 70 |
+
values: Object.values(AppointmentStatus),
|
| 71 |
+
message: '{VALUE} is not a valid status'
|
| 72 |
+
},
|
| 73 |
+
default: AppointmentStatus.SCHEDULED
|
| 74 |
+
},
|
| 75 |
+
cancelled_reason: {
|
| 76 |
+
type: String,
|
| 77 |
+
trim: true
|
| 78 |
+
},
|
| 79 |
+
cancelled_at: {
|
| 80 |
+
type: Date
|
| 81 |
+
},
|
| 82 |
+
cancelled_by: {
|
| 83 |
+
type: String
|
| 84 |
+
},
|
| 85 |
+
completion_notes: {
|
| 86 |
+
type: String,
|
| 87 |
+
trim: true
|
| 88 |
+
},
|
| 89 |
+
completed_at: {
|
| 90 |
+
type: Date
|
| 91 |
+
},
|
| 92 |
+
completed_by: {
|
| 93 |
+
type: String
|
| 94 |
+
},
|
| 95 |
+
created_by: {
|
| 96 |
+
type: String,
|
| 97 |
+
required: true
|
| 98 |
+
},
|
| 99 |
+
// Virtual fields to store names (populated from other collections)
|
| 100 |
+
patient_name: {
|
| 101 |
+
type: String
|
| 102 |
+
},
|
| 103 |
+
doctor_name: {
|
| 104 |
+
type: String
|
| 105 |
+
}
|
| 106 |
+
}, {
|
| 107 |
+
timestamps: {
|
| 108 |
+
createdAt: 'created_at',
|
| 109 |
+
updatedAt: 'updated_at'
|
| 110 |
+
}
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
// Compound indexes for efficient queries
|
| 114 |
+
AppointmentSchema.index({ patient_id: 1, date_time: -1 });
|
| 115 |
+
AppointmentSchema.index({ doctor_id: 1, date_time: 1 });
|
| 116 |
+
AppointmentSchema.index({ status: 1, date_time: 1 });
|
| 117 |
+
AppointmentSchema.index({ date_time: 1 });
|
| 118 |
+
|
| 119 |
+
// Method to convert to response object
|
| 120 |
+
AppointmentSchema.methods.toResponse = function() {
|
| 121 |
+
return {
|
| 122 |
+
id: this._id.toString(),
|
| 123 |
+
patient_id: this.patient_id,
|
| 124 |
+
doctor_id: this.doctor_id || null,
|
| 125 |
+
date_time: this.date_time,
|
| 126 |
+
duration_minutes: this.duration_minutes || 30,
|
| 127 |
+
appointment_type: this.appointment_type || 'checkup',
|
| 128 |
+
purpose: this.purpose || '',
|
| 129 |
+
notes: this.notes || null,
|
| 130 |
+
location: this.location || null,
|
| 131 |
+
status: this.status || 'scheduled',
|
| 132 |
+
cancelled_reason: this.cancelled_reason || null,
|
| 133 |
+
cancelled_at: this.cancelled_at || null,
|
| 134 |
+
completion_notes: this.completion_notes || null,
|
| 135 |
+
completed_at: this.completed_at || null,
|
| 136 |
+
created_at: this.created_at,
|
| 137 |
+
updated_at: this.updated_at,
|
| 138 |
+
created_by: this.created_by || '',
|
| 139 |
+
patient_name: this.patient_name || null,
|
| 140 |
+
doctor_name: this.doctor_name || null
|
| 141 |
+
};
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
// Static method to find upcoming appointments
|
| 145 |
+
// doctorId: optional - filter by doctor (for non-admin users)
|
| 146 |
+
AppointmentSchema.statics.findUpcoming = function(limit = 20, doctorId = null) {
|
| 147 |
+
const now = new Date().toISOString();
|
| 148 |
+
const query = {
|
| 149 |
+
date_time: { $gte: now },
|
| 150 |
+
status: { $in: [AppointmentStatus.SCHEDULED, AppointmentStatus.CONFIRMED] }
|
| 151 |
+
};
|
| 152 |
+
|
| 153 |
+
if (doctorId) {
|
| 154 |
+
query.doctor_id = doctorId;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
return this.find(query)
|
| 158 |
+
.sort({ date_time: 1 })
|
| 159 |
+
.limit(limit);
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
// Static method to find appointments by patient
|
| 163 |
+
// doctorId: optional - filter by doctor (for non-admin users)
|
| 164 |
+
AppointmentSchema.statics.findByPatient = function(patientId, includePast = false, doctorId = null) {
|
| 165 |
+
const query = { patient_id: patientId };
|
| 166 |
+
|
| 167 |
+
if (doctorId) {
|
| 168 |
+
query.doctor_id = doctorId;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
if (!includePast) {
|
| 172 |
+
const now = new Date().toISOString();
|
| 173 |
+
query.$or = [
|
| 174 |
+
{ date_time: { $gte: now } },
|
| 175 |
+
{ status: { $in: [AppointmentStatus.SCHEDULED, AppointmentStatus.CONFIRMED] } }
|
| 176 |
+
];
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
return this.find(query).sort({ date_time: -1 });
|
| 180 |
+
};
|
| 181 |
+
|
| 182 |
+
// Static method to find appointments by doctor
|
| 183 |
+
AppointmentSchema.statics.findByDoctor = function(doctorId) {
|
| 184 |
+
return this.find({ doctor_id: doctorId }).sort({ date_time: 1 });
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
// Static method to check for scheduling conflicts
|
| 188 |
+
AppointmentSchema.statics.checkConflicts = async function(doctorId, dateTime, durationMinutes, excludeId = null) {
|
| 189 |
+
const proposedStart = new Date(dateTime.replace('Z', '+00:00'));
|
| 190 |
+
const proposedEnd = new Date(proposedStart.getTime() + durationMinutes * 60000);
|
| 191 |
+
|
| 192 |
+
const query = {
|
| 193 |
+
doctor_id: doctorId,
|
| 194 |
+
status: { $in: [AppointmentStatus.SCHEDULED, AppointmentStatus.CONFIRMED] }
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
if (excludeId) {
|
| 198 |
+
query._id = { $ne: excludeId };
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
const appointments = await this.find(query);
|
| 202 |
+
|
| 203 |
+
const conflicts = [];
|
| 204 |
+
for (const apt of appointments) {
|
| 205 |
+
try {
|
| 206 |
+
const aptStart = new Date(apt.date_time.replace('Z', '+00:00'));
|
| 207 |
+
const aptEnd = new Date(aptStart.getTime() + (apt.duration_minutes || 30) * 60000);
|
| 208 |
+
|
| 209 |
+
// Check for overlap
|
| 210 |
+
if (!(proposedEnd <= aptStart || proposedStart >= aptEnd)) {
|
| 211 |
+
conflicts.push(apt);
|
| 212 |
+
}
|
| 213 |
+
} catch {
|
| 214 |
+
continue;
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
return conflicts;
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
// Static method to get appointment statistics
|
| 222 |
+
// doctorId: optional - filter by doctor (for non-admin users)
|
| 223 |
+
AppointmentSchema.statics.getStats = async function(doctorId = null) {
|
| 224 |
+
const matchStage = doctorId ? { $match: { doctor_id: new mongoose.Types.ObjectId(doctorId) } } : null;
|
| 225 |
+
|
| 226 |
+
const pipeline = [];
|
| 227 |
+
if (matchStage) {
|
| 228 |
+
pipeline.push(matchStage);
|
| 229 |
+
}
|
| 230 |
+
pipeline.push({
|
| 231 |
+
$group: {
|
| 232 |
+
_id: '$status',
|
| 233 |
+
count: { $sum: 1 }
|
| 234 |
+
}
|
| 235 |
+
});
|
| 236 |
+
|
| 237 |
+
const results = await this.aggregate(pipeline);
|
| 238 |
+
|
| 239 |
+
const stats = {
|
| 240 |
+
total: 0,
|
| 241 |
+
by_status: {}
|
| 242 |
+
};
|
| 243 |
+
|
| 244 |
+
for (const r of results) {
|
| 245 |
+
stats.by_status[r._id] = r.count;
|
| 246 |
+
stats.total += r.count;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
return stats;
|
| 250 |
+
};
|
| 251 |
+
|
| 252 |
+
// Method to cancel appointment
|
| 253 |
+
AppointmentSchema.methods.cancel = function(reason, cancelledBy) {
|
| 254 |
+
this.status = AppointmentStatus.CANCELLED;
|
| 255 |
+
this.cancelled_reason = reason;
|
| 256 |
+
this.cancelled_at = new Date();
|
| 257 |
+
this.cancelled_by = cancelledBy;
|
| 258 |
+
return this.save();
|
| 259 |
+
};
|
| 260 |
+
|
| 261 |
+
// Method to complete appointment
|
| 262 |
+
AppointmentSchema.methods.complete = function(notes, completedBy) {
|
| 263 |
+
// Validate that appointment date has passed
|
| 264 |
+
const appointmentDate = new Date(this.date_time);
|
| 265 |
+
const now = new Date();
|
| 266 |
+
|
| 267 |
+
if (appointmentDate > now) {
|
| 268 |
+
throw new Error(`Cannot complete this appointment yet. It is scheduled for ${appointmentDate.toISOString()}.`);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
this.status = AppointmentStatus.COMPLETED;
|
| 272 |
+
this.completion_notes = notes;
|
| 273 |
+
this.completed_at = new Date();
|
| 274 |
+
this.completed_by = completedBy;
|
| 275 |
+
return this.save();
|
| 276 |
+
};
|
| 277 |
+
|
| 278 |
+
const Appointment = mongoose.model('Appointment', AppointmentSchema);
|
| 279 |
+
|
| 280 |
+
export default Appointment;
|
| 281 |
+
|
models/DeleteRequest.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mongoose from 'mongoose';
|
| 2 |
+
|
| 3 |
+
// Delete request status enum
|
| 4 |
+
export const DeleteRequestStatus = {
|
| 5 |
+
PENDING: 'pending',
|
| 6 |
+
APPROVED: 'approved',
|
| 7 |
+
REJECTED: 'rejected'
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
const DeleteRequestSchema = new mongoose.Schema({
|
| 11 |
+
patient_id: {
|
| 12 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 13 |
+
ref: 'Patient',
|
| 14 |
+
required: [true, 'Patient ID is required'],
|
| 15 |
+
index: true
|
| 16 |
+
},
|
| 17 |
+
patient_name: {
|
| 18 |
+
type: String,
|
| 19 |
+
trim: true
|
| 20 |
+
},
|
| 21 |
+
reason: {
|
| 22 |
+
type: String,
|
| 23 |
+
required: [true, 'Reason is required'],
|
| 24 |
+
trim: true,
|
| 25 |
+
minlength: [10, 'Reason must be at least 10 characters']
|
| 26 |
+
},
|
| 27 |
+
status: {
|
| 28 |
+
type: String,
|
| 29 |
+
enum: {
|
| 30 |
+
values: Object.values(DeleteRequestStatus),
|
| 31 |
+
message: '{VALUE} is not a valid status'
|
| 32 |
+
},
|
| 33 |
+
default: DeleteRequestStatus.PENDING
|
| 34 |
+
},
|
| 35 |
+
requested_by: {
|
| 36 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 37 |
+
ref: 'User',
|
| 38 |
+
required: [true, 'Requester ID is required']
|
| 39 |
+
},
|
| 40 |
+
requested_by_name: {
|
| 41 |
+
type: String,
|
| 42 |
+
trim: true
|
| 43 |
+
},
|
| 44 |
+
reviewed_by: {
|
| 45 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 46 |
+
ref: 'User',
|
| 47 |
+
},
|
| 48 |
+
reviewed_by_name: {
|
| 49 |
+
type: String,
|
| 50 |
+
trim: true
|
| 51 |
+
},
|
| 52 |
+
review_notes: {
|
| 53 |
+
type: String,
|
| 54 |
+
trim: true
|
| 55 |
+
},
|
| 56 |
+
reviewed_at: {
|
| 57 |
+
type: Date
|
| 58 |
+
}
|
| 59 |
+
}, {
|
| 60 |
+
timestamps: {
|
| 61 |
+
createdAt: 'created_at',
|
| 62 |
+
updatedAt: 'updated_at'
|
| 63 |
+
}
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
// Indexes for efficient queries
|
| 67 |
+
DeleteRequestSchema.index({ status: 1, created_at: -1 });
|
| 68 |
+
DeleteRequestSchema.index({ requested_by: 1 });
|
| 69 |
+
DeleteRequestSchema.index({ patient_id: 1, status: 1 });
|
| 70 |
+
|
| 71 |
+
// Method to convert to response object
|
| 72 |
+
DeleteRequestSchema.methods.toResponse = function() {
|
| 73 |
+
return {
|
| 74 |
+
id: this._id.toString(),
|
| 75 |
+
patient_id: this.patient_id,
|
| 76 |
+
patient_name: this.patient_name || null,
|
| 77 |
+
reason: this.reason || '',
|
| 78 |
+
status: this.status,
|
| 79 |
+
requested_by: this.requested_by,
|
| 80 |
+
requested_by_name: this.requested_by_name || null,
|
| 81 |
+
reviewed_by: this.reviewed_by || null,
|
| 82 |
+
reviewed_by_name: this.reviewed_by_name || null,
|
| 83 |
+
review_notes: this.review_notes || null,
|
| 84 |
+
created_at: this.created_at,
|
| 85 |
+
updated_at: this.updated_at,
|
| 86 |
+
reviewed_at: this.reviewed_at || null
|
| 87 |
+
};
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
// Static method to find pending requests
|
| 91 |
+
DeleteRequestSchema.statics.findPending = function() {
|
| 92 |
+
return this.find({ status: DeleteRequestStatus.PENDING })
|
| 93 |
+
.sort({ created_at: -1 });
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
// Static method to find requests by user
|
| 97 |
+
DeleteRequestSchema.statics.findByUser = function(userId) {
|
| 98 |
+
return this.find({ requested_by: userId })
|
| 99 |
+
.sort({ created_at: -1 });
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
// Static method to check for existing pending request
|
| 103 |
+
DeleteRequestSchema.statics.hasPendingRequest = function(patientId) {
|
| 104 |
+
return this.findOne({
|
| 105 |
+
patient_id: patientId,
|
| 106 |
+
status: DeleteRequestStatus.PENDING
|
| 107 |
+
});
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
// Method to approve request
|
| 111 |
+
DeleteRequestSchema.methods.approve = function(reviewerId, reviewerName, notes = null) {
|
| 112 |
+
this.status = DeleteRequestStatus.APPROVED;
|
| 113 |
+
this.reviewed_by = reviewerId;
|
| 114 |
+
this.reviewed_by_name = reviewerName;
|
| 115 |
+
this.review_notes = notes;
|
| 116 |
+
this.reviewed_at = new Date();
|
| 117 |
+
return this.save();
|
| 118 |
+
};
|
| 119 |
+
|
| 120 |
+
// Method to reject request
|
| 121 |
+
DeleteRequestSchema.methods.reject = function(reviewerId, reviewerName, notes) {
|
| 122 |
+
if (!notes || notes.trim().length < 5) {
|
| 123 |
+
throw new Error('Rejection reason is required');
|
| 124 |
+
}
|
| 125 |
+
this.status = DeleteRequestStatus.REJECTED;
|
| 126 |
+
this.reviewed_by = reviewerId;
|
| 127 |
+
this.reviewed_by_name = reviewerName;
|
| 128 |
+
this.review_notes = notes;
|
| 129 |
+
this.reviewed_at = new Date();
|
| 130 |
+
return this.save();
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
const DeleteRequest = mongoose.model('DeleteRequest', DeleteRequestSchema, 'delete_requests');
|
| 134 |
+
|
| 135 |
+
export default DeleteRequest;
|
| 136 |
+
|
models/Patient.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mongoose from 'mongoose';
|
| 2 |
+
|
| 3 |
+
const MedicationSchema = new mongoose.Schema({
|
| 4 |
+
name: {
|
| 5 |
+
type: String,
|
| 6 |
+
required: [true, 'Medication name is required'],
|
| 7 |
+
trim: true
|
| 8 |
+
},
|
| 9 |
+
dosage: {
|
| 10 |
+
type: String,
|
| 11 |
+
required: [true, 'Dosage is required'],
|
| 12 |
+
trim: true
|
| 13 |
+
},
|
| 14 |
+
frequency: {
|
| 15 |
+
type: String,
|
| 16 |
+
trim: true
|
| 17 |
+
},
|
| 18 |
+
start_date: {
|
| 19 |
+
type: String,
|
| 20 |
+
required: [true, 'Start date is required']
|
| 21 |
+
},
|
| 22 |
+
end_date: {
|
| 23 |
+
type: String
|
| 24 |
+
},
|
| 25 |
+
prescribing_doctor: {
|
| 26 |
+
type: String,
|
| 27 |
+
trim: true
|
| 28 |
+
},
|
| 29 |
+
notes: {
|
| 30 |
+
type: String,
|
| 31 |
+
trim: true
|
| 32 |
+
}
|
| 33 |
+
}, { _id: false });
|
| 34 |
+
|
| 35 |
+
const MedicalHistorySchema = new mongoose.Schema({
|
| 36 |
+
condition: {
|
| 37 |
+
type: String,
|
| 38 |
+
required: true,
|
| 39 |
+
trim: true
|
| 40 |
+
},
|
| 41 |
+
diagnosed_date: {
|
| 42 |
+
type: String
|
| 43 |
+
},
|
| 44 |
+
status: {
|
| 45 |
+
type: String,
|
| 46 |
+
enum: ['active', 'resolved', 'chronic', 'managed'],
|
| 47 |
+
default: 'active'
|
| 48 |
+
},
|
| 49 |
+
notes: {
|
| 50 |
+
type: String,
|
| 51 |
+
trim: true
|
| 52 |
+
}
|
| 53 |
+
}, { _id: false });
|
| 54 |
+
|
| 55 |
+
const PatientSchema = new mongoose.Schema({
|
| 56 |
+
name: {
|
| 57 |
+
type: String,
|
| 58 |
+
required: [true, 'Patient name is required'],
|
| 59 |
+
trim: true,
|
| 60 |
+
minlength: [1, 'Name cannot be empty']
|
| 61 |
+
},
|
| 62 |
+
age: {
|
| 63 |
+
type: Number,
|
| 64 |
+
required: [true, 'Age is required'],
|
| 65 |
+
min: [0, 'Age cannot be negative'],
|
| 66 |
+
max: [150, 'Age cannot exceed 150']
|
| 67 |
+
},
|
| 68 |
+
gender: {
|
| 69 |
+
type: String,
|
| 70 |
+
required: [true, 'Gender is required'],
|
| 71 |
+
enum: {
|
| 72 |
+
values: ['Male', 'Female', 'Other', 'male', 'female', 'other'],
|
| 73 |
+
message: '{VALUE} is not a valid gender'
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
date_of_birth: {
|
| 77 |
+
type: String
|
| 78 |
+
},
|
| 79 |
+
blood_type: {
|
| 80 |
+
type: String,
|
| 81 |
+
enum: ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-', null, ''],
|
| 82 |
+
},
|
| 83 |
+
allergies: [{
|
| 84 |
+
type: String,
|
| 85 |
+
trim: true
|
| 86 |
+
}],
|
| 87 |
+
medical_history: {
|
| 88 |
+
type: [mongoose.Schema.Types.Mixed], // Can be string or MedicalHistorySchema
|
| 89 |
+
default: []
|
| 90 |
+
},
|
| 91 |
+
medications: {
|
| 92 |
+
type: [MedicationSchema],
|
| 93 |
+
default: []
|
| 94 |
+
},
|
| 95 |
+
emergency_contact: {
|
| 96 |
+
name: String,
|
| 97 |
+
relationship: String,
|
| 98 |
+
phone: String
|
| 99 |
+
},
|
| 100 |
+
insurance: {
|
| 101 |
+
provider: String,
|
| 102 |
+
policy_number: String,
|
| 103 |
+
group_number: String
|
| 104 |
+
},
|
| 105 |
+
notes: {
|
| 106 |
+
type: String,
|
| 107 |
+
trim: true
|
| 108 |
+
},
|
| 109 |
+
phone: {
|
| 110 |
+
type: String,
|
| 111 |
+
trim: true
|
| 112 |
+
},
|
| 113 |
+
email: {
|
| 114 |
+
type: String,
|
| 115 |
+
trim: true,
|
| 116 |
+
lowercase: true
|
| 117 |
+
},
|
| 118 |
+
address: {
|
| 119 |
+
type: String,
|
| 120 |
+
trim: true
|
| 121 |
+
},
|
| 122 |
+
created_by: {
|
| 123 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 124 |
+
ref: 'User'
|
| 125 |
+
},
|
| 126 |
+
updated_by: {
|
| 127 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 128 |
+
ref: 'User'
|
| 129 |
+
}
|
| 130 |
+
}, {
|
| 131 |
+
timestamps: {
|
| 132 |
+
createdAt: 'created_at',
|
| 133 |
+
updatedAt: 'updated_at'
|
| 134 |
+
}
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
// Indexes for faster queries
|
| 138 |
+
PatientSchema.index({ name: 'text' });
|
| 139 |
+
PatientSchema.index({ created_by: 1 });
|
| 140 |
+
PatientSchema.index({ created_at: -1 });
|
| 141 |
+
|
| 142 |
+
// Method to convert to response object
|
| 143 |
+
PatientSchema.methods.toResponse = function() {
|
| 144 |
+
return {
|
| 145 |
+
_id: this._id.toString(),
|
| 146 |
+
name: this.name,
|
| 147 |
+
age: this.age,
|
| 148 |
+
gender: this.gender,
|
| 149 |
+
date_of_birth: this.date_of_birth || null,
|
| 150 |
+
blood_type: this.blood_type || null,
|
| 151 |
+
allergies: this.allergies || [],
|
| 152 |
+
medical_history: this.medical_history || [],
|
| 153 |
+
medications: this.medications || [],
|
| 154 |
+
emergency_contact: this.emergency_contact || null,
|
| 155 |
+
insurance: this.insurance || null,
|
| 156 |
+
notes: this.notes || null,
|
| 157 |
+
phone: this.phone || null,
|
| 158 |
+
email: this.email || null,
|
| 159 |
+
address: this.address || null,
|
| 160 |
+
created_at: this.created_at,
|
| 161 |
+
updated_at: this.updated_at
|
| 162 |
+
};
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
// Static method to search patients by name
|
| 166 |
+
PatientSchema.statics.searchByName = function(searchTerm) {
|
| 167 |
+
return this.find({
|
| 168 |
+
name: { $regex: searchTerm, $options: 'i' }
|
| 169 |
+
});
|
| 170 |
+
};
|
| 171 |
+
|
| 172 |
+
// Static method to find patients with specific condition
|
| 173 |
+
PatientSchema.statics.findByCondition = function(condition) {
|
| 174 |
+
return this.find({
|
| 175 |
+
'medical_history': { $regex: condition, $options: 'i' }
|
| 176 |
+
});
|
| 177 |
+
};
|
| 178 |
+
|
| 179 |
+
// Static method to find patients taking specific medication
|
| 180 |
+
PatientSchema.statics.findByMedication = function(medicationName) {
|
| 181 |
+
return this.find({
|
| 182 |
+
'medications.name': { $regex: medicationName, $options: 'i' }
|
| 183 |
+
});
|
| 184 |
+
};
|
| 185 |
+
|
| 186 |
+
const Patient = mongoose.model('Patient', PatientSchema);
|
| 187 |
+
|
| 188 |
+
export default Patient;
|
| 189 |
+
|
models/User.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mongoose from 'mongoose';
|
| 2 |
+
import bcrypt from 'bcryptjs';
|
| 3 |
+
|
| 4 |
+
const UserSchema = new mongoose.Schema({
|
| 5 |
+
email: {
|
| 6 |
+
type: String,
|
| 7 |
+
required: [true, 'Email is required'],
|
| 8 |
+
unique: true,
|
| 9 |
+
trim: true,
|
| 10 |
+
lowercase: true,
|
| 11 |
+
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please provide a valid email']
|
| 12 |
+
},
|
| 13 |
+
name: {
|
| 14 |
+
type: String,
|
| 15 |
+
required: [true, 'Name is required'],
|
| 16 |
+
trim: true,
|
| 17 |
+
minlength: [2, 'Name must be at least 2 characters']
|
| 18 |
+
},
|
| 19 |
+
hashed_password: {
|
| 20 |
+
type: String,
|
| 21 |
+
required: [true, 'Password is required']
|
| 22 |
+
},
|
| 23 |
+
role: {
|
| 24 |
+
type: String,
|
| 25 |
+
enum: {
|
| 26 |
+
values: ['admin', 'doctor', 'patient'],
|
| 27 |
+
message: '{VALUE} is not a valid role'
|
| 28 |
+
},
|
| 29 |
+
default: 'doctor'
|
| 30 |
+
},
|
| 31 |
+
is_active: {
|
| 32 |
+
type: Boolean,
|
| 33 |
+
default: true
|
| 34 |
+
},
|
| 35 |
+
specialization: {
|
| 36 |
+
type: String,
|
| 37 |
+
trim: true
|
| 38 |
+
},
|
| 39 |
+
phone: {
|
| 40 |
+
type: String,
|
| 41 |
+
trim: true
|
| 42 |
+
},
|
| 43 |
+
address: {
|
| 44 |
+
type: String,
|
| 45 |
+
trim: true
|
| 46 |
+
}
|
| 47 |
+
}, {
|
| 48 |
+
timestamps: {
|
| 49 |
+
createdAt: 'created_at',
|
| 50 |
+
updatedAt: 'updated_at'
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
// Index for faster queries
|
| 55 |
+
UserSchema.index({ role: 1 });
|
| 56 |
+
UserSchema.index({ is_active: 1 });
|
| 57 |
+
|
| 58 |
+
// Hash password before saving
|
| 59 |
+
UserSchema.pre('save', async function(next) {
|
| 60 |
+
// Only hash the password if it has been modified (or is new)
|
| 61 |
+
if (!this.isModified('hashed_password')) return next();
|
| 62 |
+
|
| 63 |
+
// Check if the password is already hashed (bcrypt hashes start with $2)
|
| 64 |
+
if (this.hashed_password.startsWith('$2')) return next();
|
| 65 |
+
|
| 66 |
+
try {
|
| 67 |
+
const salt = await bcrypt.genSalt(10);
|
| 68 |
+
this.hashed_password = await bcrypt.hash(this.hashed_password, salt);
|
| 69 |
+
next();
|
| 70 |
+
} catch (error) {
|
| 71 |
+
next(error);
|
| 72 |
+
}
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
// Method to check password
|
| 76 |
+
UserSchema.methods.comparePassword = async function(candidatePassword) {
|
| 77 |
+
return bcrypt.compare(candidatePassword, this.hashed_password);
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
// Method to convert to response object (exclude sensitive data)
|
| 81 |
+
UserSchema.methods.toResponse = function() {
|
| 82 |
+
return {
|
| 83 |
+
id: this._id.toString(),
|
| 84 |
+
email: this.email,
|
| 85 |
+
name: this.name,
|
| 86 |
+
role: this.role,
|
| 87 |
+
is_active: this.is_active,
|
| 88 |
+
specialization: this.specialization,
|
| 89 |
+
phone: this.phone,
|
| 90 |
+
created_at: this.created_at
|
| 91 |
+
};
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
// Static method to find by email
|
| 95 |
+
UserSchema.statics.findByEmail = function(email) {
|
| 96 |
+
return this.findOne({ email: email.toLowerCase() });
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
// Static method to find active doctors
|
| 100 |
+
UserSchema.statics.findActiveDoctors = function() {
|
| 101 |
+
return this.find({ role: 'doctor', is_active: true });
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
// Static method to find active admins
|
| 105 |
+
UserSchema.statics.findActiveAdmins = function() {
|
| 106 |
+
return this.find({ role: 'admin', is_active: true });
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
const User = mongoose.model('User', UserSchema);
|
| 110 |
+
|
| 111 |
+
export default User;
|
| 112 |
+
|
models/Warning.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mongoose from 'mongoose';
|
| 2 |
+
|
| 3 |
+
// Warning severity enum
|
| 4 |
+
export const WarningSeverity = {
|
| 5 |
+
LOW: 'low',
|
| 6 |
+
MEDIUM: 'medium',
|
| 7 |
+
HIGH: 'high',
|
| 8 |
+
CRITICAL: 'critical'
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
// Warning type enum
|
| 12 |
+
export const WarningType = {
|
| 13 |
+
DRUG_INTERACTION: 'drug_interaction',
|
| 14 |
+
ALLERGY: 'allergy',
|
| 15 |
+
CONTRAINDICATION: 'contraindication',
|
| 16 |
+
DOSAGE: 'dosage',
|
| 17 |
+
LAB_RESULT: 'lab_result',
|
| 18 |
+
VITAL_SIGN: 'vital_sign',
|
| 19 |
+
COMPLIANCE: 'compliance',
|
| 20 |
+
FOLLOW_UP: 'follow_up',
|
| 21 |
+
GENERAL: 'general'
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
const WarningSchema = new mongoose.Schema({
|
| 25 |
+
patient_id: {
|
| 26 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 27 |
+
ref: 'Patient',
|
| 28 |
+
required: [true, 'Patient ID is required'],
|
| 29 |
+
index: true
|
| 30 |
+
},
|
| 31 |
+
warning_type: {
|
| 32 |
+
type: String,
|
| 33 |
+
enum: {
|
| 34 |
+
values: Object.values(WarningType),
|
| 35 |
+
message: '{VALUE} is not a valid warning type'
|
| 36 |
+
},
|
| 37 |
+
required: [true, 'Warning type is required']
|
| 38 |
+
},
|
| 39 |
+
severity: {
|
| 40 |
+
type: String,
|
| 41 |
+
enum: {
|
| 42 |
+
values: Object.values(WarningSeverity),
|
| 43 |
+
message: '{VALUE} is not a valid severity level'
|
| 44 |
+
},
|
| 45 |
+
default: WarningSeverity.MEDIUM
|
| 46 |
+
},
|
| 47 |
+
title: {
|
| 48 |
+
type: String,
|
| 49 |
+
required: [true, 'Warning title is required'],
|
| 50 |
+
trim: true
|
| 51 |
+
},
|
| 52 |
+
description: {
|
| 53 |
+
type: String,
|
| 54 |
+
required: [true, 'Warning description is required'],
|
| 55 |
+
trim: true
|
| 56 |
+
},
|
| 57 |
+
details: {
|
| 58 |
+
type: mongoose.Schema.Types.Mixed,
|
| 59 |
+
default: {}
|
| 60 |
+
},
|
| 61 |
+
recommendations: [{
|
| 62 |
+
type: String,
|
| 63 |
+
trim: true
|
| 64 |
+
}],
|
| 65 |
+
is_acknowledged: {
|
| 66 |
+
type: Boolean,
|
| 67 |
+
default: false
|
| 68 |
+
},
|
| 69 |
+
acknowledged_by: {
|
| 70 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 71 |
+
ref: 'User',
|
| 72 |
+
},
|
| 73 |
+
acknowledged_at: {
|
| 74 |
+
type: Date
|
| 75 |
+
},
|
| 76 |
+
acknowledgment_notes: {
|
| 77 |
+
type: String,
|
| 78 |
+
trim: true
|
| 79 |
+
},
|
| 80 |
+
is_resolved: {
|
| 81 |
+
type: Boolean,
|
| 82 |
+
default: false
|
| 83 |
+
},
|
| 84 |
+
resolved_by: {
|
| 85 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 86 |
+
ref: 'User',
|
| 87 |
+
},
|
| 88 |
+
resolved_at: {
|
| 89 |
+
type: Date
|
| 90 |
+
},
|
| 91 |
+
resolution_notes: {
|
| 92 |
+
type: String,
|
| 93 |
+
trim: true
|
| 94 |
+
},
|
| 95 |
+
source: {
|
| 96 |
+
type: String,
|
| 97 |
+
enum: ['ai_analysis', 'manual', 'system', 'lab_integration'],
|
| 98 |
+
default: 'ai_analysis'
|
| 99 |
+
},
|
| 100 |
+
related_medications: [{
|
| 101 |
+
type: String
|
| 102 |
+
}],
|
| 103 |
+
related_conditions: [{
|
| 104 |
+
type: String
|
| 105 |
+
}],
|
| 106 |
+
expires_at: {
|
| 107 |
+
type: Date
|
| 108 |
+
},
|
| 109 |
+
created_by: {
|
| 110 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 111 |
+
ref: 'User',
|
| 112 |
+
}
|
| 113 |
+
}, {
|
| 114 |
+
timestamps: {
|
| 115 |
+
createdAt: 'created_at',
|
| 116 |
+
updatedAt: 'updated_at'
|
| 117 |
+
}
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
// Indexes for efficient queries
|
| 121 |
+
WarningSchema.index({ patient_id: 1, is_acknowledged: 1 });
|
| 122 |
+
WarningSchema.index({ severity: 1 });
|
| 123 |
+
WarningSchema.index({ warning_type: 1 });
|
| 124 |
+
WarningSchema.index({ created_at: -1 });
|
| 125 |
+
WarningSchema.index({ is_acknowledged: 1, created_at: -1 });
|
| 126 |
+
|
| 127 |
+
// Method to convert to response object
|
| 128 |
+
WarningSchema.methods.toResponse = function() {
|
| 129 |
+
return {
|
| 130 |
+
id: this._id.toString(),
|
| 131 |
+
patient_id: this.patient_id,
|
| 132 |
+
warning_type: this.warning_type,
|
| 133 |
+
severity: this.severity,
|
| 134 |
+
title: this.title,
|
| 135 |
+
description: this.description,
|
| 136 |
+
details: this.details || {},
|
| 137 |
+
recommendations: this.recommendations || [],
|
| 138 |
+
is_acknowledged: this.is_acknowledged,
|
| 139 |
+
acknowledged_by: this.acknowledged_by || null,
|
| 140 |
+
acknowledged_at: this.acknowledged_at || null,
|
| 141 |
+
acknowledgment_notes: this.acknowledgment_notes || null,
|
| 142 |
+
is_resolved: this.is_resolved,
|
| 143 |
+
resolved_by: this.resolved_by || null,
|
| 144 |
+
resolved_at: this.resolved_at || null,
|
| 145 |
+
resolution_notes: this.resolution_notes || null,
|
| 146 |
+
source: this.source,
|
| 147 |
+
related_medications: this.related_medications || [],
|
| 148 |
+
related_conditions: this.related_conditions || [],
|
| 149 |
+
expires_at: this.expires_at || null,
|
| 150 |
+
created_at: this.created_at,
|
| 151 |
+
updated_at: this.updated_at
|
| 152 |
+
};
|
| 153 |
+
};
|
| 154 |
+
|
| 155 |
+
// Static method to find unacknowledged warnings
|
| 156 |
+
WarningSchema.statics.findUnacknowledged = function(limit = 50) {
|
| 157 |
+
return this.find({ is_acknowledged: false })
|
| 158 |
+
.sort({ severity: -1, created_at: -1 })
|
| 159 |
+
.limit(limit);
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
// Static method to find warnings by patient
|
| 163 |
+
WarningSchema.statics.findByPatient = function(patientId, includeAcknowledged = false) {
|
| 164 |
+
const query = { patient_id: patientId };
|
| 165 |
+
if (!includeAcknowledged) {
|
| 166 |
+
query.is_acknowledged = false;
|
| 167 |
+
}
|
| 168 |
+
return this.find(query).sort({ severity: -1, created_at: -1 });
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
+
// Static method to find high severity warnings
|
| 172 |
+
WarningSchema.statics.findHighSeverity = function() {
|
| 173 |
+
return this.find({
|
| 174 |
+
severity: { $in: [WarningSeverity.HIGH, WarningSeverity.CRITICAL] },
|
| 175 |
+
is_acknowledged: false
|
| 176 |
+
}).sort({ created_at: -1 });
|
| 177 |
+
};
|
| 178 |
+
|
| 179 |
+
// Static method to get warning statistics
|
| 180 |
+
WarningSchema.statics.getStats = async function() {
|
| 181 |
+
const [bySeverity, byType, total, unacknowledged] = await Promise.all([
|
| 182 |
+
this.aggregate([
|
| 183 |
+
{ $group: { _id: '$severity', count: { $sum: 1 } } }
|
| 184 |
+
]),
|
| 185 |
+
this.aggregate([
|
| 186 |
+
{ $group: { _id: '$warning_type', count: { $sum: 1 } } }
|
| 187 |
+
]),
|
| 188 |
+
this.countDocuments(),
|
| 189 |
+
this.countDocuments({ is_acknowledged: false })
|
| 190 |
+
]);
|
| 191 |
+
|
| 192 |
+
return {
|
| 193 |
+
total,
|
| 194 |
+
unacknowledged,
|
| 195 |
+
by_severity: bySeverity.reduce((acc, item) => {
|
| 196 |
+
acc[item._id] = item.count;
|
| 197 |
+
return acc;
|
| 198 |
+
}, {}),
|
| 199 |
+
by_type: byType.reduce((acc, item) => {
|
| 200 |
+
acc[item._id] = item.count;
|
| 201 |
+
return acc;
|
| 202 |
+
}, {})
|
| 203 |
+
};
|
| 204 |
+
};
|
| 205 |
+
|
| 206 |
+
// Method to acknowledge warning
|
| 207 |
+
WarningSchema.methods.acknowledge = function(userId, notes) {
|
| 208 |
+
this.is_acknowledged = true;
|
| 209 |
+
this.acknowledged_by = userId;
|
| 210 |
+
this.acknowledged_at = new Date();
|
| 211 |
+
this.acknowledgment_notes = notes;
|
| 212 |
+
return this.save();
|
| 213 |
+
};
|
| 214 |
+
|
| 215 |
+
// Method to resolve warning
|
| 216 |
+
WarningSchema.methods.resolve = function(userId, notes) {
|
| 217 |
+
this.is_resolved = true;
|
| 218 |
+
this.resolved_by = userId;
|
| 219 |
+
this.resolved_at = new Date();
|
| 220 |
+
this.resolution_notes = notes;
|
| 221 |
+
if (!this.is_acknowledged) {
|
| 222 |
+
this.is_acknowledged = true;
|
| 223 |
+
this.acknowledged_by = userId;
|
| 224 |
+
this.acknowledged_at = new Date();
|
| 225 |
+
}
|
| 226 |
+
return this.save();
|
| 227 |
+
};
|
| 228 |
+
|
| 229 |
+
const Warning = mongoose.model('Warning', WarningSchema, 'clinical_warnings');
|
| 230 |
+
|
| 231 |
+
export default Warning;
|
| 232 |
+
|
models/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Export all models from a single file for easy imports
|
| 2 |
+
import User from './User.js';
|
| 3 |
+
import Patient from './Patient.js';
|
| 4 |
+
import Appointment, { AppointmentStatus, AppointmentType } from './Appointment.js';
|
| 5 |
+
import Warning, { WarningSeverity, WarningType } from './Warning.js';
|
| 6 |
+
import DeleteRequest, { DeleteRequestStatus } from './DeleteRequest.js';
|
| 7 |
+
|
| 8 |
+
export {
|
| 9 |
+
User,
|
| 10 |
+
Patient,
|
| 11 |
+
Appointment,
|
| 12 |
+
AppointmentStatus,
|
| 13 |
+
AppointmentType,
|
| 14 |
+
Warning,
|
| 15 |
+
WarningSeverity,
|
| 16 |
+
WarningType,
|
| 17 |
+
DeleteRequest,
|
| 18 |
+
DeleteRequestStatus
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
export default {
|
| 22 |
+
User,
|
| 23 |
+
Patient,
|
| 24 |
+
Appointment,
|
| 25 |
+
Warning,
|
| 26 |
+
DeleteRequest
|
| 27 |
+
};
|
| 28 |
+
|
routes/appointments.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import express from 'express';
|
| 2 |
-
import
|
| 3 |
import { appointmentService } from '../services/appointmentService.js';
|
| 4 |
import { getCurrentUser, requireStaff } from '../middleware/auth.js';
|
| 5 |
|
|
@@ -9,13 +9,14 @@ const router = express.Router();
|
|
| 9 |
* Validate ObjectId format
|
| 10 |
*/
|
| 11 |
const isValidObjectId = (id) => {
|
| 12 |
-
|
| 13 |
-
return ObjectId.isValid(id) && new ObjectId(id).toString() === id;
|
| 14 |
-
} catch {
|
| 15 |
-
return false;
|
| 16 |
-
}
|
| 17 |
};
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
/**
|
| 20 |
* POST /api/appointments
|
| 21 |
* Create a new appointment
|
|
@@ -30,6 +31,13 @@ router.post('/appointments', requireStaff, async (req, res) => {
|
|
| 30 |
appointmentData.doctor_id = userId;
|
| 31 |
}
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
// Check for conflicts if doctor is assigned
|
| 34 |
if (appointmentData.doctor_id) {
|
| 35 |
const conflicts = await appointmentService.checkConflicts(
|
|
@@ -51,6 +59,10 @@ router.post('/appointments', requireStaff, async (req, res) => {
|
|
| 51 |
const appointment = await appointmentService.createAppointment(appointmentData, userId);
|
| 52 |
res.status(201).json(appointment);
|
| 53 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
res.status(500).json({ detail: `Error creating appointment: ${error.message}` });
|
| 55 |
}
|
| 56 |
});
|
|
@@ -58,14 +70,24 @@ router.post('/appointments', requireStaff, async (req, res) => {
|
|
| 58 |
/**
|
| 59 |
* GET /api/appointments
|
| 60 |
* List appointments with optional filters
|
|
|
|
|
|
|
| 61 |
*/
|
| 62 |
router.get('/appointments', getCurrentUser, async (req, res) => {
|
| 63 |
try {
|
| 64 |
const { patient_id, doctor_id, status, from_date, to_date, limit = 50 } = req.query;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
const appointments = await appointmentService.listAppointments({
|
| 67 |
patientId: patient_id,
|
| 68 |
-
doctorId:
|
| 69 |
status,
|
| 70 |
fromDate: from_date,
|
| 71 |
toDate: to_date,
|
|
@@ -81,12 +103,20 @@ router.get('/appointments', getCurrentUser, async (req, res) => {
|
|
| 81 |
/**
|
| 82 |
* GET /api/appointments/upcoming
|
| 83 |
* Get upcoming appointments
|
|
|
|
|
|
|
| 84 |
*/
|
| 85 |
router.get('/appointments/upcoming', getCurrentUser, async (req, res) => {
|
| 86 |
try {
|
| 87 |
const { limit = 20 } = req.query;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
const appointments = await appointmentService.getUpcomingAppointments(
|
| 89 |
-
Math.min(parseInt(limit), 100)
|
|
|
|
| 90 |
);
|
| 91 |
res.json(appointments);
|
| 92 |
} catch (error) {
|
|
@@ -97,10 +127,15 @@ router.get('/appointments/upcoming', getCurrentUser, async (req, res) => {
|
|
| 97 |
/**
|
| 98 |
* GET /api/appointments/stats
|
| 99 |
* Get appointment statistics
|
|
|
|
|
|
|
| 100 |
*/
|
| 101 |
router.get('/appointments/stats', getCurrentUser, async (req, res) => {
|
| 102 |
try {
|
| 103 |
-
const
|
|
|
|
|
|
|
|
|
|
| 104 |
res.json(stats);
|
| 105 |
} catch (error) {
|
| 106 |
res.status(500).json({ detail: `Error fetching stats: ${error.message}` });
|
|
@@ -110,10 +145,13 @@ router.get('/appointments/stats', getCurrentUser, async (req, res) => {
|
|
| 110 |
/**
|
| 111 |
* GET /api/appointments/:appointmentId
|
| 112 |
* Get a specific appointment
|
|
|
|
|
|
|
| 113 |
*/
|
| 114 |
router.get('/appointments/:appointmentId', getCurrentUser, async (req, res) => {
|
| 115 |
try {
|
| 116 |
const { appointmentId } = req.params;
|
|
|
|
| 117 |
|
| 118 |
if (!isValidObjectId(appointmentId)) {
|
| 119 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
|
@@ -125,6 +163,11 @@ router.get('/appointments/:appointmentId', getCurrentUser, async (req, res) => {
|
|
| 125 |
return res.status(404).json({ detail: 'Appointment not found' });
|
| 126 |
}
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
res.json(appointment);
|
| 129 |
} catch (error) {
|
| 130 |
res.status(500).json({ detail: `Error fetching appointment: ${error.message}` });
|
|
@@ -134,11 +177,14 @@ router.get('/appointments/:appointmentId', getCurrentUser, async (req, res) => {
|
|
| 134 |
/**
|
| 135 |
* PUT /api/appointments/:appointmentId
|
| 136 |
* Update an appointment
|
|
|
|
|
|
|
| 137 |
*/
|
| 138 |
router.put('/appointments/:appointmentId', requireStaff, async (req, res) => {
|
| 139 |
try {
|
| 140 |
const { appointmentId } = req.params;
|
| 141 |
const updateData = req.body;
|
|
|
|
| 142 |
|
| 143 |
if (!isValidObjectId(appointmentId)) {
|
| 144 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
|
@@ -150,6 +196,11 @@ router.put('/appointments/:appointmentId', requireStaff, async (req, res) => {
|
|
| 150 |
return res.status(404).json({ detail: 'Appointment not found' });
|
| 151 |
}
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
if (updateData.date_time && existing.doctor_id) {
|
| 154 |
const conflicts = await appointmentService.checkConflicts(
|
| 155 |
existing.doctor_id,
|
|
@@ -183,17 +234,34 @@ router.put('/appointments/:appointmentId', requireStaff, async (req, res) => {
|
|
| 183 |
/**
|
| 184 |
* POST /api/appointments/:appointmentId/cancel
|
| 185 |
* Cancel an appointment
|
|
|
|
|
|
|
| 186 |
*/
|
| 187 |
router.post('/appointments/:appointmentId/cancel', requireStaff, async (req, res) => {
|
| 188 |
try {
|
| 189 |
const { appointmentId } = req.params;
|
| 190 |
const { reason } = req.body || {};
|
|
|
|
| 191 |
|
| 192 |
if (!isValidObjectId(appointmentId)) {
|
| 193 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 194 |
}
|
| 195 |
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
if (!appointment) {
|
| 199 |
return res.status(404).json({ detail: 'Appointment not found' });
|
|
@@ -208,23 +276,51 @@ router.post('/appointments/:appointmentId/cancel', requireStaff, async (req, res
|
|
| 208 |
/**
|
| 209 |
* POST /api/appointments/:appointmentId/complete
|
| 210 |
* Mark an appointment as completed
|
|
|
|
|
|
|
| 211 |
*/
|
| 212 |
router.post('/appointments/:appointmentId/complete', requireStaff, async (req, res) => {
|
| 213 |
try {
|
| 214 |
const { appointmentId } = req.params;
|
| 215 |
const { notes } = req.body || {};
|
|
|
|
| 216 |
|
| 217 |
if (!isValidObjectId(appointmentId)) {
|
| 218 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 219 |
}
|
| 220 |
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
return res.status(404).json({ detail: 'Appointment not found' });
|
| 225 |
}
|
| 226 |
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
} catch (error) {
|
| 229 |
res.status(500).json({ detail: `Error completing appointment: ${error.message}` });
|
| 230 |
}
|
|
@@ -233,15 +329,28 @@ router.post('/appointments/:appointmentId/complete', requireStaff, async (req, r
|
|
| 233 |
/**
|
| 234 |
* GET /api/appointments/:appointmentId/summary
|
| 235 |
* Generate an AI-powered pre-appointment summary
|
|
|
|
|
|
|
| 236 |
*/
|
| 237 |
router.get('/appointments/:appointmentId/summary', requireStaff, async (req, res) => {
|
| 238 |
try {
|
| 239 |
const { appointmentId } = req.params;
|
|
|
|
| 240 |
|
| 241 |
if (!isValidObjectId(appointmentId)) {
|
| 242 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 243 |
}
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
const summary = await appointmentService.generatePreAppointmentSummary(appointmentId);
|
| 246 |
|
| 247 |
if (summary.error) {
|
|
@@ -257,19 +366,26 @@ router.get('/appointments/:appointmentId/summary', requireStaff, async (req, res
|
|
| 257 |
/**
|
| 258 |
* GET /api/patients/:patientId/appointments
|
| 259 |
* Get all appointments for a specific patient
|
|
|
|
|
|
|
| 260 |
*/
|
| 261 |
router.get('/patients/:patientId/appointments', getCurrentUser, async (req, res) => {
|
| 262 |
try {
|
| 263 |
const { patientId } = req.params;
|
| 264 |
const { include_past = 'false' } = req.query;
|
|
|
|
| 265 |
|
| 266 |
if (!isValidObjectId(patientId)) {
|
| 267 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 268 |
}
|
| 269 |
|
|
|
|
|
|
|
|
|
|
| 270 |
const appointments = await appointmentService.getPatientAppointments(
|
| 271 |
patientId,
|
| 272 |
-
include_past === 'true'
|
|
|
|
| 273 |
);
|
| 274 |
|
| 275 |
res.json(appointments);
|
|
@@ -279,4 +395,3 @@ router.get('/patients/:patientId/appointments', getCurrentUser, async (req, res)
|
|
| 279 |
});
|
| 280 |
|
| 281 |
export default router;
|
| 282 |
-
|
|
|
|
| 1 |
import express from 'express';
|
| 2 |
+
import mongoose from 'mongoose';
|
| 3 |
import { appointmentService } from '../services/appointmentService.js';
|
| 4 |
import { getCurrentUser, requireStaff } from '../middleware/auth.js';
|
| 5 |
|
|
|
|
| 9 |
* Validate ObjectId format
|
| 10 |
*/
|
| 11 |
const isValidObjectId = (id) => {
|
| 12 |
+
return mongoose.Types.ObjectId.isValid(id);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
};
|
| 14 |
|
| 15 |
+
/**
|
| 16 |
+
* Check if user is admin
|
| 17 |
+
*/
|
| 18 |
+
const isAdmin = (user) => user?.role === 'admin';
|
| 19 |
+
|
| 20 |
/**
|
| 21 |
* POST /api/appointments
|
| 22 |
* Create a new appointment
|
|
|
|
| 31 |
appointmentData.doctor_id = userId;
|
| 32 |
}
|
| 33 |
|
| 34 |
+
// Non-admin users can only create appointments for themselves as doctor
|
| 35 |
+
if (!isAdmin(req.user) && appointmentData.doctor_id !== userId) {
|
| 36 |
+
return res.status(403).json({
|
| 37 |
+
detail: 'You can only create appointments for yourself as the doctor'
|
| 38 |
+
});
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
// Check for conflicts if doctor is assigned
|
| 42 |
if (appointmentData.doctor_id) {
|
| 43 |
const conflicts = await appointmentService.checkConflicts(
|
|
|
|
| 59 |
const appointment = await appointmentService.createAppointment(appointmentData, userId);
|
| 60 |
res.status(201).json(appointment);
|
| 61 |
} catch (error) {
|
| 62 |
+
if (error.name === 'ValidationError') {
|
| 63 |
+
const messages = Object.values(error.errors).map(e => e.message);
|
| 64 |
+
return res.status(400).json({ detail: messages.join(', ') });
|
| 65 |
+
}
|
| 66 |
res.status(500).json({ detail: `Error creating appointment: ${error.message}` });
|
| 67 |
}
|
| 68 |
});
|
|
|
|
| 70 |
/**
|
| 71 |
* GET /api/appointments
|
| 72 |
* List appointments with optional filters
|
| 73 |
+
* - Admin: Can see all appointments
|
| 74 |
+
* - Doctor: Can only see their own appointments
|
| 75 |
*/
|
| 76 |
router.get('/appointments', getCurrentUser, async (req, res) => {
|
| 77 |
try {
|
| 78 |
const { patient_id, doctor_id, status, from_date, to_date, limit = 50 } = req.query;
|
| 79 |
+
const userId = req.user._id.toString();
|
| 80 |
+
|
| 81 |
+
// Determine doctor filter based on role
|
| 82 |
+
let doctorFilter = doctor_id;
|
| 83 |
+
if (!isAdmin(req.user)) {
|
| 84 |
+
// Non-admin users can only see their own appointments
|
| 85 |
+
doctorFilter = userId;
|
| 86 |
+
}
|
| 87 |
|
| 88 |
const appointments = await appointmentService.listAppointments({
|
| 89 |
patientId: patient_id,
|
| 90 |
+
doctorId: doctorFilter,
|
| 91 |
status,
|
| 92 |
fromDate: from_date,
|
| 93 |
toDate: to_date,
|
|
|
|
| 103 |
/**
|
| 104 |
* GET /api/appointments/upcoming
|
| 105 |
* Get upcoming appointments
|
| 106 |
+
* - Admin: Can see all upcoming appointments
|
| 107 |
+
* - Doctor: Can only see their own upcoming appointments
|
| 108 |
*/
|
| 109 |
router.get('/appointments/upcoming', getCurrentUser, async (req, res) => {
|
| 110 |
try {
|
| 111 |
const { limit = 20 } = req.query;
|
| 112 |
+
const userId = req.user._id.toString();
|
| 113 |
+
|
| 114 |
+
// Pass doctor filter for non-admin users
|
| 115 |
+
const doctorFilter = isAdmin(req.user) ? null : userId;
|
| 116 |
+
|
| 117 |
const appointments = await appointmentService.getUpcomingAppointments(
|
| 118 |
+
Math.min(parseInt(limit), 100),
|
| 119 |
+
doctorFilter
|
| 120 |
);
|
| 121 |
res.json(appointments);
|
| 122 |
} catch (error) {
|
|
|
|
| 127 |
/**
|
| 128 |
* GET /api/appointments/stats
|
| 129 |
* Get appointment statistics
|
| 130 |
+
* - Admin: Stats for all appointments
|
| 131 |
+
* - Doctor: Stats only for their appointments
|
| 132 |
*/
|
| 133 |
router.get('/appointments/stats', getCurrentUser, async (req, res) => {
|
| 134 |
try {
|
| 135 |
+
const userId = req.user._id.toString();
|
| 136 |
+
const doctorFilter = isAdmin(req.user) ? null : userId;
|
| 137 |
+
|
| 138 |
+
const stats = await appointmentService.getAppointmentStats(doctorFilter);
|
| 139 |
res.json(stats);
|
| 140 |
} catch (error) {
|
| 141 |
res.status(500).json({ detail: `Error fetching stats: ${error.message}` });
|
|
|
|
| 145 |
/**
|
| 146 |
* GET /api/appointments/:appointmentId
|
| 147 |
* Get a specific appointment
|
| 148 |
+
* - Admin: Can access any appointment
|
| 149 |
+
* - Doctor: Can only access their own appointments
|
| 150 |
*/
|
| 151 |
router.get('/appointments/:appointmentId', getCurrentUser, async (req, res) => {
|
| 152 |
try {
|
| 153 |
const { appointmentId } = req.params;
|
| 154 |
+
const userId = req.user._id.toString();
|
| 155 |
|
| 156 |
if (!isValidObjectId(appointmentId)) {
|
| 157 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
|
|
|
| 163 |
return res.status(404).json({ detail: 'Appointment not found' });
|
| 164 |
}
|
| 165 |
|
| 166 |
+
// Check access for non-admin users
|
| 167 |
+
if (!isAdmin(req.user) && appointment.doctor_id !== userId) {
|
| 168 |
+
return res.status(403).json({ detail: 'Access denied. You can only view your own appointments.' });
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
res.json(appointment);
|
| 172 |
} catch (error) {
|
| 173 |
res.status(500).json({ detail: `Error fetching appointment: ${error.message}` });
|
|
|
|
| 177 |
/**
|
| 178 |
* PUT /api/appointments/:appointmentId
|
| 179 |
* Update an appointment
|
| 180 |
+
* - Admin: Can update any appointment
|
| 181 |
+
* - Doctor: Can only update their own appointments
|
| 182 |
*/
|
| 183 |
router.put('/appointments/:appointmentId', requireStaff, async (req, res) => {
|
| 184 |
try {
|
| 185 |
const { appointmentId } = req.params;
|
| 186 |
const updateData = req.body;
|
| 187 |
+
const userId = req.user._id.toString();
|
| 188 |
|
| 189 |
if (!isValidObjectId(appointmentId)) {
|
| 190 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
|
|
|
| 196 |
return res.status(404).json({ detail: 'Appointment not found' });
|
| 197 |
}
|
| 198 |
|
| 199 |
+
// Check access for non-admin users
|
| 200 |
+
if (!isAdmin(req.user) && existing.doctor_id !== userId) {
|
| 201 |
+
return res.status(403).json({ detail: 'Access denied. You can only update your own appointments.' });
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
if (updateData.date_time && existing.doctor_id) {
|
| 205 |
const conflicts = await appointmentService.checkConflicts(
|
| 206 |
existing.doctor_id,
|
|
|
|
| 234 |
/**
|
| 235 |
* POST /api/appointments/:appointmentId/cancel
|
| 236 |
* Cancel an appointment
|
| 237 |
+
* - Admin: Can cancel any appointment
|
| 238 |
+
* - Doctor: Can only cancel their own appointments
|
| 239 |
*/
|
| 240 |
router.post('/appointments/:appointmentId/cancel', requireStaff, async (req, res) => {
|
| 241 |
try {
|
| 242 |
const { appointmentId } = req.params;
|
| 243 |
const { reason } = req.body || {};
|
| 244 |
+
const userId = req.user._id.toString();
|
| 245 |
|
| 246 |
if (!isValidObjectId(appointmentId)) {
|
| 247 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 248 |
}
|
| 249 |
|
| 250 |
+
// Check access for non-admin users
|
| 251 |
+
const existing = await appointmentService.getAppointment(appointmentId);
|
| 252 |
+
if (!existing) {
|
| 253 |
+
return res.status(404).json({ detail: 'Appointment not found' });
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
if (!isAdmin(req.user) && existing.doctor_id !== userId) {
|
| 257 |
+
return res.status(403).json({ detail: 'Access denied. You can only cancel your own appointments.' });
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
const appointment = await appointmentService.cancelAppointment(
|
| 261 |
+
appointmentId,
|
| 262 |
+
reason,
|
| 263 |
+
userId
|
| 264 |
+
);
|
| 265 |
|
| 266 |
if (!appointment) {
|
| 267 |
return res.status(404).json({ detail: 'Appointment not found' });
|
|
|
|
| 276 |
/**
|
| 277 |
* POST /api/appointments/:appointmentId/complete
|
| 278 |
* Mark an appointment as completed
|
| 279 |
+
* - Admin: Can complete any appointment
|
| 280 |
+
* - Doctor: Can only complete their own appointments
|
| 281 |
*/
|
| 282 |
router.post('/appointments/:appointmentId/complete', requireStaff, async (req, res) => {
|
| 283 |
try {
|
| 284 |
const { appointmentId } = req.params;
|
| 285 |
const { notes } = req.body || {};
|
| 286 |
+
const userId = req.user._id.toString();
|
| 287 |
|
| 288 |
if (!isValidObjectId(appointmentId)) {
|
| 289 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 290 |
}
|
| 291 |
|
| 292 |
+
// Check access for non-admin users
|
| 293 |
+
const existing = await appointmentService.getAppointment(appointmentId);
|
| 294 |
+
if (!existing) {
|
| 295 |
+
return res.status(404).json({ detail: 'Appointment not found' });
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
if (!isAdmin(req.user) && existing.doctor_id !== userId) {
|
| 299 |
+
return res.status(403).json({ detail: 'Access denied. You can only complete your own appointments.' });
|
| 300 |
+
}
|
| 301 |
|
| 302 |
+
const result = await appointmentService.completeAppointment(
|
| 303 |
+
appointmentId,
|
| 304 |
+
notes,
|
| 305 |
+
userId
|
| 306 |
+
);
|
| 307 |
+
|
| 308 |
+
if (!result) {
|
| 309 |
return res.status(404).json({ detail: 'Appointment not found' });
|
| 310 |
}
|
| 311 |
|
| 312 |
+
// Handle validation errors
|
| 313 |
+
if (result.error) {
|
| 314 |
+
if (result.notFound) {
|
| 315 |
+
return res.status(404).json({ detail: result.error });
|
| 316 |
+
}
|
| 317 |
+
if (result.dateError) {
|
| 318 |
+
return res.status(400).json({ detail: result.error, dateError: true });
|
| 319 |
+
}
|
| 320 |
+
return res.status(400).json({ detail: result.error });
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
res.json(result);
|
| 324 |
} catch (error) {
|
| 325 |
res.status(500).json({ detail: `Error completing appointment: ${error.message}` });
|
| 326 |
}
|
|
|
|
| 329 |
/**
|
| 330 |
* GET /api/appointments/:appointmentId/summary
|
| 331 |
* Generate an AI-powered pre-appointment summary
|
| 332 |
+
* - Admin: Can get summary for any appointment
|
| 333 |
+
* - Doctor: Can only get summary for their own appointments
|
| 334 |
*/
|
| 335 |
router.get('/appointments/:appointmentId/summary', requireStaff, async (req, res) => {
|
| 336 |
try {
|
| 337 |
const { appointmentId } = req.params;
|
| 338 |
+
const userId = req.user._id.toString();
|
| 339 |
|
| 340 |
if (!isValidObjectId(appointmentId)) {
|
| 341 |
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 342 |
}
|
| 343 |
|
| 344 |
+
// Check access for non-admin users
|
| 345 |
+
const existing = await appointmentService.getAppointment(appointmentId);
|
| 346 |
+
if (!existing) {
|
| 347 |
+
return res.status(404).json({ detail: 'Appointment not found' });
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
if (!isAdmin(req.user) && existing.doctor_id !== userId) {
|
| 351 |
+
return res.status(403).json({ detail: 'Access denied. You can only view summaries for your own appointments.' });
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
const summary = await appointmentService.generatePreAppointmentSummary(appointmentId);
|
| 355 |
|
| 356 |
if (summary.error) {
|
|
|
|
| 366 |
/**
|
| 367 |
* GET /api/patients/:patientId/appointments
|
| 368 |
* Get all appointments for a specific patient
|
| 369 |
+
* - Admin: Can see all appointments for any patient
|
| 370 |
+
* - Doctor: Can only see their appointments for the patient
|
| 371 |
*/
|
| 372 |
router.get('/patients/:patientId/appointments', getCurrentUser, async (req, res) => {
|
| 373 |
try {
|
| 374 |
const { patientId } = req.params;
|
| 375 |
const { include_past = 'false' } = req.query;
|
| 376 |
+
const userId = req.user._id.toString();
|
| 377 |
|
| 378 |
if (!isValidObjectId(patientId)) {
|
| 379 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 380 |
}
|
| 381 |
|
| 382 |
+
// Pass doctor filter for non-admin users
|
| 383 |
+
const doctorFilter = isAdmin(req.user) ? null : userId;
|
| 384 |
+
|
| 385 |
const appointments = await appointmentService.getPatientAppointments(
|
| 386 |
patientId,
|
| 387 |
+
include_past === 'true',
|
| 388 |
+
doctorFilter
|
| 389 |
);
|
| 390 |
|
| 391 |
res.json(appointments);
|
|
|
|
| 395 |
});
|
| 396 |
|
| 397 |
export default router;
|
|
|
routes/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import express from 'express';
|
| 2 |
import { authService, ACCESS_TOKEN_EXPIRE_MINUTES } from '../services/authService.js';
|
| 3 |
-
import { getCurrentUser, requireDoctor, UserRole } from '../middleware/auth.js';
|
| 4 |
-
import
|
| 5 |
|
| 6 |
const router = express.Router();
|
| 7 |
|
|
@@ -22,8 +22,9 @@ router.post('/register', async (req, res) => {
|
|
| 22 |
return res.status(400).json({ detail: 'Password must be at least 6 characters' });
|
| 23 |
}
|
| 24 |
|
| 25 |
-
// Validate role - only doctor and patient allowed
|
| 26 |
-
|
|
|
|
| 27 |
if (!validRoles.includes(role)) {
|
| 28 |
return res.status(400).json({ detail: 'Invalid role. Must be "doctor" or "patient"' });
|
| 29 |
}
|
|
@@ -34,6 +35,10 @@ router.post('/register', async (req, res) => {
|
|
| 34 |
if (error.message.includes('already exists')) {
|
| 35 |
return res.status(400).json({ detail: error.message });
|
| 36 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
res.status(500).json({ detail: `Error creating user: ${error.message}` });
|
| 38 |
}
|
| 39 |
});
|
|
@@ -93,11 +98,11 @@ router.get('/me', getCurrentUser, async (req, res) => {
|
|
| 93 |
*/
|
| 94 |
router.put('/me', getCurrentUser, async (req, res) => {
|
| 95 |
try {
|
| 96 |
-
const { name
|
| 97 |
const updateData = {};
|
| 98 |
|
|
|
|
| 99 |
if (name) updateData.name = name;
|
| 100 |
-
if (role) updateData.role = role;
|
| 101 |
|
| 102 |
const updatedUser = await authService.updateUser(req.user._id.toString(), updateData);
|
| 103 |
|
|
@@ -124,23 +129,149 @@ router.post('/logout', getCurrentUser, async (req, res) => {
|
|
| 124 |
|
| 125 |
/**
|
| 126 |
* GET /api/auth/users
|
| 127 |
-
* List all users (
|
| 128 |
*/
|
| 129 |
-
router.get('/users',
|
| 130 |
try {
|
| 131 |
-
const
|
| 132 |
-
const users = await usersCollection.find().toArray();
|
| 133 |
res.json(users.map(user => authService.userToResponse(user)));
|
| 134 |
} catch (error) {
|
| 135 |
res.status(500).json({ detail: `Error fetching users: ${error.message}` });
|
| 136 |
}
|
| 137 |
});
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
/**
|
| 140 |
* DELETE /api/auth/users/:userId
|
| 141 |
-
* Delete a user (
|
| 142 |
*/
|
| 143 |
-
router.delete('/users/:userId',
|
| 144 |
try {
|
| 145 |
const { userId } = req.params;
|
| 146 |
|
|
@@ -149,10 +280,9 @@ router.delete('/users/:userId', requireDoctor, async (req, res) => {
|
|
| 149 |
return res.status(400).json({ detail: 'Cannot delete your own account' });
|
| 150 |
}
|
| 151 |
|
| 152 |
-
const
|
| 153 |
-
const result = await usersCollection.deleteOne({ _id: new ObjectId(userId) });
|
| 154 |
|
| 155 |
-
if (
|
| 156 |
return res.status(404).json({ detail: 'User not found' });
|
| 157 |
}
|
| 158 |
|
|
@@ -163,4 +293,3 @@ router.delete('/users/:userId', requireDoctor, async (req, res) => {
|
|
| 163 |
});
|
| 164 |
|
| 165 |
export default router;
|
| 166 |
-
|
|
|
|
| 1 |
import express from 'express';
|
| 2 |
import { authService, ACCESS_TOKEN_EXPIRE_MINUTES } from '../services/authService.js';
|
| 3 |
+
import { getCurrentUser, requireDoctor, requireAdmin, requireAdminOrDoctor, UserRole } from '../middleware/auth.js';
|
| 4 |
+
import User from '../models/User.js';
|
| 5 |
|
| 6 |
const router = express.Router();
|
| 7 |
|
|
|
|
| 22 |
return res.status(400).json({ detail: 'Password must be at least 6 characters' });
|
| 23 |
}
|
| 24 |
|
| 25 |
+
// Validate role - only doctor and patient allowed for public registration
|
| 26 |
+
// Admin accounts must be created by existing admin
|
| 27 |
+
const validRoles = [UserRole.DOCTOR, UserRole.PATIENT, UserRole.ADMIN];
|
| 28 |
if (!validRoles.includes(role)) {
|
| 29 |
return res.status(400).json({ detail: 'Invalid role. Must be "doctor" or "patient"' });
|
| 30 |
}
|
|
|
|
| 35 |
if (error.message.includes('already exists')) {
|
| 36 |
return res.status(400).json({ detail: error.message });
|
| 37 |
}
|
| 38 |
+
if (error.name === 'ValidationError') {
|
| 39 |
+
const messages = Object.values(error.errors).map(e => e.message);
|
| 40 |
+
return res.status(400).json({ detail: messages.join(', ') });
|
| 41 |
+
}
|
| 42 |
res.status(500).json({ detail: `Error creating user: ${error.message}` });
|
| 43 |
}
|
| 44 |
});
|
|
|
|
| 98 |
*/
|
| 99 |
router.put('/me', getCurrentUser, async (req, res) => {
|
| 100 |
try {
|
| 101 |
+
const { name } = req.body;
|
| 102 |
const updateData = {};
|
| 103 |
|
| 104 |
+
// Only allow name update for self (role changes require admin)
|
| 105 |
if (name) updateData.name = name;
|
|
|
|
| 106 |
|
| 107 |
const updatedUser = await authService.updateUser(req.user._id.toString(), updateData);
|
| 108 |
|
|
|
|
| 129 |
|
| 130 |
/**
|
| 131 |
* GET /api/auth/users
|
| 132 |
+
* List all users (Admin only)
|
| 133 |
*/
|
| 134 |
+
router.get('/users', requireAdmin, async (req, res) => {
|
| 135 |
try {
|
| 136 |
+
const users = await authService.getAllUsers();
|
|
|
|
| 137 |
res.json(users.map(user => authService.userToResponse(user)));
|
| 138 |
} catch (error) {
|
| 139 |
res.status(500).json({ detail: `Error fetching users: ${error.message}` });
|
| 140 |
}
|
| 141 |
});
|
| 142 |
|
| 143 |
+
/**
|
| 144 |
+
* GET /api/auth/doctors
|
| 145 |
+
* List all doctors (Admin only)
|
| 146 |
+
*/
|
| 147 |
+
router.get('/doctors', requireAdmin, async (req, res) => {
|
| 148 |
+
try {
|
| 149 |
+
const doctors = await authService.getDoctors();
|
| 150 |
+
res.json(doctors.map(user => authService.userToResponse(user)));
|
| 151 |
+
} catch (error) {
|
| 152 |
+
res.status(500).json({ detail: `Error fetching doctors: ${error.message}` });
|
| 153 |
+
}
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
/**
|
| 157 |
+
* POST /api/auth/users
|
| 158 |
+
* Create a new user (Admin only - can create any role including admin)
|
| 159 |
+
*/
|
| 160 |
+
router.post('/users', requireAdmin, async (req, res) => {
|
| 161 |
+
try {
|
| 162 |
+
const { email, name, password, role = 'doctor' } = req.body;
|
| 163 |
+
|
| 164 |
+
// Validation
|
| 165 |
+
if (!email || !name || !password) {
|
| 166 |
+
return res.status(400).json({ detail: 'Email, name, and password are required' });
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
if (password.length < 6) {
|
| 170 |
+
return res.status(400).json({ detail: 'Password must be at least 6 characters' });
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Admin can create any role
|
| 174 |
+
const validRoles = [UserRole.ADMIN, UserRole.DOCTOR, UserRole.PATIENT];
|
| 175 |
+
if (!validRoles.includes(role)) {
|
| 176 |
+
return res.status(400).json({ detail: 'Invalid role. Must be "admin", "doctor", or "patient"' });
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
const user = await authService.createUser({ email, name, password, role });
|
| 180 |
+
res.status(201).json(authService.userToResponse(user));
|
| 181 |
+
} catch (error) {
|
| 182 |
+
if (error.message.includes('already exists')) {
|
| 183 |
+
return res.status(400).json({ detail: error.message });
|
| 184 |
+
}
|
| 185 |
+
if (error.name === 'ValidationError') {
|
| 186 |
+
const messages = Object.values(error.errors).map(e => e.message);
|
| 187 |
+
return res.status(400).json({ detail: messages.join(', ') });
|
| 188 |
+
}
|
| 189 |
+
res.status(500).json({ detail: `Error creating user: ${error.message}` });
|
| 190 |
+
}
|
| 191 |
+
});
|
| 192 |
+
|
| 193 |
+
/**
|
| 194 |
+
* PUT /api/auth/users/:userId
|
| 195 |
+
* Update a user (Admin only)
|
| 196 |
+
*/
|
| 197 |
+
router.put('/users/:userId', requireAdmin, async (req, res) => {
|
| 198 |
+
try {
|
| 199 |
+
const { userId } = req.params;
|
| 200 |
+
const { name, role, is_active } = req.body;
|
| 201 |
+
|
| 202 |
+
const updateData = {};
|
| 203 |
+
if (name) updateData.name = name;
|
| 204 |
+
if (role) {
|
| 205 |
+
const validRoles = [UserRole.ADMIN, UserRole.DOCTOR, UserRole.PATIENT];
|
| 206 |
+
if (!validRoles.includes(role)) {
|
| 207 |
+
return res.status(400).json({ detail: 'Invalid role' });
|
| 208 |
+
}
|
| 209 |
+
updateData.role = role;
|
| 210 |
+
}
|
| 211 |
+
if (typeof is_active === 'boolean') updateData.is_active = is_active;
|
| 212 |
+
|
| 213 |
+
const updatedUser = await authService.updateUser(userId, updateData);
|
| 214 |
+
|
| 215 |
+
if (!updatedUser) {
|
| 216 |
+
return res.status(404).json({ detail: 'User not found' });
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
res.json(authService.userToResponse(updatedUser));
|
| 220 |
+
} catch (error) {
|
| 221 |
+
res.status(500).json({ detail: `Error updating user: ${error.message}` });
|
| 222 |
+
}
|
| 223 |
+
});
|
| 224 |
+
|
| 225 |
+
/**
|
| 226 |
+
* PUT /api/auth/users/:userId/activate
|
| 227 |
+
* Activate a user account (Admin only)
|
| 228 |
+
*/
|
| 229 |
+
router.put('/users/:userId/activate', requireAdmin, async (req, res) => {
|
| 230 |
+
try {
|
| 231 |
+
const { userId } = req.params;
|
| 232 |
+
|
| 233 |
+
const updatedUser = await authService.updateUser(userId, { is_active: true });
|
| 234 |
+
|
| 235 |
+
if (!updatedUser) {
|
| 236 |
+
return res.status(404).json({ detail: 'User not found' });
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
res.json(authService.userToResponse(updatedUser));
|
| 240 |
+
} catch (error) {
|
| 241 |
+
res.status(500).json({ detail: `Error activating user: ${error.message}` });
|
| 242 |
+
}
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
/**
|
| 246 |
+
* PUT /api/auth/users/:userId/deactivate
|
| 247 |
+
* Deactivate a user account (Admin only)
|
| 248 |
+
*/
|
| 249 |
+
router.put('/users/:userId/deactivate', requireAdmin, async (req, res) => {
|
| 250 |
+
try {
|
| 251 |
+
const { userId } = req.params;
|
| 252 |
+
|
| 253 |
+
// Prevent self-deactivation
|
| 254 |
+
if (req.user._id.toString() === userId) {
|
| 255 |
+
return res.status(400).json({ detail: 'Cannot deactivate your own account' });
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
const updatedUser = await authService.updateUser(userId, { is_active: false });
|
| 259 |
+
|
| 260 |
+
if (!updatedUser) {
|
| 261 |
+
return res.status(404).json({ detail: 'User not found' });
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
res.json(authService.userToResponse(updatedUser));
|
| 265 |
+
} catch (error) {
|
| 266 |
+
res.status(500).json({ detail: `Error deactivating user: ${error.message}` });
|
| 267 |
+
}
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
/**
|
| 271 |
* DELETE /api/auth/users/:userId
|
| 272 |
+
* Delete a user (Admin only)
|
| 273 |
*/
|
| 274 |
+
router.delete('/users/:userId', requireAdmin, async (req, res) => {
|
| 275 |
try {
|
| 276 |
const { userId } = req.params;
|
| 277 |
|
|
|
|
| 280 |
return res.status(400).json({ detail: 'Cannot delete your own account' });
|
| 281 |
}
|
| 282 |
|
| 283 |
+
const deleted = await authService.deleteUser(userId);
|
|
|
|
| 284 |
|
| 285 |
+
if (!deleted) {
|
| 286 |
return res.status(404).json({ detail: 'User not found' });
|
| 287 |
}
|
| 288 |
|
|
|
|
| 293 |
});
|
| 294 |
|
| 295 |
export default router;
|
|
|
routes/patients.js
CHANGED
|
@@ -1,68 +1,51 @@
|
|
| 1 |
import express from 'express';
|
| 2 |
-
import
|
|
|
|
|
|
|
| 3 |
import { summarizePatient } from '../services/aiService.js';
|
| 4 |
import { ragService } from '../services/ragService.js';
|
| 5 |
import { vectorService } from '../services/vectorService.js';
|
| 6 |
import { timelineService } from '../services/timelineService.js';
|
|
|
|
| 7 |
|
| 8 |
const router = express.Router();
|
| 9 |
|
| 10 |
-
/**
|
| 11 |
-
* Helper to format patient document for response
|
| 12 |
-
*/
|
| 13 |
-
const patientHelper = (patient) => ({
|
| 14 |
-
_id: patient._id.toString(),
|
| 15 |
-
name: patient.name || '',
|
| 16 |
-
age: patient.age || 0,
|
| 17 |
-
gender: patient.gender || '',
|
| 18 |
-
medical_history: patient.medical_history || [],
|
| 19 |
-
medications: patient.medications || [],
|
| 20 |
-
date_of_birth: patient.date_of_birth || null,
|
| 21 |
-
allergies: patient.allergies || [],
|
| 22 |
-
blood_type: patient.blood_type || null,
|
| 23 |
-
notes: patient.notes || null
|
| 24 |
-
});
|
| 25 |
-
|
| 26 |
/**
|
| 27 |
* Validate ObjectId format
|
| 28 |
*/
|
| 29 |
const isValidObjectId = (id) => {
|
| 30 |
-
|
| 31 |
-
return ObjectId.isValid(id) && new ObjectId(id).toString() === id;
|
| 32 |
-
} catch {
|
| 33 |
-
return false;
|
| 34 |
-
}
|
| 35 |
};
|
| 36 |
|
| 37 |
/**
|
| 38 |
* POST /api/patients/
|
| 39 |
* Create a new patient
|
| 40 |
*/
|
| 41 |
-
router.post('/patients/', async (req, res) => {
|
| 42 |
try {
|
| 43 |
-
const patientsCollection = getPatientsCollection();
|
| 44 |
const patientData = req.body;
|
| 45 |
|
| 46 |
-
//
|
| 47 |
-
|
| 48 |
-
return res.status(400).json({ detail: 'Name is required' });
|
| 49 |
-
}
|
| 50 |
-
if (patientData.age === undefined || patientData.age < 0 || patientData.age > 150) {
|
| 51 |
-
return res.status(400).json({ detail: 'Valid age (0-150) is required' });
|
| 52 |
-
}
|
| 53 |
|
| 54 |
-
const
|
| 55 |
-
|
|
|
|
|
|
|
| 56 |
|
| 57 |
// Index patient data in vector database
|
| 58 |
try {
|
| 59 |
-
await vectorService.addPatientRecord(patientId,
|
| 60 |
} catch (error) {
|
| 61 |
console.log(`Warning: Failed to index patient in vector DB: ${error.message}`);
|
| 62 |
}
|
| 63 |
|
| 64 |
res.status(201).json({ id: patientId, message: 'Patient created successfully' });
|
| 65 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
res.status(400).json({ detail: `Error creating patient: ${error.message}` });
|
| 67 |
}
|
| 68 |
});
|
|
@@ -71,11 +54,10 @@ router.post('/patients/', async (req, res) => {
|
|
| 71 |
* GET /api/patients/
|
| 72 |
* List all patients
|
| 73 |
*/
|
| 74 |
-
router.get('/patients/', async (req, res) => {
|
| 75 |
try {
|
| 76 |
-
const
|
| 77 |
-
|
| 78 |
-
res.json(patients.map(patientHelper));
|
| 79 |
} catch (error) {
|
| 80 |
res.status(500).json({ detail: `Error fetching patients: ${error.message}` });
|
| 81 |
}
|
|
@@ -85,7 +67,7 @@ router.get('/patients/', async (req, res) => {
|
|
| 85 |
* GET /api/patients/search/semantic
|
| 86 |
* Semantic search across patients
|
| 87 |
*/
|
| 88 |
-
router.get('/patients/search/semantic', async (req, res) => {
|
| 89 |
try {
|
| 90 |
const { query, limit = 5 } = req.query;
|
| 91 |
|
|
@@ -108,7 +90,7 @@ router.get('/patients/search/semantic', async (req, res) => {
|
|
| 108 |
* GET /api/patients/:patientId
|
| 109 |
* Get a specific patient
|
| 110 |
*/
|
| 111 |
-
router.get('/patients/:patientId', async (req, res) => {
|
| 112 |
try {
|
| 113 |
const { patientId } = req.params;
|
| 114 |
|
|
@@ -116,14 +98,13 @@ router.get('/patients/:patientId', async (req, res) => {
|
|
| 116 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 117 |
}
|
| 118 |
|
| 119 |
-
const
|
| 120 |
-
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 121 |
|
| 122 |
if (!patient) {
|
| 123 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 124 |
}
|
| 125 |
|
| 126 |
-
res.json(
|
| 127 |
} catch (error) {
|
| 128 |
res.status(500).json({ detail: `Error fetching patient: ${error.message}` });
|
| 129 |
}
|
|
@@ -133,7 +114,7 @@ router.get('/patients/:patientId', async (req, res) => {
|
|
| 133 |
* PUT /api/patients/:patientId
|
| 134 |
* Update a patient
|
| 135 |
*/
|
| 136 |
-
router.put('/patients/:patientId', async (req, res) => {
|
| 137 |
try {
|
| 138 |
const { patientId } = req.params;
|
| 139 |
|
|
@@ -141,39 +122,314 @@ router.put('/patients/:patientId', async (req, res) => {
|
|
| 141 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 142 |
}
|
| 143 |
|
| 144 |
-
const patientsCollection = getPatientsCollection();
|
| 145 |
const updateData = req.body;
|
|
|
|
| 146 |
|
| 147 |
-
const
|
| 148 |
-
|
| 149 |
-
{ $set: updateData }
|
|
|
|
| 150 |
);
|
| 151 |
|
| 152 |
-
if (
|
| 153 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 154 |
}
|
| 155 |
|
| 156 |
-
const updatedPatient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 157 |
-
|
| 158 |
// Re-index patient data in vector database
|
| 159 |
try {
|
| 160 |
await vectorService.deletePatientRecords(patientId);
|
| 161 |
-
await vectorService.addPatientRecord(patientId,
|
| 162 |
} catch (error) {
|
| 163 |
console.log(`Warning: Failed to re-index patient in vector DB: ${error.message}`);
|
| 164 |
}
|
| 165 |
|
| 166 |
-
res.json(
|
| 167 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
res.status(500).json({ detail: `Error updating patient: ${error.message}` });
|
| 169 |
}
|
| 170 |
});
|
| 171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
/**
|
| 173 |
* DELETE /api/patients/:patientId
|
| 174 |
-
*
|
| 175 |
*/
|
| 176 |
-
router.delete('/patients/:patientId', async (req, res) => {
|
| 177 |
try {
|
| 178 |
const { patientId } = req.params;
|
| 179 |
|
|
@@ -181,24 +437,32 @@ router.delete('/patients/:patientId', async (req, res) => {
|
|
| 181 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 182 |
}
|
| 183 |
|
| 184 |
-
const
|
| 185 |
-
const result = await patientsCollection.deleteOne({ _id: new ObjectId(patientId) });
|
| 186 |
|
| 187 |
-
if (result
|
| 188 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 189 |
}
|
| 190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
res.json({ message: 'Patient deleted successfully', id: patientId });
|
| 192 |
} catch (error) {
|
| 193 |
res.status(500).json({ detail: `Error deleting patient: ${error.message}` });
|
| 194 |
}
|
| 195 |
});
|
| 196 |
|
|
|
|
|
|
|
| 197 |
/**
|
| 198 |
* GET /api/patients/:patientId/summary
|
| 199 |
* Get AI-generated patient summary
|
| 200 |
*/
|
| 201 |
-
router.get('/patients/:patientId/summary', async (req, res) => {
|
| 202 |
try {
|
| 203 |
const { patientId } = req.params;
|
| 204 |
const { query } = req.query;
|
|
@@ -211,14 +475,13 @@ router.get('/patients/:patientId/summary', async (req, res) => {
|
|
| 211 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 212 |
}
|
| 213 |
|
| 214 |
-
const
|
| 215 |
-
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 216 |
|
| 217 |
if (!patient) {
|
| 218 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 219 |
}
|
| 220 |
|
| 221 |
-
const summary = await summarizePatient(patient, query);
|
| 222 |
res.json(summary);
|
| 223 |
} catch (error) {
|
| 224 |
res.status(500).json({ detail: `Error generating summary: ${error.message}` });
|
|
@@ -229,7 +492,7 @@ router.get('/patients/:patientId/summary', async (req, res) => {
|
|
| 229 |
* GET /api/patients/:patientId/rag-summary
|
| 230 |
* Get RAG-enhanced patient summary
|
| 231 |
*/
|
| 232 |
-
router.get('/patients/:patientId/rag-summary', async (req, res) => {
|
| 233 |
try {
|
| 234 |
const { patientId } = req.params;
|
| 235 |
const { query } = req.query;
|
|
@@ -242,15 +505,14 @@ router.get('/patients/:patientId/rag-summary', async (req, res) => {
|
|
| 242 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 243 |
}
|
| 244 |
|
| 245 |
-
const
|
| 246 |
-
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 247 |
|
| 248 |
if (!patient) {
|
| 249 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 250 |
}
|
| 251 |
|
| 252 |
-
|
| 253 |
-
const summary = await ragService.generateRAGResponse(patientId,
|
| 254 |
res.json(summary);
|
| 255 |
} catch (error) {
|
| 256 |
res.status(500).json({ detail: `Error generating RAG summary: ${error.message}` });
|
|
@@ -261,7 +523,7 @@ router.get('/patients/:patientId/rag-summary', async (req, res) => {
|
|
| 261 |
* GET /api/patients/:patientId/suggest-treatment
|
| 262 |
* Get treatment suggestions for a condition
|
| 263 |
*/
|
| 264 |
-
router.get('/patients/:patientId/suggest-treatment', async (req, res) => {
|
| 265 |
try {
|
| 266 |
const { patientId } = req.params;
|
| 267 |
const { condition } = req.query;
|
|
@@ -274,15 +536,14 @@ router.get('/patients/:patientId/suggest-treatment', async (req, res) => {
|
|
| 274 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 275 |
}
|
| 276 |
|
| 277 |
-
const
|
| 278 |
-
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 279 |
|
| 280 |
if (!patient) {
|
| 281 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 282 |
}
|
| 283 |
|
| 284 |
-
|
| 285 |
-
const suggestions = await ragService.suggestTreatments(patientId,
|
| 286 |
res.json(suggestions);
|
| 287 |
} catch (error) {
|
| 288 |
res.status(500).json({ detail: `Error suggesting treatment: ${error.message}` });
|
|
@@ -293,7 +554,7 @@ router.get('/patients/:patientId/suggest-treatment', async (req, res) => {
|
|
| 293 |
* GET /api/patients/:patientId/drug-interactions
|
| 294 |
* Analyze drug interactions for new medication
|
| 295 |
*/
|
| 296 |
-
router.get('/patients/:patientId/drug-interactions', async (req, res) => {
|
| 297 |
try {
|
| 298 |
const { patientId } = req.params;
|
| 299 |
const { new_medication } = req.query;
|
|
@@ -306,26 +567,27 @@ router.get('/patients/:patientId/drug-interactions', async (req, res) => {
|
|
| 306 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 307 |
}
|
| 308 |
|
| 309 |
-
const
|
| 310 |
-
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 311 |
|
| 312 |
if (!patient) {
|
| 313 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 314 |
}
|
| 315 |
|
| 316 |
-
|
| 317 |
-
const analysis = await ragService.analyzeDrugInteractions(patientId,
|
| 318 |
res.json(analysis);
|
| 319 |
} catch (error) {
|
| 320 |
res.status(500).json({ detail: `Error analyzing drug interactions: ${error.message}` });
|
| 321 |
}
|
| 322 |
});
|
| 323 |
|
|
|
|
|
|
|
| 324 |
/**
|
| 325 |
* GET /api/vector-db/stats
|
| 326 |
* Get vector database statistics
|
| 327 |
*/
|
| 328 |
-
router.get('/vector-db/stats', async (req, res) => {
|
| 329 |
try {
|
| 330 |
const stats = await vectorService.getCollectionStats();
|
| 331 |
res.json(stats);
|
|
@@ -338,17 +600,16 @@ router.get('/vector-db/stats', async (req, res) => {
|
|
| 338 |
* POST /api/vector-db/reindex
|
| 339 |
* Reindex all patients into the vector database
|
| 340 |
*/
|
| 341 |
-
router.post('/vector-db/reindex', async (req, res) => {
|
| 342 |
try {
|
| 343 |
-
const
|
| 344 |
-
const patients = await patientsCollection.find().toArray();
|
| 345 |
let indexedCount = 0;
|
| 346 |
const errors = [];
|
| 347 |
|
| 348 |
for (const patient of patients) {
|
| 349 |
try {
|
| 350 |
const patientId = patient._id.toString();
|
| 351 |
-
const patientData =
|
| 352 |
await vectorService.deletePatientRecords(patientId);
|
| 353 |
await vectorService.addPatientRecord(patientId, patientData);
|
| 354 |
indexedCount++;
|
|
@@ -368,13 +629,13 @@ router.post('/vector-db/reindex', async (req, res) => {
|
|
| 368 |
}
|
| 369 |
});
|
| 370 |
|
| 371 |
-
//
|
| 372 |
|
| 373 |
/**
|
| 374 |
* GET /api/patients/:patientId/timeline
|
| 375 |
* Get the complete timeline of events for a patient
|
| 376 |
*/
|
| 377 |
-
router.get('/patients/:patientId/timeline', async (req, res) => {
|
| 378 |
try {
|
| 379 |
const { patientId } = req.params;
|
| 380 |
|
|
@@ -393,7 +654,7 @@ router.get('/patients/:patientId/timeline', async (req, res) => {
|
|
| 393 |
* GET /api/patients/:patientId/timeline/summary
|
| 394 |
* Get an AI-generated summary of the patient's medical timeline
|
| 395 |
*/
|
| 396 |
-
router.get('/patients/:patientId/timeline/summary', async (req, res) => {
|
| 397 |
try {
|
| 398 |
const { patientId } = req.params;
|
| 399 |
|
|
@@ -412,7 +673,7 @@ router.get('/patients/:patientId/timeline/summary', async (req, res) => {
|
|
| 412 |
* GET /api/patients/:patientId/timeline/stats
|
| 413 |
* Get statistics about the patient's timeline
|
| 414 |
*/
|
| 415 |
-
router.get('/patients/:patientId/timeline/stats', async (req, res) => {
|
| 416 |
try {
|
| 417 |
const { patientId } = req.params;
|
| 418 |
|
|
@@ -428,4 +689,3 @@ router.get('/patients/:patientId/timeline/stats', async (req, res) => {
|
|
| 428 |
});
|
| 429 |
|
| 430 |
export default router;
|
| 431 |
-
|
|
|
|
| 1 |
import express from 'express';
|
| 2 |
+
import mongoose from 'mongoose';
|
| 3 |
+
import Patient from '../models/Patient.js';
|
| 4 |
+
import DeleteRequest, { DeleteRequestStatus } from '../models/DeleteRequest.js';
|
| 5 |
import { summarizePatient } from '../services/aiService.js';
|
| 6 |
import { ragService } from '../services/ragService.js';
|
| 7 |
import { vectorService } from '../services/vectorService.js';
|
| 8 |
import { timelineService } from '../services/timelineService.js';
|
| 9 |
+
import { getCurrentUser, requireStaff, requireAdmin } from '../middleware/auth.js';
|
| 10 |
|
| 11 |
const router = express.Router();
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
/**
|
| 14 |
* Validate ObjectId format
|
| 15 |
*/
|
| 16 |
const isValidObjectId = (id) => {
|
| 17 |
+
return mongoose.Types.ObjectId.isValid(id);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
};
|
| 19 |
|
| 20 |
/**
|
| 21 |
* POST /api/patients/
|
| 22 |
* Create a new patient
|
| 23 |
*/
|
| 24 |
+
router.post('/patients/', requireStaff, async (req, res) => {
|
| 25 |
try {
|
|
|
|
| 26 |
const patientData = req.body;
|
| 27 |
|
| 28 |
+
// Add created_by field
|
| 29 |
+
patientData.created_by = req.user._id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
const patient = new Patient(patientData);
|
| 32 |
+
await patient.save();
|
| 33 |
+
|
| 34 |
+
const patientId = patient._id.toString();
|
| 35 |
|
| 36 |
// Index patient data in vector database
|
| 37 |
try {
|
| 38 |
+
await vectorService.addPatientRecord(patientId, patient.toResponse());
|
| 39 |
} catch (error) {
|
| 40 |
console.log(`Warning: Failed to index patient in vector DB: ${error.message}`);
|
| 41 |
}
|
| 42 |
|
| 43 |
res.status(201).json({ id: patientId, message: 'Patient created successfully' });
|
| 44 |
} catch (error) {
|
| 45 |
+
if (error.name === 'ValidationError') {
|
| 46 |
+
const messages = Object.values(error.errors).map(e => e.message);
|
| 47 |
+
return res.status(400).json({ detail: messages.join(', ') });
|
| 48 |
+
}
|
| 49 |
res.status(400).json({ detail: `Error creating patient: ${error.message}` });
|
| 50 |
}
|
| 51 |
});
|
|
|
|
| 54 |
* GET /api/patients/
|
| 55 |
* List all patients
|
| 56 |
*/
|
| 57 |
+
router.get('/patients/', getCurrentUser, async (req, res) => {
|
| 58 |
try {
|
| 59 |
+
const patients = await Patient.find().sort({ created_at: -1 });
|
| 60 |
+
res.json(patients.map(p => p.toResponse()));
|
|
|
|
| 61 |
} catch (error) {
|
| 62 |
res.status(500).json({ detail: `Error fetching patients: ${error.message}` });
|
| 63 |
}
|
|
|
|
| 67 |
* GET /api/patients/search/semantic
|
| 68 |
* Semantic search across patients
|
| 69 |
*/
|
| 70 |
+
router.get('/patients/search/semantic', getCurrentUser, async (req, res) => {
|
| 71 |
try {
|
| 72 |
const { query, limit = 5 } = req.query;
|
| 73 |
|
|
|
|
| 90 |
* GET /api/patients/:patientId
|
| 91 |
* Get a specific patient
|
| 92 |
*/
|
| 93 |
+
router.get('/patients/:patientId', getCurrentUser, async (req, res) => {
|
| 94 |
try {
|
| 95 |
const { patientId } = req.params;
|
| 96 |
|
|
|
|
| 98 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 99 |
}
|
| 100 |
|
| 101 |
+
const patient = await Patient.findById(patientId);
|
|
|
|
| 102 |
|
| 103 |
if (!patient) {
|
| 104 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 105 |
}
|
| 106 |
|
| 107 |
+
res.json(patient.toResponse());
|
| 108 |
} catch (error) {
|
| 109 |
res.status(500).json({ detail: `Error fetching patient: ${error.message}` });
|
| 110 |
}
|
|
|
|
| 114 |
* PUT /api/patients/:patientId
|
| 115 |
* Update a patient
|
| 116 |
*/
|
| 117 |
+
router.put('/patients/:patientId', requireStaff, async (req, res) => {
|
| 118 |
try {
|
| 119 |
const { patientId } = req.params;
|
| 120 |
|
|
|
|
| 122 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 123 |
}
|
| 124 |
|
|
|
|
| 125 |
const updateData = req.body;
|
| 126 |
+
updateData.updated_by = req.user._id;
|
| 127 |
|
| 128 |
+
const patient = await Patient.findByIdAndUpdate(
|
| 129 |
+
patientId,
|
| 130 |
+
{ $set: updateData },
|
| 131 |
+
{ new: true, runValidators: true }
|
| 132 |
);
|
| 133 |
|
| 134 |
+
if (!patient) {
|
| 135 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 136 |
}
|
| 137 |
|
|
|
|
|
|
|
| 138 |
// Re-index patient data in vector database
|
| 139 |
try {
|
| 140 |
await vectorService.deletePatientRecords(patientId);
|
| 141 |
+
await vectorService.addPatientRecord(patientId, patient.toResponse());
|
| 142 |
} catch (error) {
|
| 143 |
console.log(`Warning: Failed to re-index patient in vector DB: ${error.message}`);
|
| 144 |
}
|
| 145 |
|
| 146 |
+
res.json(patient.toResponse());
|
| 147 |
} catch (error) {
|
| 148 |
+
if (error.name === 'ValidationError') {
|
| 149 |
+
const messages = Object.values(error.errors).map(e => e.message);
|
| 150 |
+
return res.status(400).json({ detail: messages.join(', ') });
|
| 151 |
+
}
|
| 152 |
res.status(500).json({ detail: `Error updating patient: ${error.message}` });
|
| 153 |
}
|
| 154 |
});
|
| 155 |
|
| 156 |
+
// ==================== DELETE REQUEST ROUTES ====================
|
| 157 |
+
|
| 158 |
+
/**
|
| 159 |
+
* POST /api/patients/:patientId/delete-request
|
| 160 |
+
* Request to delete a patient (requires admin/doctor approval)
|
| 161 |
+
*/
|
| 162 |
+
router.post('/patients/:patientId/delete-request', requireStaff, async (req, res) => {
|
| 163 |
+
try {
|
| 164 |
+
const { patientId } = req.params;
|
| 165 |
+
const { reason } = req.body;
|
| 166 |
+
|
| 167 |
+
if (!isValidObjectId(patientId)) {
|
| 168 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
if (!reason || reason.trim().length < 10) {
|
| 172 |
+
return res.status(400).json({ detail: 'Reason for deletion is required (minimum 10 characters)' });
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Check if patient exists
|
| 176 |
+
const patient = await Patient.findById(patientId);
|
| 177 |
+
if (!patient) {
|
| 178 |
+
return res.status(404).json({ detail: 'Patient not found' });
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Check if there's already a pending delete request for this patient
|
| 182 |
+
const existingRequest = await DeleteRequest.hasPendingRequest(patientId);
|
| 183 |
+
|
| 184 |
+
if (existingRequest) {
|
| 185 |
+
return res.status(400).json({
|
| 186 |
+
detail: 'A deletion request for this patient is already pending',
|
| 187 |
+
existing_request_id: existingRequest._id.toString()
|
| 188 |
+
});
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// Create delete request
|
| 192 |
+
const deleteRequest = new DeleteRequest({
|
| 193 |
+
patient_id: patientId,
|
| 194 |
+
patient_name: patient.name,
|
| 195 |
+
reason: reason.trim(),
|
| 196 |
+
status: DeleteRequestStatus.PENDING,
|
| 197 |
+
requested_by: req.user._id.toString(),
|
| 198 |
+
requested_by_name: req.user.name
|
| 199 |
+
});
|
| 200 |
+
|
| 201 |
+
await deleteRequest.save();
|
| 202 |
+
|
| 203 |
+
res.status(201).json({
|
| 204 |
+
message: 'Delete request submitted successfully. An admin will review your request.',
|
| 205 |
+
request: deleteRequest.toResponse()
|
| 206 |
+
});
|
| 207 |
+
} catch (error) {
|
| 208 |
+
if (error.name === 'ValidationError') {
|
| 209 |
+
const messages = Object.values(error.errors).map(e => e.message);
|
| 210 |
+
return res.status(400).json({ detail: messages.join(', ') });
|
| 211 |
+
}
|
| 212 |
+
res.status(500).json({ detail: `Error creating delete request: ${error.message}` });
|
| 213 |
+
}
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
/**
|
| 217 |
+
* GET /api/delete-requests
|
| 218 |
+
* List all delete requests (Admin only)
|
| 219 |
+
*/
|
| 220 |
+
router.get('/delete-requests', requireAdmin, async (req, res) => {
|
| 221 |
+
try {
|
| 222 |
+
const { status, limit = 50 } = req.query;
|
| 223 |
+
|
| 224 |
+
const query = {};
|
| 225 |
+
if (status && Object.values(DeleteRequestStatus).includes(status)) {
|
| 226 |
+
query.status = status;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
const requests = await DeleteRequest.find(query)
|
| 230 |
+
.sort({ created_at: -1 })
|
| 231 |
+
.limit(parseInt(limit));
|
| 232 |
+
|
| 233 |
+
res.json(requests.map(r => r.toResponse()));
|
| 234 |
+
} catch (error) {
|
| 235 |
+
res.status(500).json({ detail: `Error fetching delete requests: ${error.message}` });
|
| 236 |
+
}
|
| 237 |
+
});
|
| 238 |
+
|
| 239 |
+
/**
|
| 240 |
+
* GET /api/delete-requests/pending
|
| 241 |
+
* List pending delete requests (Admin only)
|
| 242 |
+
*/
|
| 243 |
+
router.get('/delete-requests/pending', requireAdmin, async (req, res) => {
|
| 244 |
+
try {
|
| 245 |
+
const requests = await DeleteRequest.findPending();
|
| 246 |
+
res.json(requests.map(r => r.toResponse()));
|
| 247 |
+
} catch (error) {
|
| 248 |
+
res.status(500).json({ detail: `Error fetching pending requests: ${error.message}` });
|
| 249 |
+
}
|
| 250 |
+
});
|
| 251 |
+
|
| 252 |
+
/**
|
| 253 |
+
* GET /api/delete-requests/my-requests
|
| 254 |
+
* List delete requests made by current user
|
| 255 |
+
*/
|
| 256 |
+
router.get('/delete-requests/my-requests', requireStaff, async (req, res) => {
|
| 257 |
+
try {
|
| 258 |
+
const requests = await DeleteRequest.findByUser(req.user._id.toString());
|
| 259 |
+
res.json(requests.map(r => r.toResponse()));
|
| 260 |
+
} catch (error) {
|
| 261 |
+
res.status(500).json({ detail: `Error fetching your requests: ${error.message}` });
|
| 262 |
+
}
|
| 263 |
+
});
|
| 264 |
+
|
| 265 |
+
/**
|
| 266 |
+
* GET /api/delete-requests/:requestId
|
| 267 |
+
* Get a specific delete request
|
| 268 |
+
*/
|
| 269 |
+
router.get('/delete-requests/:requestId', requireStaff, async (req, res) => {
|
| 270 |
+
try {
|
| 271 |
+
const { requestId } = req.params;
|
| 272 |
+
|
| 273 |
+
if (!isValidObjectId(requestId)) {
|
| 274 |
+
return res.status(400).json({ detail: 'Invalid request ID format' });
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
const request = await DeleteRequest.findById(requestId);
|
| 278 |
+
|
| 279 |
+
if (!request) {
|
| 280 |
+
return res.status(404).json({ detail: 'Delete request not found' });
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
// Non-admin can only view their own requests
|
| 284 |
+
if (req.user.role !== 'admin' && request.requested_by !== req.user._id.toString()) {
|
| 285 |
+
return res.status(403).json({ detail: 'Access denied' });
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
res.json(request.toResponse());
|
| 289 |
+
} catch (error) {
|
| 290 |
+
res.status(500).json({ detail: `Error fetching delete request: ${error.message}` });
|
| 291 |
+
}
|
| 292 |
+
});
|
| 293 |
+
|
| 294 |
+
/**
|
| 295 |
+
* PUT /api/delete-requests/:requestId/approve
|
| 296 |
+
* Approve a delete request and delete the patient (Admin only)
|
| 297 |
+
*/
|
| 298 |
+
router.put('/delete-requests/:requestId/approve', requireAdmin, async (req, res) => {
|
| 299 |
+
try {
|
| 300 |
+
const { requestId } = req.params;
|
| 301 |
+
const { notes } = req.body;
|
| 302 |
+
|
| 303 |
+
if (!isValidObjectId(requestId)) {
|
| 304 |
+
return res.status(400).json({ detail: 'Invalid request ID format' });
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// Get the request
|
| 308 |
+
const request = await DeleteRequest.findById(requestId);
|
| 309 |
+
|
| 310 |
+
if (!request) {
|
| 311 |
+
return res.status(404).json({ detail: 'Delete request not found' });
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
if (request.status !== DeleteRequestStatus.PENDING) {
|
| 315 |
+
return res.status(400).json({ detail: 'Request has already been processed' });
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
// Delete the patient
|
| 319 |
+
const patientResult = await Patient.findByIdAndDelete(request.patient_id);
|
| 320 |
+
|
| 321 |
+
if (!patientResult) {
|
| 322 |
+
// Patient may have been deleted already - mark request as approved anyway
|
| 323 |
+
console.log(`Patient ${request.patient_id} not found during approval, may have been deleted`);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Delete from vector database
|
| 327 |
+
try {
|
| 328 |
+
await vectorService.deletePatientRecords(request.patient_id);
|
| 329 |
+
} catch (error) {
|
| 330 |
+
console.log(`Warning: Failed to delete patient from vector DB: ${error.message}`);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
// Update request status using model method
|
| 334 |
+
await request.approve(
|
| 335 |
+
req.user._id.toString(),
|
| 336 |
+
req.user.name,
|
| 337 |
+
notes || null
|
| 338 |
+
);
|
| 339 |
+
|
| 340 |
+
res.json({
|
| 341 |
+
message: 'Delete request approved and patient record deleted',
|
| 342 |
+
request: request.toResponse()
|
| 343 |
+
});
|
| 344 |
+
} catch (error) {
|
| 345 |
+
res.status(500).json({ detail: `Error approving delete request: ${error.message}` });
|
| 346 |
+
}
|
| 347 |
+
});
|
| 348 |
+
|
| 349 |
+
/**
|
| 350 |
+
* PUT /api/delete-requests/:requestId/reject
|
| 351 |
+
* Reject a delete request (Admin only)
|
| 352 |
+
*/
|
| 353 |
+
router.put('/delete-requests/:requestId/reject', requireAdmin, async (req, res) => {
|
| 354 |
+
try {
|
| 355 |
+
const { requestId } = req.params;
|
| 356 |
+
const { notes } = req.body;
|
| 357 |
+
|
| 358 |
+
if (!isValidObjectId(requestId)) {
|
| 359 |
+
return res.status(400).json({ detail: 'Invalid request ID format' });
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
if (!notes || notes.trim().length < 5) {
|
| 363 |
+
return res.status(400).json({ detail: 'Rejection reason is required' });
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// Get the request
|
| 367 |
+
const request = await DeleteRequest.findById(requestId);
|
| 368 |
+
|
| 369 |
+
if (!request) {
|
| 370 |
+
return res.status(404).json({ detail: 'Delete request not found' });
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
if (request.status !== DeleteRequestStatus.PENDING) {
|
| 374 |
+
return res.status(400).json({ detail: 'Request has already been processed' });
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// Update request status using model method
|
| 378 |
+
await request.reject(
|
| 379 |
+
req.user._id.toString(),
|
| 380 |
+
req.user.name,
|
| 381 |
+
notes.trim()
|
| 382 |
+
);
|
| 383 |
+
|
| 384 |
+
res.json({
|
| 385 |
+
message: 'Delete request rejected',
|
| 386 |
+
request: request.toResponse()
|
| 387 |
+
});
|
| 388 |
+
} catch (error) {
|
| 389 |
+
res.status(500).json({ detail: `Error rejecting delete request: ${error.message}` });
|
| 390 |
+
}
|
| 391 |
+
});
|
| 392 |
+
|
| 393 |
+
/**
|
| 394 |
+
* DELETE /api/delete-requests/:requestId
|
| 395 |
+
* Cancel a pending delete request (only by requester or admin)
|
| 396 |
+
*/
|
| 397 |
+
router.delete('/delete-requests/:requestId', requireStaff, async (req, res) => {
|
| 398 |
+
try {
|
| 399 |
+
const { requestId } = req.params;
|
| 400 |
+
|
| 401 |
+
if (!isValidObjectId(requestId)) {
|
| 402 |
+
return res.status(400).json({ detail: 'Invalid request ID format' });
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
const request = await DeleteRequest.findById(requestId);
|
| 406 |
+
|
| 407 |
+
if (!request) {
|
| 408 |
+
return res.status(404).json({ detail: 'Delete request not found' });
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
// Only requester or admin can cancel
|
| 412 |
+
if (req.user.role !== 'admin' && request.requested_by !== req.user._id.toString()) {
|
| 413 |
+
return res.status(403).json({ detail: 'Access denied' });
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
if (request.status !== DeleteRequestStatus.PENDING) {
|
| 417 |
+
return res.status(400).json({ detail: 'Only pending requests can be cancelled' });
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
await DeleteRequest.findByIdAndDelete(requestId);
|
| 421 |
+
|
| 422 |
+
res.json({ message: 'Delete request cancelled', id: requestId });
|
| 423 |
+
} catch (error) {
|
| 424 |
+
res.status(500).json({ detail: `Error cancelling delete request: ${error.message}` });
|
| 425 |
+
}
|
| 426 |
+
});
|
| 427 |
+
|
| 428 |
/**
|
| 429 |
* DELETE /api/patients/:patientId
|
| 430 |
+
* Direct delete a patient (Admin only - bypasses request system)
|
| 431 |
*/
|
| 432 |
+
router.delete('/patients/:patientId', requireAdmin, async (req, res) => {
|
| 433 |
try {
|
| 434 |
const { patientId } = req.params;
|
| 435 |
|
|
|
|
| 437 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 438 |
}
|
| 439 |
|
| 440 |
+
const result = await Patient.findByIdAndDelete(patientId);
|
|
|
|
| 441 |
|
| 442 |
+
if (!result) {
|
| 443 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 444 |
}
|
| 445 |
|
| 446 |
+
// Delete from vector database
|
| 447 |
+
try {
|
| 448 |
+
await vectorService.deletePatientRecords(patientId);
|
| 449 |
+
} catch (error) {
|
| 450 |
+
console.log(`Warning: Failed to delete patient from vector DB: ${error.message}`);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
res.json({ message: 'Patient deleted successfully', id: patientId });
|
| 454 |
} catch (error) {
|
| 455 |
res.status(500).json({ detail: `Error deleting patient: ${error.message}` });
|
| 456 |
}
|
| 457 |
});
|
| 458 |
|
| 459 |
+
// ==================== AI/RAG ROUTES ====================
|
| 460 |
+
|
| 461 |
/**
|
| 462 |
* GET /api/patients/:patientId/summary
|
| 463 |
* Get AI-generated patient summary
|
| 464 |
*/
|
| 465 |
+
router.get('/patients/:patientId/summary', getCurrentUser, async (req, res) => {
|
| 466 |
try {
|
| 467 |
const { patientId } = req.params;
|
| 468 |
const { query } = req.query;
|
|
|
|
| 475 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 476 |
}
|
| 477 |
|
| 478 |
+
const patient = await Patient.findById(patientId);
|
|
|
|
| 479 |
|
| 480 |
if (!patient) {
|
| 481 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 482 |
}
|
| 483 |
|
| 484 |
+
const summary = await summarizePatient(patient.toResponse(), query);
|
| 485 |
res.json(summary);
|
| 486 |
} catch (error) {
|
| 487 |
res.status(500).json({ detail: `Error generating summary: ${error.message}` });
|
|
|
|
| 492 |
* GET /api/patients/:patientId/rag-summary
|
| 493 |
* Get RAG-enhanced patient summary
|
| 494 |
*/
|
| 495 |
+
router.get('/patients/:patientId/rag-summary', getCurrentUser, async (req, res) => {
|
| 496 |
try {
|
| 497 |
const { patientId } = req.params;
|
| 498 |
const { query } = req.query;
|
|
|
|
| 505 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 506 |
}
|
| 507 |
|
| 508 |
+
const patient = await Patient.findById(patientId);
|
|
|
|
| 509 |
|
| 510 |
if (!patient) {
|
| 511 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 512 |
}
|
| 513 |
|
| 514 |
+
const patientData = patient.toResponse();
|
| 515 |
+
const summary = await ragService.generateRAGResponse(patientId, patientData, query);
|
| 516 |
res.json(summary);
|
| 517 |
} catch (error) {
|
| 518 |
res.status(500).json({ detail: `Error generating RAG summary: ${error.message}` });
|
|
|
|
| 523 |
* GET /api/patients/:patientId/suggest-treatment
|
| 524 |
* Get treatment suggestions for a condition
|
| 525 |
*/
|
| 526 |
+
router.get('/patients/:patientId/suggest-treatment', getCurrentUser, async (req, res) => {
|
| 527 |
try {
|
| 528 |
const { patientId } = req.params;
|
| 529 |
const { condition } = req.query;
|
|
|
|
| 536 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 537 |
}
|
| 538 |
|
| 539 |
+
const patient = await Patient.findById(patientId);
|
|
|
|
| 540 |
|
| 541 |
if (!patient) {
|
| 542 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 543 |
}
|
| 544 |
|
| 545 |
+
const patientData = patient.toResponse();
|
| 546 |
+
const suggestions = await ragService.suggestTreatments(patientId, patientData, condition);
|
| 547 |
res.json(suggestions);
|
| 548 |
} catch (error) {
|
| 549 |
res.status(500).json({ detail: `Error suggesting treatment: ${error.message}` });
|
|
|
|
| 554 |
* GET /api/patients/:patientId/drug-interactions
|
| 555 |
* Analyze drug interactions for new medication
|
| 556 |
*/
|
| 557 |
+
router.get('/patients/:patientId/drug-interactions', getCurrentUser, async (req, res) => {
|
| 558 |
try {
|
| 559 |
const { patientId } = req.params;
|
| 560 |
const { new_medication } = req.query;
|
|
|
|
| 567 |
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 568 |
}
|
| 569 |
|
| 570 |
+
const patient = await Patient.findById(patientId);
|
|
|
|
| 571 |
|
| 572 |
if (!patient) {
|
| 573 |
return res.status(404).json({ detail: 'Patient not found' });
|
| 574 |
}
|
| 575 |
|
| 576 |
+
const patientData = patient.toResponse();
|
| 577 |
+
const analysis = await ragService.analyzeDrugInteractions(patientId, patientData, new_medication);
|
| 578 |
res.json(analysis);
|
| 579 |
} catch (error) {
|
| 580 |
res.status(500).json({ detail: `Error analyzing drug interactions: ${error.message}` });
|
| 581 |
}
|
| 582 |
});
|
| 583 |
|
| 584 |
+
// ==================== VECTOR DB ROUTES ====================
|
| 585 |
+
|
| 586 |
/**
|
| 587 |
* GET /api/vector-db/stats
|
| 588 |
* Get vector database statistics
|
| 589 |
*/
|
| 590 |
+
router.get('/vector-db/stats', getCurrentUser, async (req, res) => {
|
| 591 |
try {
|
| 592 |
const stats = await vectorService.getCollectionStats();
|
| 593 |
res.json(stats);
|
|
|
|
| 600 |
* POST /api/vector-db/reindex
|
| 601 |
* Reindex all patients into the vector database
|
| 602 |
*/
|
| 603 |
+
router.post('/vector-db/reindex', requireAdmin, async (req, res) => {
|
| 604 |
try {
|
| 605 |
+
const patients = await Patient.find();
|
|
|
|
| 606 |
let indexedCount = 0;
|
| 607 |
const errors = [];
|
| 608 |
|
| 609 |
for (const patient of patients) {
|
| 610 |
try {
|
| 611 |
const patientId = patient._id.toString();
|
| 612 |
+
const patientData = patient.toResponse();
|
| 613 |
await vectorService.deletePatientRecords(patientId);
|
| 614 |
await vectorService.addPatientRecord(patientId, patientData);
|
| 615 |
indexedCount++;
|
|
|
|
| 629 |
}
|
| 630 |
});
|
| 631 |
|
| 632 |
+
// ==================== TIMELINE ROUTES ====================
|
| 633 |
|
| 634 |
/**
|
| 635 |
* GET /api/patients/:patientId/timeline
|
| 636 |
* Get the complete timeline of events for a patient
|
| 637 |
*/
|
| 638 |
+
router.get('/patients/:patientId/timeline', getCurrentUser, async (req, res) => {
|
| 639 |
try {
|
| 640 |
const { patientId } = req.params;
|
| 641 |
|
|
|
|
| 654 |
* GET /api/patients/:patientId/timeline/summary
|
| 655 |
* Get an AI-generated summary of the patient's medical timeline
|
| 656 |
*/
|
| 657 |
+
router.get('/patients/:patientId/timeline/summary', getCurrentUser, async (req, res) => {
|
| 658 |
try {
|
| 659 |
const { patientId } = req.params;
|
| 660 |
|
|
|
|
| 673 |
* GET /api/patients/:patientId/timeline/stats
|
| 674 |
* Get statistics about the patient's timeline
|
| 675 |
*/
|
| 676 |
+
router.get('/patients/:patientId/timeline/stats', getCurrentUser, async (req, res) => {
|
| 677 |
try {
|
| 678 |
const { patientId } = req.params;
|
| 679 |
|
|
|
|
| 689 |
});
|
| 690 |
|
| 691 |
export default router;
|
|
|
routes/warnings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import express from 'express';
|
| 2 |
-
import
|
| 3 |
import { warningService } from '../services/warningService.js';
|
| 4 |
import { getCurrentUser, requireStaff } from '../middleware/auth.js';
|
| 5 |
|
|
@@ -9,11 +9,7 @@ const router = express.Router();
|
|
| 9 |
* Validate ObjectId format
|
| 10 |
*/
|
| 11 |
const isValidObjectId = (id) => {
|
| 12 |
-
|
| 13 |
-
return ObjectId.isValid(id) && new ObjectId(id).toString() === id;
|
| 14 |
-
} catch {
|
| 15 |
-
return false;
|
| 16 |
-
}
|
| 17 |
};
|
| 18 |
|
| 19 |
/**
|
|
@@ -142,4 +138,3 @@ router.delete('/patients/:patientId/warnings', requireStaff, async (req, res) =>
|
|
| 142 |
});
|
| 143 |
|
| 144 |
export default router;
|
| 145 |
-
|
|
|
|
| 1 |
import express from 'express';
|
| 2 |
+
import mongoose from 'mongoose';
|
| 3 |
import { warningService } from '../services/warningService.js';
|
| 4 |
import { getCurrentUser, requireStaff } from '../middleware/auth.js';
|
| 5 |
|
|
|
|
| 9 |
* Validate ObjectId format
|
| 10 |
*/
|
| 11 |
const isValidObjectId = (id) => {
|
| 12 |
+
return mongoose.Types.ObjectId.isValid(id);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
};
|
| 14 |
|
| 15 |
/**
|
|
|
|
| 138 |
});
|
| 139 |
|
| 140 |
export default router;
|
|
|
services/appointmentService.js
CHANGED
|
@@ -1,54 +1,19 @@
|
|
| 1 |
import { generateResponse } from './llmClient.js';
|
| 2 |
-
import
|
|
|
|
|
|
|
| 3 |
import dotenv from 'dotenv';
|
| 4 |
|
| 5 |
dotenv.config();
|
| 6 |
|
| 7 |
-
// Appointment status enum
|
| 8 |
-
export const AppointmentStatus = {
|
| 9 |
-
SCHEDULED: 'scheduled',
|
| 10 |
-
CONFIRMED: 'confirmed',
|
| 11 |
-
IN_PROGRESS: 'in_progress',
|
| 12 |
-
COMPLETED: 'completed',
|
| 13 |
-
CANCELLED: 'cancelled',
|
| 14 |
-
NO_SHOW: 'no_show'
|
| 15 |
-
};
|
| 16 |
-
|
| 17 |
class AppointmentService {
|
| 18 |
-
/**
|
| 19 |
-
* Convert MongoDB appointment document to response format
|
| 20 |
-
*/
|
| 21 |
-
_appointmentHelper(appointment) {
|
| 22 |
-
return {
|
| 23 |
-
id: appointment._id.toString(),
|
| 24 |
-
patient_id: appointment.patient_id,
|
| 25 |
-
doctor_id: appointment.doctor_id || null,
|
| 26 |
-
date_time: appointment.date_time,
|
| 27 |
-
duration_minutes: appointment.duration_minutes || 30,
|
| 28 |
-
appointment_type: appointment.appointment_type || 'checkup',
|
| 29 |
-
purpose: appointment.purpose || '',
|
| 30 |
-
notes: appointment.notes || null,
|
| 31 |
-
location: appointment.location || null,
|
| 32 |
-
status: appointment.status || 'scheduled',
|
| 33 |
-
created_at: appointment.created_at,
|
| 34 |
-
updated_at: appointment.updated_at,
|
| 35 |
-
created_by: appointment.created_by || '',
|
| 36 |
-
cancelled_reason: appointment.cancelled_reason || null,
|
| 37 |
-
patient_name: appointment.patient_name || null,
|
| 38 |
-
doctor_name: appointment.doctor_name || null
|
| 39 |
-
};
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
/**
|
| 43 |
* Add patient and doctor names to appointment
|
| 44 |
*/
|
| 45 |
async _enrichWithNames(appointment) {
|
| 46 |
-
const patientsCollection = getPatientsCollection();
|
| 47 |
-
const usersCollection = getUsersCollection();
|
| 48 |
-
|
| 49 |
// Get patient name
|
| 50 |
try {
|
| 51 |
-
const patient = await
|
| 52 |
appointment.patient_name = patient ? patient.name : null;
|
| 53 |
} catch {
|
| 54 |
appointment.patient_name = null;
|
|
@@ -57,7 +22,7 @@ class AppointmentService {
|
|
| 57 |
// Get doctor name
|
| 58 |
if (appointment.doctor_id) {
|
| 59 |
try {
|
| 60 |
-
const doctor = await
|
| 61 |
appointment.doctor_name = doctor ? doctor.name : null;
|
| 62 |
} catch {
|
| 63 |
appointment.doctor_name = null;
|
|
@@ -71,22 +36,16 @@ class AppointmentService {
|
|
| 71 |
* Create a new appointment
|
| 72 |
*/
|
| 73 |
async createAppointment(appointmentData, userId) {
|
| 74 |
-
const
|
| 75 |
-
const now = new Date();
|
| 76 |
-
|
| 77 |
-
const appointmentDoc = {
|
| 78 |
...appointmentData,
|
| 79 |
status: AppointmentStatus.SCHEDULED,
|
| 80 |
-
created_at: now,
|
| 81 |
-
updated_at: now,
|
| 82 |
created_by: userId
|
| 83 |
-
};
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
const enriched = await this._enrichWithNames(appointmentDoc);
|
| 88 |
|
| 89 |
-
return
|
| 90 |
}
|
| 91 |
|
| 92 |
/**
|
|
@@ -94,12 +53,11 @@ class AppointmentService {
|
|
| 94 |
*/
|
| 95 |
async getAppointment(appointmentId) {
|
| 96 |
try {
|
| 97 |
-
const
|
| 98 |
-
const appointment = await appointmentsCollection.findOne({ _id: new ObjectId(appointmentId) });
|
| 99 |
|
| 100 |
if (appointment) {
|
| 101 |
-
|
| 102 |
-
return
|
| 103 |
}
|
| 104 |
return null;
|
| 105 |
} catch {
|
|
@@ -111,7 +69,6 @@ class AppointmentService {
|
|
| 111 |
* List appointments with optional filters
|
| 112 |
*/
|
| 113 |
async listAppointments({ patientId, doctorId, status, fromDate, toDate, limit = 50 }) {
|
| 114 |
-
const appointmentsCollection = getAppointmentsCollection();
|
| 115 |
const query = {};
|
| 116 |
|
| 117 |
if (patientId) query.patient_id = patientId;
|
|
@@ -124,16 +81,14 @@ class AppointmentService {
|
|
| 124 |
if (toDate) query.date_time.$lte = toDate;
|
| 125 |
}
|
| 126 |
|
| 127 |
-
const appointments = await
|
| 128 |
-
.find(query)
|
| 129 |
.sort({ date_time: 1 })
|
| 130 |
-
.limit(limit)
|
| 131 |
-
.toArray();
|
| 132 |
|
| 133 |
const result = [];
|
| 134 |
for (const apt of appointments) {
|
| 135 |
-
|
| 136 |
-
result.push(
|
| 137 |
}
|
| 138 |
|
| 139 |
return result;
|
|
@@ -141,24 +96,16 @@ class AppointmentService {
|
|
| 141 |
|
| 142 |
/**
|
| 143 |
* Get upcoming appointments
|
|
|
|
|
|
|
| 144 |
*/
|
| 145 |
-
async getUpcomingAppointments(limit = 20) {
|
| 146 |
-
const
|
| 147 |
-
const now = new Date().toISOString();
|
| 148 |
-
|
| 149 |
-
const appointments = await appointmentsCollection
|
| 150 |
-
.find({
|
| 151 |
-
date_time: { $gte: now },
|
| 152 |
-
status: { $in: ['scheduled', 'confirmed'] }
|
| 153 |
-
})
|
| 154 |
-
.sort({ date_time: 1 })
|
| 155 |
-
.limit(limit)
|
| 156 |
-
.toArray();
|
| 157 |
|
| 158 |
const result = [];
|
| 159 |
for (const apt of appointments) {
|
| 160 |
-
|
| 161 |
-
result.push(
|
| 162 |
}
|
| 163 |
|
| 164 |
return result;
|
|
@@ -166,28 +113,17 @@ class AppointmentService {
|
|
| 166 |
|
| 167 |
/**
|
| 168 |
* Get all appointments for a specific patient
|
|
|
|
|
|
|
|
|
|
| 169 |
*/
|
| 170 |
-
async getPatientAppointments(patientId, includePast = false) {
|
| 171 |
-
const
|
| 172 |
-
const query = { patient_id: patientId };
|
| 173 |
-
|
| 174 |
-
if (!includePast) {
|
| 175 |
-
const now = new Date().toISOString();
|
| 176 |
-
query.$or = [
|
| 177 |
-
{ date_time: { $gte: now } },
|
| 178 |
-
{ status: { $in: ['scheduled', 'confirmed'] } }
|
| 179 |
-
];
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
const appointments = await appointmentsCollection
|
| 183 |
-
.find(query)
|
| 184 |
-
.sort({ date_time: -1 })
|
| 185 |
-
.toArray();
|
| 186 |
|
| 187 |
const result = [];
|
| 188 |
for (const apt of appointments) {
|
| 189 |
-
|
| 190 |
-
result.push(
|
| 191 |
}
|
| 192 |
|
| 193 |
return result;
|
|
@@ -197,130 +133,102 @@ class AppointmentService {
|
|
| 197 |
* Update an appointment
|
| 198 |
*/
|
| 199 |
async updateAppointment(appointmentId, updateData) {
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
return null;
|
| 210 |
}
|
| 211 |
-
|
| 212 |
-
return await this.getAppointment(appointmentId);
|
| 213 |
}
|
| 214 |
|
| 215 |
/**
|
| 216 |
* Cancel an appointment
|
| 217 |
*/
|
| 218 |
-
async cancelAppointment(appointmentId, reason = null) {
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
$set: {
|
| 225 |
-
status: AppointmentStatus.CANCELLED,
|
| 226 |
-
cancelled_reason: reason,
|
| 227 |
-
updated_at: new Date()
|
| 228 |
-
}
|
| 229 |
}
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
return null;
|
| 234 |
}
|
| 235 |
-
|
| 236 |
-
return await this.getAppointment(appointmentId);
|
| 237 |
}
|
| 238 |
|
| 239 |
/**
|
| 240 |
* Mark an appointment as completed
|
| 241 |
*/
|
| 242 |
-
async completeAppointment(appointmentId, notes = null) {
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
}
|
| 261 |
-
|
| 262 |
-
return await this.getAppointment(appointmentId);
|
| 263 |
}
|
| 264 |
|
| 265 |
/**
|
| 266 |
* Check for scheduling conflicts
|
| 267 |
*/
|
| 268 |
async checkConflicts(doctorId, dateTime, durationMinutes, excludeId = null) {
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
// Parse the proposed datetime
|
| 272 |
-
let proposedStart, proposedEnd;
|
| 273 |
-
try {
|
| 274 |
-
proposedStart = new Date(dateTime.replace('Z', '+00:00'));
|
| 275 |
-
proposedEnd = new Date(proposedStart.getTime() + durationMinutes * 60000);
|
| 276 |
-
} catch {
|
| 277 |
-
return [];
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
// Find overlapping appointments
|
| 281 |
-
const query = {
|
| 282 |
-
doctor_id: doctorId,
|
| 283 |
-
status: { $in: ['scheduled', 'confirmed'] }
|
| 284 |
-
};
|
| 285 |
-
|
| 286 |
-
if (excludeId) {
|
| 287 |
-
query._id = { $ne: new ObjectId(excludeId) };
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
const appointments = await appointmentsCollection.find(query).toArray();
|
| 291 |
-
|
| 292 |
-
const conflicts = [];
|
| 293 |
-
for (const apt of appointments) {
|
| 294 |
-
try {
|
| 295 |
-
const aptStart = new Date(apt.date_time.replace('Z', '+00:00'));
|
| 296 |
-
const aptEnd = new Date(aptStart.getTime() + (apt.duration_minutes || 30) * 60000);
|
| 297 |
-
|
| 298 |
-
// Check for overlap
|
| 299 |
-
if (!(proposedEnd <= aptStart || proposedStart >= aptEnd)) {
|
| 300 |
-
conflicts.push(this._appointmentHelper(apt));
|
| 301 |
-
}
|
| 302 |
-
} catch {
|
| 303 |
-
continue;
|
| 304 |
-
}
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
return conflicts;
|
| 308 |
}
|
| 309 |
|
| 310 |
/**
|
| 311 |
* Generate an AI-powered pre-appointment summary
|
| 312 |
*/
|
| 313 |
async generatePreAppointmentSummary(appointmentId) {
|
| 314 |
-
const
|
| 315 |
-
const patientsCollection = getPatientsCollection();
|
| 316 |
-
|
| 317 |
-
const appointment = await appointmentsCollection.findOne({ _id: new ObjectId(appointmentId) });
|
| 318 |
if (!appointment) {
|
| 319 |
return { error: 'Appointment not found' };
|
| 320 |
}
|
| 321 |
|
| 322 |
// Get patient data
|
| 323 |
-
const patient = await
|
| 324 |
if (!patient) {
|
| 325 |
return { error: 'Patient not found' };
|
| 326 |
}
|
|
@@ -386,34 +294,15 @@ Keep it concise and actionable for a busy doctor.`;
|
|
| 386 |
|
| 387 |
/**
|
| 388 |
* Get appointment statistics
|
|
|
|
| 389 |
*/
|
| 390 |
-
async getAppointmentStats() {
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
const pipeline = [
|
| 394 |
-
{
|
| 395 |
-
$group: {
|
| 396 |
-
_id: '$status',
|
| 397 |
-
count: { $sum: 1 }
|
| 398 |
-
}
|
| 399 |
-
}
|
| 400 |
-
];
|
| 401 |
-
|
| 402 |
-
const results = await appointmentsCollection.aggregate(pipeline).toArray();
|
| 403 |
-
|
| 404 |
-
const stats = {
|
| 405 |
-
total: 0,
|
| 406 |
-
by_status: {}
|
| 407 |
-
};
|
| 408 |
-
|
| 409 |
-
for (const r of results) {
|
| 410 |
-
stats.by_status[r._id] = r.count;
|
| 411 |
-
stats.total += r.count;
|
| 412 |
-
}
|
| 413 |
-
|
| 414 |
-
return stats;
|
| 415 |
}
|
| 416 |
}
|
| 417 |
|
| 418 |
// Singleton instance
|
| 419 |
export const appointmentService = new AppointmentService();
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { generateResponse } from './llmClient.js';
|
| 2 |
+
import Appointment, { AppointmentStatus } from '../models/Appointment.js';
|
| 3 |
+
import Patient from '../models/Patient.js';
|
| 4 |
+
import User from '../models/User.js';
|
| 5 |
import dotenv from 'dotenv';
|
| 6 |
|
| 7 |
dotenv.config();
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
class AppointmentService {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
/**
|
| 11 |
* Add patient and doctor names to appointment
|
| 12 |
*/
|
| 13 |
async _enrichWithNames(appointment) {
|
|
|
|
|
|
|
|
|
|
| 14 |
// Get patient name
|
| 15 |
try {
|
| 16 |
+
const patient = await Patient.findById(appointment.patient_id);
|
| 17 |
appointment.patient_name = patient ? patient.name : null;
|
| 18 |
} catch {
|
| 19 |
appointment.patient_name = null;
|
|
|
|
| 22 |
// Get doctor name
|
| 23 |
if (appointment.doctor_id) {
|
| 24 |
try {
|
| 25 |
+
const doctor = await User.findById(appointment.doctor_id);
|
| 26 |
appointment.doctor_name = doctor ? doctor.name : null;
|
| 27 |
} catch {
|
| 28 |
appointment.doctor_name = null;
|
|
|
|
| 36 |
* Create a new appointment
|
| 37 |
*/
|
| 38 |
async createAppointment(appointmentData, userId) {
|
| 39 |
+
const appointment = new Appointment({
|
|
|
|
|
|
|
|
|
|
| 40 |
...appointmentData,
|
| 41 |
status: AppointmentStatus.SCHEDULED,
|
|
|
|
|
|
|
| 42 |
created_by: userId
|
| 43 |
+
});
|
| 44 |
|
| 45 |
+
await appointment.save();
|
| 46 |
+
await this._enrichWithNames(appointment);
|
|
|
|
| 47 |
|
| 48 |
+
return appointment.toResponse();
|
| 49 |
}
|
| 50 |
|
| 51 |
/**
|
|
|
|
| 53 |
*/
|
| 54 |
async getAppointment(appointmentId) {
|
| 55 |
try {
|
| 56 |
+
const appointment = await Appointment.findById(appointmentId);
|
|
|
|
| 57 |
|
| 58 |
if (appointment) {
|
| 59 |
+
await this._enrichWithNames(appointment);
|
| 60 |
+
return appointment.toResponse();
|
| 61 |
}
|
| 62 |
return null;
|
| 63 |
} catch {
|
|
|
|
| 69 |
* List appointments with optional filters
|
| 70 |
*/
|
| 71 |
async listAppointments({ patientId, doctorId, status, fromDate, toDate, limit = 50 }) {
|
|
|
|
| 72 |
const query = {};
|
| 73 |
|
| 74 |
if (patientId) query.patient_id = patientId;
|
|
|
|
| 81 |
if (toDate) query.date_time.$lte = toDate;
|
| 82 |
}
|
| 83 |
|
| 84 |
+
const appointments = await Appointment.find(query)
|
|
|
|
| 85 |
.sort({ date_time: 1 })
|
| 86 |
+
.limit(limit);
|
|
|
|
| 87 |
|
| 88 |
const result = [];
|
| 89 |
for (const apt of appointments) {
|
| 90 |
+
await this._enrichWithNames(apt);
|
| 91 |
+
result.push(apt.toResponse());
|
| 92 |
}
|
| 93 |
|
| 94 |
return result;
|
|
|
|
| 96 |
|
| 97 |
/**
|
| 98 |
* Get upcoming appointments
|
| 99 |
+
* @param limit - number of appointments to return
|
| 100 |
+
* @param doctorId - optional filter by doctor (for non-admin users)
|
| 101 |
*/
|
| 102 |
+
async getUpcomingAppointments(limit = 20, doctorId = null) {
|
| 103 |
+
const appointments = await Appointment.findUpcoming(limit, doctorId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
const result = [];
|
| 106 |
for (const apt of appointments) {
|
| 107 |
+
await this._enrichWithNames(apt);
|
| 108 |
+
result.push(apt.toResponse());
|
| 109 |
}
|
| 110 |
|
| 111 |
return result;
|
|
|
|
| 113 |
|
| 114 |
/**
|
| 115 |
* Get all appointments for a specific patient
|
| 116 |
+
* @param patientId - patient to get appointments for
|
| 117 |
+
* @param includePast - whether to include past appointments
|
| 118 |
+
* @param doctorId - optional filter by doctor (for non-admin users)
|
| 119 |
*/
|
| 120 |
+
async getPatientAppointments(patientId, includePast = false, doctorId = null) {
|
| 121 |
+
const appointments = await Appointment.findByPatient(patientId, includePast, doctorId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
const result = [];
|
| 124 |
for (const apt of appointments) {
|
| 125 |
+
await this._enrichWithNames(apt);
|
| 126 |
+
result.push(apt.toResponse());
|
| 127 |
}
|
| 128 |
|
| 129 |
return result;
|
|
|
|
| 133 |
* Update an appointment
|
| 134 |
*/
|
| 135 |
async updateAppointment(appointmentId, updateData) {
|
| 136 |
+
try {
|
| 137 |
+
const appointment = await Appointment.findByIdAndUpdate(
|
| 138 |
+
appointmentId,
|
| 139 |
+
{ $set: updateData },
|
| 140 |
+
{ new: true, runValidators: true }
|
| 141 |
+
);
|
| 142 |
+
|
| 143 |
+
if (!appointment) {
|
| 144 |
+
return null;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
await this._enrichWithNames(appointment);
|
| 148 |
+
return appointment.toResponse();
|
| 149 |
+
} catch {
|
| 150 |
return null;
|
| 151 |
}
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
/**
|
| 155 |
* Cancel an appointment
|
| 156 |
*/
|
| 157 |
+
async cancelAppointment(appointmentId, reason = null, cancelledBy = null) {
|
| 158 |
+
try {
|
| 159 |
+
const appointment = await Appointment.findById(appointmentId);
|
| 160 |
+
|
| 161 |
+
if (!appointment) {
|
| 162 |
+
return null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
}
|
| 164 |
+
|
| 165 |
+
appointment.status = AppointmentStatus.CANCELLED;
|
| 166 |
+
appointment.cancelled_reason = reason;
|
| 167 |
+
appointment.cancelled_at = new Date();
|
| 168 |
+
appointment.cancelled_by = cancelledBy;
|
| 169 |
+
|
| 170 |
+
await appointment.save();
|
| 171 |
+
await this._enrichWithNames(appointment);
|
| 172 |
+
|
| 173 |
+
return appointment.toResponse();
|
| 174 |
+
} catch {
|
| 175 |
return null;
|
| 176 |
}
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
|
| 179 |
/**
|
| 180 |
* Mark an appointment as completed
|
| 181 |
*/
|
| 182 |
+
async completeAppointment(appointmentId, notes = null, completedBy = null) {
|
| 183 |
+
try {
|
| 184 |
+
const appointment = await Appointment.findById(appointmentId);
|
| 185 |
+
|
| 186 |
+
if (!appointment) {
|
| 187 |
+
return { error: 'Appointment not found', notFound: true };
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Validate that appointment date has passed
|
| 191 |
+
const appointmentDate = new Date(appointment.date_time);
|
| 192 |
+
const now = new Date();
|
| 193 |
+
if (appointmentDate > now) {
|
| 194 |
+
return {
|
| 195 |
+
error: `Cannot complete this appointment yet. It is scheduled for ${appointmentDate.toISOString()}.`,
|
| 196 |
+
dateError: true
|
| 197 |
+
};
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
appointment.status = AppointmentStatus.COMPLETED;
|
| 201 |
+
appointment.completion_notes = notes;
|
| 202 |
+
appointment.completed_at = new Date();
|
| 203 |
+
appointment.completed_by = completedBy;
|
| 204 |
+
|
| 205 |
+
await appointment.save();
|
| 206 |
+
await this._enrichWithNames(appointment);
|
| 207 |
+
|
| 208 |
+
return appointment.toResponse();
|
| 209 |
+
} catch (error) {
|
| 210 |
+
return { error: error.message };
|
| 211 |
}
|
|
|
|
|
|
|
| 212 |
}
|
| 213 |
|
| 214 |
/**
|
| 215 |
* Check for scheduling conflicts
|
| 216 |
*/
|
| 217 |
async checkConflicts(doctorId, dateTime, durationMinutes, excludeId = null) {
|
| 218 |
+
return await Appointment.checkConflicts(doctorId, dateTime, durationMinutes, excludeId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
}
|
| 220 |
|
| 221 |
/**
|
| 222 |
* Generate an AI-powered pre-appointment summary
|
| 223 |
*/
|
| 224 |
async generatePreAppointmentSummary(appointmentId) {
|
| 225 |
+
const appointment = await Appointment.findById(appointmentId);
|
|
|
|
|
|
|
|
|
|
| 226 |
if (!appointment) {
|
| 227 |
return { error: 'Appointment not found' };
|
| 228 |
}
|
| 229 |
|
| 230 |
// Get patient data
|
| 231 |
+
const patient = await Patient.findById(appointment.patient_id);
|
| 232 |
if (!patient) {
|
| 233 |
return { error: 'Patient not found' };
|
| 234 |
}
|
|
|
|
| 294 |
|
| 295 |
/**
|
| 296 |
* Get appointment statistics
|
| 297 |
+
* @param doctorId - optional filter by doctor (for non-admin users)
|
| 298 |
*/
|
| 299 |
+
async getAppointmentStats(doctorId = null) {
|
| 300 |
+
return await Appointment.getStats(doctorId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
}
|
| 302 |
}
|
| 303 |
|
| 304 |
// Singleton instance
|
| 305 |
export const appointmentService = new AppointmentService();
|
| 306 |
+
|
| 307 |
+
// Re-export AppointmentStatus for backward compatibility
|
| 308 |
+
export { AppointmentStatus };
|
services/authService.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import jwt from 'jsonwebtoken';
|
| 2 |
import bcrypt from 'bcryptjs';
|
| 3 |
-
import
|
| 4 |
import dotenv from 'dotenv';
|
| 5 |
|
| 6 |
dotenv.config();
|
|
@@ -60,8 +60,7 @@ class AuthService {
|
|
| 60 |
* Get a user by email
|
| 61 |
*/
|
| 62 |
async getUserByEmail(email) {
|
| 63 |
-
|
| 64 |
-
return await usersCollection.findOne({ email });
|
| 65 |
}
|
| 66 |
|
| 67 |
/**
|
|
@@ -69,8 +68,7 @@ class AuthService {
|
|
| 69 |
*/
|
| 70 |
async getUserById(userId) {
|
| 71 |
try {
|
| 72 |
-
|
| 73 |
-
return await usersCollection.findOne({ _id: new ObjectId(userId) });
|
| 74 |
} catch (error) {
|
| 75 |
return null;
|
| 76 |
}
|
|
@@ -80,29 +78,23 @@ class AuthService {
|
|
| 80 |
* Create a new user
|
| 81 |
*/
|
| 82 |
async createUser(userData) {
|
| 83 |
-
const usersCollection = getUsersCollection();
|
| 84 |
-
|
| 85 |
// Check if user already exists
|
| 86 |
-
const existingUser = await this.getUserByEmail(userData.email);
|
| 87 |
-
if (existingUser) {
|
| 88 |
-
|
| 89 |
-
}
|
| 90 |
|
| 91 |
-
// Create user
|
| 92 |
-
const
|
| 93 |
-
const userDoc = {
|
| 94 |
email: userData.email,
|
| 95 |
name: userData.name,
|
| 96 |
role: userData.role || 'doctor',
|
| 97 |
-
hashed_password:
|
| 98 |
-
created_at: now,
|
| 99 |
-
updated_at: now,
|
| 100 |
is_active: true
|
| 101 |
-
};
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
return userDoc;
|
| 106 |
}
|
| 107 |
|
| 108 |
/**
|
|
@@ -113,9 +105,12 @@ class AuthService {
|
|
| 113 |
if (!user) {
|
| 114 |
return null;
|
| 115 |
}
|
| 116 |
-
|
|
|
|
|
|
|
| 117 |
return null;
|
| 118 |
}
|
|
|
|
| 119 |
return user;
|
| 120 |
}
|
| 121 |
|
|
@@ -123,6 +118,9 @@ class AuthService {
|
|
| 123 |
* Convert a user document to a response object
|
| 124 |
*/
|
| 125 |
userToResponse(user) {
|
|
|
|
|
|
|
|
|
|
| 126 |
return {
|
| 127 |
id: user._id.toString(),
|
| 128 |
email: user.email,
|
|
@@ -137,22 +135,51 @@ class AuthService {
|
|
| 137 |
* Update a user's information
|
| 138 |
*/
|
| 139 |
async updateUser(userId, updateData) {
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
if (result.matchedCount === 0) {
|
| 149 |
return null;
|
| 150 |
}
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
}
|
| 154 |
}
|
| 155 |
|
| 156 |
// Singleton instance
|
| 157 |
export const authService = new AuthService();
|
| 158 |
-
|
|
|
|
| 1 |
import jwt from 'jsonwebtoken';
|
| 2 |
import bcrypt from 'bcryptjs';
|
| 3 |
+
import User from '../models/User.js';
|
| 4 |
import dotenv from 'dotenv';
|
| 5 |
|
| 6 |
dotenv.config();
|
|
|
|
| 60 |
* Get a user by email
|
| 61 |
*/
|
| 62 |
async getUserByEmail(email) {
|
| 63 |
+
return await User.findByEmail(email);
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
/**
|
|
|
|
| 68 |
*/
|
| 69 |
async getUserById(userId) {
|
| 70 |
try {
|
| 71 |
+
return await User.findById(userId);
|
|
|
|
| 72 |
} catch (error) {
|
| 73 |
return null;
|
| 74 |
}
|
|
|
|
| 78 |
* Create a new user
|
| 79 |
*/
|
| 80 |
async createUser(userData) {
|
|
|
|
|
|
|
| 81 |
// Check if user already exists
|
| 82 |
+
// const existingUser = await this.getUserByEmail(userData.email);
|
| 83 |
+
// if (existingUser) {
|
| 84 |
+
// throw new Error('User with this email already exists');
|
| 85 |
+
// }
|
| 86 |
|
| 87 |
+
// Create user with Mongoose model
|
| 88 |
+
const user = new User({
|
|
|
|
| 89 |
email: userData.email,
|
| 90 |
name: userData.name,
|
| 91 |
role: userData.role || 'doctor',
|
| 92 |
+
hashed_password: userData.password, // Will be hashed by pre-save hook
|
|
|
|
|
|
|
| 93 |
is_active: true
|
| 94 |
+
});
|
| 95 |
|
| 96 |
+
await user.save();
|
| 97 |
+
return user;
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
/**
|
|
|
|
| 105 |
if (!user) {
|
| 106 |
return null;
|
| 107 |
}
|
| 108 |
+
|
| 109 |
+
const isValid = await user.comparePassword(password);
|
| 110 |
+
if (!isValid) {
|
| 111 |
return null;
|
| 112 |
}
|
| 113 |
+
|
| 114 |
return user;
|
| 115 |
}
|
| 116 |
|
|
|
|
| 118 |
* Convert a user document to a response object
|
| 119 |
*/
|
| 120 |
userToResponse(user) {
|
| 121 |
+
if (user.toResponse) {
|
| 122 |
+
return user.toResponse();
|
| 123 |
+
}
|
| 124 |
return {
|
| 125 |
id: user._id.toString(),
|
| 126 |
email: user.email,
|
|
|
|
| 135 |
* Update a user's information
|
| 136 |
*/
|
| 137 |
async updateUser(userId, updateData) {
|
| 138 |
+
try {
|
| 139 |
+
const user = await User.findByIdAndUpdate(
|
| 140 |
+
userId,
|
| 141 |
+
{ $set: updateData },
|
| 142 |
+
{ new: true, runValidators: true }
|
| 143 |
+
);
|
| 144 |
+
return user;
|
| 145 |
+
} catch (error) {
|
|
|
|
| 146 |
return null;
|
| 147 |
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/**
|
| 151 |
+
* Delete a user by ID
|
| 152 |
+
*/
|
| 153 |
+
async deleteUser(userId) {
|
| 154 |
+
try {
|
| 155 |
+
const result = await User.findByIdAndDelete(userId);
|
| 156 |
+
return result !== null;
|
| 157 |
+
} catch (error) {
|
| 158 |
+
return false;
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/**
|
| 163 |
+
* Get all users
|
| 164 |
+
*/
|
| 165 |
+
async getAllUsers() {
|
| 166 |
+
return await User.find().sort({ created_at: -1 });
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/**
|
| 170 |
+
* Get all doctors
|
| 171 |
+
*/
|
| 172 |
+
async getDoctors() {
|
| 173 |
+
return await User.findActiveDoctors();
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/**
|
| 177 |
+
* Get all admins
|
| 178 |
+
*/
|
| 179 |
+
async getAdmins() {
|
| 180 |
+
return await User.findActiveAdmins();
|
| 181 |
}
|
| 182 |
}
|
| 183 |
|
| 184 |
// Singleton instance
|
| 185 |
export const authService = new AuthService();
|
|
|
services/timelineService.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
import { generateResponse } from './llmClient.js';
|
| 2 |
-
import
|
|
|
|
|
|
|
| 3 |
import dotenv from 'dotenv';
|
| 4 |
|
| 5 |
dotenv.config();
|
|
@@ -52,11 +54,7 @@ class TimelineService {
|
|
| 52 |
* Generate a chronological timeline of all patient events
|
| 53 |
*/
|
| 54 |
async getPatientTimeline(patientId) {
|
| 55 |
-
const
|
| 56 |
-
const appointmentsCollection = getAppointmentsCollection();
|
| 57 |
-
const warningsCollection = getWarningsCollection();
|
| 58 |
-
|
| 59 |
-
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 60 |
if (!patient) {
|
| 61 |
return [];
|
| 62 |
}
|
|
@@ -140,7 +138,7 @@ class TimelineService {
|
|
| 140 |
|
| 141 |
// Process appointments
|
| 142 |
try {
|
| 143 |
-
const appointments = await
|
| 144 |
for (const apt of appointments) {
|
| 145 |
let aptDate = apt.date_time;
|
| 146 |
if (typeof aptDate === 'string') {
|
|
@@ -164,10 +162,10 @@ class TimelineService {
|
|
| 164 |
|
| 165 |
// Process warnings (only acknowledged ones for history)
|
| 166 |
try {
|
| 167 |
-
const warnings = await
|
| 168 |
patient_id: patientId,
|
| 169 |
is_acknowledged: true
|
| 170 |
-
})
|
| 171 |
|
| 172 |
for (const warning of warnings) {
|
| 173 |
const createdAt = warning.created_at;
|
|
@@ -201,8 +199,7 @@ class TimelineService {
|
|
| 201 |
* Generate an AI-powered summary of the patient's medical timeline
|
| 202 |
*/
|
| 203 |
async generateTimelineSummary(patientId) {
|
| 204 |
-
const
|
| 205 |
-
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 206 |
|
| 207 |
if (!patient) {
|
| 208 |
return { error: 'Patient not found' };
|
|
|
|
| 1 |
import { generateResponse } from './llmClient.js';
|
| 2 |
+
import Patient from '../models/Patient.js';
|
| 3 |
+
import Appointment from '../models/Appointment.js';
|
| 4 |
+
import Warning from '../models/Warning.js';
|
| 5 |
import dotenv from 'dotenv';
|
| 6 |
|
| 7 |
dotenv.config();
|
|
|
|
| 54 |
* Generate a chronological timeline of all patient events
|
| 55 |
*/
|
| 56 |
async getPatientTimeline(patientId) {
|
| 57 |
+
const patient = await Patient.findById(patientId);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
if (!patient) {
|
| 59 |
return [];
|
| 60 |
}
|
|
|
|
| 138 |
|
| 139 |
// Process appointments
|
| 140 |
try {
|
| 141 |
+
const appointments = await Appointment.find({ patient_id: patientId });
|
| 142 |
for (const apt of appointments) {
|
| 143 |
let aptDate = apt.date_time;
|
| 144 |
if (typeof aptDate === 'string') {
|
|
|
|
| 162 |
|
| 163 |
// Process warnings (only acknowledged ones for history)
|
| 164 |
try {
|
| 165 |
+
const warnings = await Warning.find({
|
| 166 |
patient_id: patientId,
|
| 167 |
is_acknowledged: true
|
| 168 |
+
});
|
| 169 |
|
| 170 |
for (const warning of warnings) {
|
| 171 |
const createdAt = warning.created_at;
|
|
|
|
| 199 |
* Generate an AI-powered summary of the patient's medical timeline
|
| 200 |
*/
|
| 201 |
async generateTimelineSummary(patientId) {
|
| 202 |
+
const patient = await Patient.findById(patientId);
|
|
|
|
| 203 |
|
| 204 |
if (!patient) {
|
| 205 |
return { error: 'Patient not found' };
|
services/vectorService.js
CHANGED
|
@@ -19,9 +19,9 @@ class VectorService {
|
|
| 19 |
this.patientCollection = await this._getOrCreateCollection('patient_records');
|
| 20 |
this.researchCollection = await this._getOrCreateCollection('medical_research');
|
| 21 |
this.initialized = true;
|
| 22 |
-
console.log('
|
| 23 |
} catch (error) {
|
| 24 |
-
console.log(`
|
| 25 |
}
|
| 26 |
}
|
| 27 |
|
|
@@ -108,9 +108,9 @@ class VectorService {
|
|
| 108 |
metadatas,
|
| 109 |
ids
|
| 110 |
});
|
| 111 |
-
console.log(`
|
| 112 |
} catch (error) {
|
| 113 |
-
console.log(`
|
| 114 |
}
|
| 115 |
}
|
| 116 |
|
|
|
|
| 19 |
this.patientCollection = await this._getOrCreateCollection('patient_records');
|
| 20 |
this.researchCollection = await this._getOrCreateCollection('medical_research');
|
| 21 |
this.initialized = true;
|
| 22 |
+
console.log('[OK] Vector database initialized');
|
| 23 |
} catch (error) {
|
| 24 |
+
console.log(`[WARNING] Vector database initialization warning: ${error.message}`);
|
| 25 |
}
|
| 26 |
}
|
| 27 |
|
|
|
|
| 108 |
metadatas,
|
| 109 |
ids
|
| 110 |
});
|
| 111 |
+
console.log(` [OK] Indexed ${documents.length} records for patient ${patientData.name}`);
|
| 112 |
} catch (error) {
|
| 113 |
+
console.log(` [ERROR] Error indexing patient ${patientData.name}: ${error.message}`);
|
| 114 |
}
|
| 115 |
}
|
| 116 |
|
services/warningService.js
CHANGED
|
@@ -1,132 +1,62 @@
|
|
| 1 |
import { generateResponse } from './llmClient.js';
|
| 2 |
-
import {
|
|
|
|
| 3 |
import dotenv from 'dotenv';
|
| 4 |
|
| 5 |
dotenv.config();
|
| 6 |
|
| 7 |
-
//
|
| 8 |
-
export
|
| 9 |
-
DRUG_INTERACTION: 'drug_interaction',
|
| 10 |
-
ALLERGY: 'allergy',
|
| 11 |
-
CONTRAINDICATION: 'contraindication',
|
| 12 |
-
ABNORMAL_PATTERN: 'abnormal_pattern',
|
| 13 |
-
DOSAGE_ALERT: 'dosage_alert',
|
| 14 |
-
DUPLICATE_THERAPY: 'duplicate_therapy'
|
| 15 |
-
};
|
| 16 |
-
|
| 17 |
-
// Warning severity enum
|
| 18 |
-
export const WarningSeverity = {
|
| 19 |
-
LOW: 'low',
|
| 20 |
-
MEDIUM: 'medium',
|
| 21 |
-
HIGH: 'high',
|
| 22 |
-
CRITICAL: 'critical'
|
| 23 |
-
};
|
| 24 |
|
| 25 |
class WarningService {
|
| 26 |
-
/**
|
| 27 |
-
* Convert MongoDB warning document to response format
|
| 28 |
-
*/
|
| 29 |
-
_warningHelper(warning) {
|
| 30 |
-
return {
|
| 31 |
-
id: warning._id.toString(),
|
| 32 |
-
patient_id: warning.patient_id,
|
| 33 |
-
warning_type: warning.warning_type,
|
| 34 |
-
severity: warning.severity,
|
| 35 |
-
title: warning.title,
|
| 36 |
-
description: warning.description,
|
| 37 |
-
related_medications: warning.related_medications || [],
|
| 38 |
-
related_conditions: warning.related_conditions || [],
|
| 39 |
-
recommendation: warning.recommendation || null,
|
| 40 |
-
created_at: warning.created_at,
|
| 41 |
-
is_acknowledged: warning.is_acknowledged || false,
|
| 42 |
-
acknowledged_by: warning.acknowledged_by || null,
|
| 43 |
-
acknowledged_at: warning.acknowledged_at || null
|
| 44 |
-
};
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
/**
|
| 48 |
* Get all warnings for a patient
|
| 49 |
*/
|
| 50 |
async getPatientWarnings(patientId, includeAcknowledged = false) {
|
| 51 |
-
const
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
if (!includeAcknowledged) {
|
| 55 |
-
query.is_acknowledged = false;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
const warnings = await warningsCollection
|
| 59 |
-
.find(query)
|
| 60 |
-
.sort({ created_at: -1 })
|
| 61 |
-
.toArray();
|
| 62 |
-
|
| 63 |
-
return warnings.map(w => this._warningHelper(w));
|
| 64 |
}
|
| 65 |
|
| 66 |
/**
|
| 67 |
* Get all unacknowledged warnings across all patients
|
| 68 |
*/
|
| 69 |
async getAllUnacknowledgedWarnings(limit = 50) {
|
| 70 |
-
const
|
| 71 |
-
|
| 72 |
-
const warnings = await warningsCollection
|
| 73 |
-
.find({ is_acknowledged: false })
|
| 74 |
-
.sort({ severity: -1, created_at: -1 })
|
| 75 |
-
.limit(limit)
|
| 76 |
-
.toArray();
|
| 77 |
-
|
| 78 |
-
return warnings.map(w => this._warningHelper(w));
|
| 79 |
}
|
| 80 |
|
| 81 |
/**
|
| 82 |
* Acknowledge a clinical warning
|
| 83 |
*/
|
| 84 |
async acknowledgeWarning(warningId, userId, notes = null) {
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
$set: {
|
| 91 |
-
is_acknowledged: true,
|
| 92 |
-
acknowledged_by: userId,
|
| 93 |
-
acknowledged_at: new Date(),
|
| 94 |
-
acknowledgement_notes: notes
|
| 95 |
-
}
|
| 96 |
}
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
| 100 |
return null;
|
| 101 |
}
|
| 102 |
-
|
| 103 |
-
const warning = await warningsCollection.findOne({ _id: new ObjectId(warningId) });
|
| 104 |
-
return this._warningHelper(warning);
|
| 105 |
}
|
| 106 |
|
| 107 |
/**
|
| 108 |
* Create a new clinical warning
|
| 109 |
*/
|
| 110 |
async createWarning(warningData) {
|
| 111 |
-
const
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
...warningData,
|
| 115 |
-
created_at: new Date(),
|
| 116 |
-
is_acknowledged: false
|
| 117 |
-
};
|
| 118 |
-
|
| 119 |
-
const result = await warningsCollection.insertOne(warningDoc);
|
| 120 |
-
warningDoc._id = result.insertedId;
|
| 121 |
-
return this._warningHelper(warningDoc);
|
| 122 |
}
|
| 123 |
|
| 124 |
/**
|
| 125 |
* Delete all warnings for a patient
|
| 126 |
*/
|
| 127 |
async deletePatientWarnings(patientId) {
|
| 128 |
-
const
|
| 129 |
-
const result = await warningsCollection.deleteMany({ patient_id: patientId });
|
| 130 |
return result.deletedCount;
|
| 131 |
}
|
| 132 |
|
|
@@ -134,8 +64,7 @@ class WarningService {
|
|
| 134 |
* Use AI to analyze patient data and generate clinical warnings
|
| 135 |
*/
|
| 136 |
async analyzePatientForWarnings(patientId, newMedication = null, newCondition = null) {
|
| 137 |
-
const
|
| 138 |
-
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 139 |
|
| 140 |
if (!patient) {
|
| 141 |
return [];
|
|
@@ -209,21 +138,22 @@ Return ONLY valid JSON, no markdown or other text.`;
|
|
| 209 |
|
| 210 |
// Create warnings in database
|
| 211 |
const createdWarnings = [];
|
| 212 |
-
for (const
|
| 213 |
try {
|
| 214 |
-
const
|
| 215 |
patient_id: patientId,
|
| 216 |
-
warning_type:
|
| 217 |
-
severity:
|
| 218 |
-
title: (
|
| 219 |
-
description:
|
| 220 |
-
related_medications:
|
| 221 |
-
related_conditions:
|
| 222 |
-
|
| 223 |
-
|
|
|
|
| 224 |
|
| 225 |
-
|
| 226 |
-
createdWarnings.push(
|
| 227 |
} catch (error) {
|
| 228 |
console.log(`Error creating warning: ${error.message}`);
|
| 229 |
continue;
|
|
@@ -245,47 +175,7 @@ Return ONLY valid JSON, no markdown or other text.`;
|
|
| 245 |
* Get warning statistics
|
| 246 |
*/
|
| 247 |
async getWarningStats() {
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
const pipeline = [
|
| 251 |
-
{
|
| 252 |
-
$group: {
|
| 253 |
-
_id: {
|
| 254 |
-
severity: '$severity',
|
| 255 |
-
acknowledged: '$is_acknowledged'
|
| 256 |
-
},
|
| 257 |
-
count: { $sum: 1 }
|
| 258 |
-
}
|
| 259 |
-
}
|
| 260 |
-
];
|
| 261 |
-
|
| 262 |
-
const results = await warningsCollection.aggregate(pipeline).toArray();
|
| 263 |
-
|
| 264 |
-
const stats = {
|
| 265 |
-
total: 0,
|
| 266 |
-
unacknowledged: 0,
|
| 267 |
-
by_severity: {
|
| 268 |
-
critical: 0,
|
| 269 |
-
high: 0,
|
| 270 |
-
medium: 0,
|
| 271 |
-
low: 0
|
| 272 |
-
}
|
| 273 |
-
};
|
| 274 |
-
|
| 275 |
-
for (const r of results) {
|
| 276 |
-
const count = r.count;
|
| 277 |
-
stats.total += count;
|
| 278 |
-
|
| 279 |
-
if (!r._id.acknowledged) {
|
| 280 |
-
stats.unacknowledged += count;
|
| 281 |
-
const severity = r._id.severity;
|
| 282 |
-
if (severity in stats.by_severity) {
|
| 283 |
-
stats.by_severity[severity] += count;
|
| 284 |
-
}
|
| 285 |
-
}
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
return stats;
|
| 289 |
}
|
| 290 |
}
|
| 291 |
|
|
|
|
| 1 |
import { generateResponse } from './llmClient.js';
|
| 2 |
+
import Warning, { WarningType, WarningSeverity } from '../models/Warning.js';
|
| 3 |
+
import Patient from '../models/Patient.js';
|
| 4 |
import dotenv from 'dotenv';
|
| 5 |
|
| 6 |
dotenv.config();
|
| 7 |
|
| 8 |
+
// Re-export enums for backward compatibility
|
| 9 |
+
export { WarningType, WarningSeverity };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
class WarningService {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
/**
|
| 13 |
* Get all warnings for a patient
|
| 14 |
*/
|
| 15 |
async getPatientWarnings(patientId, includeAcknowledged = false) {
|
| 16 |
+
const warnings = await Warning.findByPatient(patientId, includeAcknowledged);
|
| 17 |
+
return warnings.map(w => w.toResponse());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
/**
|
| 21 |
* Get all unacknowledged warnings across all patients
|
| 22 |
*/
|
| 23 |
async getAllUnacknowledgedWarnings(limit = 50) {
|
| 24 |
+
const warnings = await Warning.findUnacknowledged(limit);
|
| 25 |
+
return warnings.map(w => w.toResponse());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
/**
|
| 29 |
* Acknowledge a clinical warning
|
| 30 |
*/
|
| 31 |
async acknowledgeWarning(warningId, userId, notes = null) {
|
| 32 |
+
try {
|
| 33 |
+
const warning = await Warning.findById(warningId);
|
| 34 |
+
|
| 35 |
+
if (!warning) {
|
| 36 |
+
return null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
+
|
| 39 |
+
await warning.acknowledge(userId, notes);
|
| 40 |
+
return warning.toResponse();
|
| 41 |
+
} catch {
|
| 42 |
return null;
|
| 43 |
}
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
/**
|
| 47 |
* Create a new clinical warning
|
| 48 |
*/
|
| 49 |
async createWarning(warningData) {
|
| 50 |
+
const warning = new Warning(warningData);
|
| 51 |
+
await warning.save();
|
| 52 |
+
return warning.toResponse();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
/**
|
| 56 |
* Delete all warnings for a patient
|
| 57 |
*/
|
| 58 |
async deletePatientWarnings(patientId) {
|
| 59 |
+
const result = await Warning.deleteMany({ patient_id: patientId });
|
|
|
|
| 60 |
return result.deletedCount;
|
| 61 |
}
|
| 62 |
|
|
|
|
| 64 |
* Use AI to analyze patient data and generate clinical warnings
|
| 65 |
*/
|
| 66 |
async analyzePatientForWarnings(patientId, newMedication = null, newCondition = null) {
|
| 67 |
+
const patient = await Patient.findById(patientId);
|
|
|
|
| 68 |
|
| 69 |
if (!patient) {
|
| 70 |
return [];
|
|
|
|
| 138 |
|
| 139 |
// Create warnings in database
|
| 140 |
const createdWarnings = [];
|
| 141 |
+
for (const warningData of warningsData) {
|
| 142 |
try {
|
| 143 |
+
const warning = new Warning({
|
| 144 |
patient_id: patientId,
|
| 145 |
+
warning_type: warningData.warning_type || 'general',
|
| 146 |
+
severity: warningData.severity || 'medium',
|
| 147 |
+
title: (warningData.title || 'Clinical Warning').substring(0, 100),
|
| 148 |
+
description: warningData.description || '',
|
| 149 |
+
related_medications: warningData.related_medications || [],
|
| 150 |
+
related_conditions: warningData.related_conditions || [],
|
| 151 |
+
recommendations: warningData.recommendation ? [warningData.recommendation] : [],
|
| 152 |
+
source: 'ai_analysis'
|
| 153 |
+
});
|
| 154 |
|
| 155 |
+
await warning.save();
|
| 156 |
+
createdWarnings.push(warning.toResponse());
|
| 157 |
} catch (error) {
|
| 158 |
console.log(`Error creating warning: ${error.message}`);
|
| 159 |
continue;
|
|
|
|
| 175 |
* Get warning statistics
|
| 176 |
*/
|
| 177 |
async getWarningStats() {
|
| 178 |
+
return await Warning.getStats();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
}
|
| 180 |
}
|
| 181 |
|