aliroohan179 commited on
Commit
5266fc5
·
1 Parent(s): cd5e33d
db/mongo.js CHANGED
@@ -1,73 +1,138 @@
1
- import { MongoClient, ObjectId } from 'mongodb';
2
  import dotenv from 'dotenv';
3
 
4
  dotenv.config();
5
 
6
  const MONGO_URI = process.env.MONGO_URI;
7
 
8
- let client;
9
- let db;
10
 
11
- // Collections
12
- let patientsCollection;
13
- let usersCollection;
14
- let appointmentsCollection;
15
- let warningsCollection;
16
 
17
  export const connectDB = async () => {
 
 
 
 
 
18
  try {
19
- client = new MongoClient(MONGO_URI, {
20
- serverSelectionTimeoutMS: 5000
 
21
  });
 
 
 
22
 
23
- await client.connect();
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(`⚠️ MongoDB connection warning: ${error.message}`);
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
- await usersCollection.createIndex({ email: 1 }, { unique: true });
60
- console.log('✓ Database indexes created');
 
 
 
 
 
 
 
 
 
 
 
61
  } catch (error) {
62
- console.log(`⚠️ Could not create indexes: ${error.message}`);
63
  }
64
  };
65
 
66
- export const getDB = () => db;
67
- export const getPatientsCollection = () => patientsCollection;
68
- export const getUsersCollection = () => usersCollection;
69
- export const getAppointmentsCollection = () => appointmentsCollection;
70
- export const getWarningsCollection = () => warningsCollection;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- export { ObjectId };
 
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 requireDoctor = requireRoles([UserRole.DOCTOR]);
91
- export const requireStaff = requireRoles([UserRole.DOCTOR]);
 
 
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 { ObjectId } from '../db/mongo.js';
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
- try {
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: doctor_id,
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 stats = await appointmentService.getAppointmentStats();
 
 
 
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
- const appointment = await appointmentService.cancelAppointment(appointmentId, reason);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const appointment = await appointmentService.completeAppointment(appointmentId, notes);
 
 
 
 
 
 
 
 
222
 
223
- if (!appointment) {
 
 
 
 
 
 
224
  return res.status(404).json({ detail: 'Appointment not found' });
225
  }
226
 
227
- res.json(appointment);
 
 
 
 
 
 
 
 
 
 
 
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 { ObjectId, getUsersCollection } from '../db/mongo.js';
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
- const validRoles = [UserRole.DOCTOR, UserRole.PATIENT];
 
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, role } = req.body;
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 (Doctor only)
128
  */
129
- router.get('/users', requireDoctor, async (req, res) => {
130
  try {
131
- const usersCollection = getUsersCollection();
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 (Doctor only)
142
  */
143
- router.delete('/users/:userId', requireDoctor, async (req, res) => {
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 usersCollection = getUsersCollection();
153
- const result = await usersCollection.deleteOne({ _id: new ObjectId(userId) });
154
 
155
- if (result.deletedCount === 0) {
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 { ObjectId, getPatientsCollection } from '../db/mongo.js';
 
 
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
- try {
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
- // Validation
47
- if (!patientData.name || patientData.name.length < 1) {
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 result = await patientsCollection.insertOne(patientData);
55
- const patientId = result.insertedId.toString();
 
 
56
 
57
  // Index patient data in vector database
58
  try {
59
- await vectorService.addPatientRecord(patientId, patientData);
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 patientsCollection = getPatientsCollection();
77
- const patients = await patientsCollection.find().toArray();
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 patientsCollection = getPatientsCollection();
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(patientHelper(patient));
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 result = await patientsCollection.updateOne(
148
- { _id: new ObjectId(patientId) },
149
- { $set: updateData }
 
150
  );
151
 
152
- if (result.matchedCount === 0) {
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, patientHelper(updatedPatient));
162
  } catch (error) {
163
  console.log(`Warning: Failed to re-index patient in vector DB: ${error.message}`);
164
  }
165
 
166
- res.json(patientHelper(updatedPatient));
167
  } catch (error) {
 
 
 
 
168
  res.status(500).json({ detail: `Error updating patient: ${error.message}` });
169
  }
170
  });
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  /**
173
  * DELETE /api/patients/:patientId
174
- * Delete a patient
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 patientsCollection = getPatientsCollection();
185
- const result = await patientsCollection.deleteOne({ _id: new ObjectId(patientId) });
186
 
187
- if (result.deletedCount === 0) {
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 patientsCollection = getPatientsCollection();
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 patientsCollection = getPatientsCollection();
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
- patient._id = patient._id.toString();
253
- const summary = await ragService.generateRAGResponse(patientId, patient, query);
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 patientsCollection = getPatientsCollection();
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
- patient._id = patient._id.toString();
285
- const suggestions = await ragService.suggestTreatments(patientId, patient, condition);
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 patientsCollection = getPatientsCollection();
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
- patient._id = patient._id.toString();
317
- const analysis = await ragService.analyzeDrugInteractions(patientId, patient, new_medication);
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 patientsCollection = getPatientsCollection();
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 = patientHelper(patient);
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
- // Timeline Routes
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 { ObjectId } from '../db/mongo.js';
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
- try {
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 { ObjectId, getAppointmentsCollection, getPatientsCollection, getUsersCollection } from '../db/mongo.js';
 
 
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 patientsCollection.findOne({ _id: new ObjectId(appointment.patient_id) });
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 usersCollection.findOne({ _id: new ObjectId(appointment.doctor_id) });
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 appointmentsCollection = getAppointmentsCollection();
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
- const result = await appointmentsCollection.insertOne(appointmentDoc);
86
- appointmentDoc._id = result.insertedId;
87
- const enriched = await this._enrichWithNames(appointmentDoc);
88
 
89
- return this._appointmentHelper(enriched);
90
  }
91
 
92
  /**
@@ -94,12 +53,11 @@ class AppointmentService {
94
  */
95
  async getAppointment(appointmentId) {
96
  try {
97
- const appointmentsCollection = getAppointmentsCollection();
98
- const appointment = await appointmentsCollection.findOne({ _id: new ObjectId(appointmentId) });
99
 
100
  if (appointment) {
101
- const enriched = await this._enrichWithNames(appointment);
102
- return this._appointmentHelper(enriched);
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 appointmentsCollection
128
- .find(query)
129
  .sort({ date_time: 1 })
130
- .limit(limit)
131
- .toArray();
132
 
133
  const result = [];
134
  for (const apt of appointments) {
135
- const enriched = await this._enrichWithNames(apt);
136
- result.push(this._appointmentHelper(enriched));
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 appointmentsCollection = getAppointmentsCollection();
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
- const enriched = await this._enrichWithNames(apt);
161
- result.push(this._appointmentHelper(enriched));
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 appointmentsCollection = getAppointmentsCollection();
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
- const enriched = await this._enrichWithNames(apt);
190
- result.push(this._appointmentHelper(enriched));
191
  }
192
 
193
  return result;
@@ -197,130 +133,102 @@ class AppointmentService {
197
  * Update an appointment
198
  */
199
  async updateAppointment(appointmentId, updateData) {
200
- const appointmentsCollection = getAppointmentsCollection();
201
- const updateDict = { ...updateData, updated_at: new Date() };
202
-
203
- const result = await appointmentsCollection.updateOne(
204
- { _id: new ObjectId(appointmentId) },
205
- { $set: updateDict }
206
- );
207
-
208
- if (result.matchedCount === 0) {
 
 
 
 
 
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
- const appointmentsCollection = getAppointmentsCollection();
220
-
221
- const result = await appointmentsCollection.updateOne(
222
- { _id: new ObjectId(appointmentId) },
223
- {
224
- $set: {
225
- status: AppointmentStatus.CANCELLED,
226
- cancelled_reason: reason,
227
- updated_at: new Date()
228
- }
229
  }
230
- );
231
-
232
- if (result.matchedCount === 0) {
 
 
 
 
 
 
 
 
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
- const appointmentsCollection = getAppointmentsCollection();
244
- const updateData = {
245
- status: AppointmentStatus.COMPLETED,
246
- updated_at: new Date()
247
- };
248
-
249
- if (notes) {
250
- updateData.notes = notes;
251
- }
252
-
253
- const result = await appointmentsCollection.updateOne(
254
- { _id: new ObjectId(appointmentId) },
255
- { $set: updateData }
256
- );
257
-
258
- if (result.matchedCount === 0) {
259
- return null;
 
 
 
 
 
 
 
 
 
 
 
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
- const appointmentsCollection = getAppointmentsCollection();
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 appointmentsCollection = getAppointmentsCollection();
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 patientsCollection.findOne({ _id: new ObjectId(appointment.patient_id) });
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
- const appointmentsCollection = getAppointmentsCollection();
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 { ObjectId, getUsersCollection } from '../db/mongo.js';
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
- const usersCollection = getUsersCollection();
64
- return await usersCollection.findOne({ email });
65
  }
66
 
67
  /**
@@ -69,8 +68,7 @@ class AuthService {
69
  */
70
  async getUserById(userId) {
71
  try {
72
- const usersCollection = getUsersCollection();
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
- throw new Error('User with this email already exists');
89
- }
90
 
91
- // Create user document
92
- const now = new Date();
93
- const userDoc = {
94
  email: userData.email,
95
  name: userData.name,
96
  role: userData.role || 'doctor',
97
- hashed_password: this.getPasswordHash(userData.password),
98
- created_at: now,
99
- updated_at: now,
100
  is_active: true
101
- };
102
 
103
- const result = await usersCollection.insertOne(userDoc);
104
- userDoc._id = result.insertedId;
105
- return userDoc;
106
  }
107
 
108
  /**
@@ -113,9 +105,12 @@ class AuthService {
113
  if (!user) {
114
  return null;
115
  }
116
- if (!this.verifyPassword(password, user.hashed_password)) {
 
 
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
- const usersCollection = getUsersCollection();
141
- updateData.updated_at = new Date();
142
-
143
- const result = await usersCollection.updateOne(
144
- { _id: new ObjectId(userId) },
145
- { $set: updateData }
146
- );
147
-
148
- if (result.matchedCount === 0) {
149
  return null;
150
  }
151
-
152
- return await this.getUserById(userId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { ObjectId, getPatientsCollection, getAppointmentsCollection, getWarningsCollection } from '../db/mongo.js';
 
 
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 patientsCollection = getPatientsCollection();
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 appointmentsCollection.find({ patient_id: patientId }).toArray();
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 warningsCollection.find({
168
  patient_id: patientId,
169
  is_acknowledged: true
170
- }).toArray();
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 patientsCollection = getPatientsCollection();
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(' Vector database initialized');
23
  } catch (error) {
24
- console.log(`⚠️ Vector database initialization warning: ${error.message}`);
25
  }
26
  }
27
 
@@ -108,9 +108,9 @@ class VectorService {
108
  metadatas,
109
  ids
110
  });
111
- console.log(` Indexed ${documents.length} records for patient ${patientData.name}`);
112
  } catch (error) {
113
- console.log(` Error indexing patient ${patientData.name}: ${error.message}`);
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 { ObjectId, getWarningsCollection, getPatientsCollection } from '../db/mongo.js';
 
3
  import dotenv from 'dotenv';
4
 
5
  dotenv.config();
6
 
7
- // Warning types enum
8
- export const WarningType = {
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 warningsCollection = getWarningsCollection();
52
- const query = { patient_id: patientId };
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 warningsCollection = getWarningsCollection();
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
- const warningsCollection = getWarningsCollection();
86
-
87
- const result = await warningsCollection.updateOne(
88
- { _id: new ObjectId(warningId) },
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
- if (result.matchedCount === 0) {
 
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 warningsCollection = getWarningsCollection();
112
-
113
- const warningDoc = {
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 warningsCollection = getWarningsCollection();
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 patientsCollection = getPatientsCollection();
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 warning of warningsData) {
213
  try {
214
- const warningCreate = {
215
  patient_id: patientId,
216
- warning_type: warning.warning_type || 'abnormal_pattern',
217
- severity: warning.severity || 'medium',
218
- title: (warning.title || 'Clinical Warning').substring(0, 100),
219
- description: warning.description || '',
220
- related_medications: warning.related_medications || [],
221
- related_conditions: warning.related_conditions || [],
222
- recommendation: warning.recommendation || null
223
- };
 
224
 
225
- const createdWarning = await this.createWarning(warningCreate);
226
- createdWarnings.push(createdWarning);
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
- const warningsCollection = getWarningsCollection();
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