Spaces:
Sleeping
Sleeping
| import mongoose from 'mongoose'; | |
| // Appointment status enum | |
| export const AppointmentStatus = { | |
| SCHEDULED: 'scheduled', | |
| CONFIRMED: 'confirmed', | |
| IN_PROGRESS: 'in_progress', | |
| COMPLETED: 'completed', | |
| CANCELLED: 'cancelled', | |
| NO_SHOW: 'no_show' | |
| }; | |
| // Appointment type enum | |
| export const AppointmentType = { | |
| CHECKUP: 'checkup', | |
| FOLLOW_UP: 'follow_up', | |
| CONSULTATION: 'consultation', | |
| EMERGENCY: 'emergency', | |
| PROCEDURE: 'procedure', | |
| LAB_WORK: 'lab_work', | |
| OTHER: 'other' | |
| }; | |
| const AppointmentSchema = new mongoose.Schema({ | |
| patient_id: { | |
| type: mongoose.Schema.Types.ObjectId, | |
| required: [true, 'Patient ID is required'], | |
| index: true, | |
| ref: 'Patient' | |
| }, | |
| doctor_id: { | |
| type: mongoose.Schema.Types.ObjectId, | |
| ref: 'User', | |
| index: true | |
| }, | |
| date_time: { | |
| type: String, | |
| required: [true, 'Date and time is required'] | |
| }, | |
| duration_minutes: { | |
| type: Number, | |
| default: 30, | |
| min: [5, 'Duration must be at least 5 minutes'], | |
| max: [480, 'Duration cannot exceed 8 hours'] | |
| }, | |
| appointment_type: { | |
| type: String, | |
| enum: { | |
| values: Object.values(AppointmentType), | |
| message: '{VALUE} is not a valid appointment type' | |
| }, | |
| default: AppointmentType.CHECKUP | |
| }, | |
| purpose: { | |
| type: String, | |
| required: [true, 'Purpose is required'], | |
| trim: true | |
| }, | |
| notes: { | |
| type: String, | |
| trim: true | |
| }, | |
| location: { | |
| type: String, | |
| trim: true | |
| }, | |
| status: { | |
| type: String, | |
| enum: { | |
| values: Object.values(AppointmentStatus), | |
| message: '{VALUE} is not a valid status' | |
| }, | |
| default: AppointmentStatus.SCHEDULED | |
| }, | |
| cancelled_reason: { | |
| type: String, | |
| trim: true | |
| }, | |
| cancelled_at: { | |
| type: Date | |
| }, | |
| cancelled_by: { | |
| type: String | |
| }, | |
| completion_notes: { | |
| type: String, | |
| trim: true | |
| }, | |
| completed_at: { | |
| type: Date | |
| }, | |
| completed_by: { | |
| type: String | |
| }, | |
| created_by: { | |
| type: String, | |
| required: true | |
| }, | |
| // Virtual fields to store names (populated from other collections) | |
| patient_name: { | |
| type: String | |
| }, | |
| doctor_name: { | |
| type: String | |
| } | |
| }, { | |
| timestamps: { | |
| createdAt: 'created_at', | |
| updatedAt: 'updated_at' | |
| } | |
| }); | |
| // Compound indexes for efficient queries | |
| AppointmentSchema.index({ patient_id: 1, date_time: -1 }); | |
| AppointmentSchema.index({ doctor_id: 1, date_time: 1 }); | |
| AppointmentSchema.index({ status: 1, date_time: 1 }); | |
| AppointmentSchema.index({ date_time: 1 }); | |
| // Method to convert to response object | |
| AppointmentSchema.methods.toResponse = function() { | |
| return { | |
| id: this._id.toString(), | |
| patient_id: this.patient_id, | |
| doctor_id: this.doctor_id || null, | |
| date_time: this.date_time, | |
| duration_minutes: this.duration_minutes || 30, | |
| appointment_type: this.appointment_type || 'checkup', | |
| purpose: this.purpose || '', | |
| notes: this.notes || null, | |
| location: this.location || null, | |
| status: this.status || 'scheduled', | |
| cancelled_reason: this.cancelled_reason || null, | |
| cancelled_at: this.cancelled_at || null, | |
| completion_notes: this.completion_notes || null, | |
| completed_at: this.completed_at || null, | |
| created_at: this.created_at, | |
| updated_at: this.updated_at, | |
| created_by: this.created_by || '', | |
| patient_name: this.patient_name || null, | |
| doctor_name: this.doctor_name || null | |
| }; | |
| }; | |
| // Static method to find upcoming appointments | |
| // doctorId: optional - filter by doctor (for non-admin users) | |
| AppointmentSchema.statics.findUpcoming = function(limit = 20, doctorId = null) { | |
| const now = new Date().toISOString(); | |
| const query = { | |
| date_time: { $gte: now }, | |
| status: { $in: [AppointmentStatus.SCHEDULED, AppointmentStatus.CONFIRMED] } | |
| }; | |
| if (doctorId) { | |
| query.doctor_id = doctorId; | |
| } | |
| return this.find(query) | |
| .sort({ date_time: 1 }) | |
| .limit(limit); | |
| }; | |
| // Static method to find appointments by patient | |
| // doctorId: optional - filter by doctor (for non-admin users) | |
| AppointmentSchema.statics.findByPatient = function(patientId, includePast = false, doctorId = null) { | |
| const query = { patient_id: patientId }; | |
| if (doctorId) { | |
| query.doctor_id = doctorId; | |
| } | |
| if (!includePast) { | |
| const now = new Date().toISOString(); | |
| query.$or = [ | |
| { date_time: { $gte: now } }, | |
| { status: { $in: [AppointmentStatus.SCHEDULED, AppointmentStatus.CONFIRMED] } } | |
| ]; | |
| } | |
| return this.find(query).sort({ date_time: -1 }); | |
| }; | |
| // Static method to find appointments by doctor | |
| AppointmentSchema.statics.findByDoctor = function(doctorId) { | |
| return this.find({ doctor_id: doctorId }).sort({ date_time: 1 }); | |
| }; | |
| // Static method to check for scheduling conflicts | |
| AppointmentSchema.statics.checkConflicts = async function(doctorId, dateTime, durationMinutes, excludeId = null) { | |
| const proposedStart = new Date(dateTime.replace('Z', '+00:00')); | |
| const proposedEnd = new Date(proposedStart.getTime() + durationMinutes * 60000); | |
| const query = { | |
| doctor_id: doctorId, | |
| status: { $in: [AppointmentStatus.SCHEDULED, AppointmentStatus.CONFIRMED] } | |
| }; | |
| if (excludeId) { | |
| query._id = { $ne: excludeId }; | |
| } | |
| const appointments = await this.find(query); | |
| const conflicts = []; | |
| for (const apt of appointments) { | |
| try { | |
| const aptStart = new Date(apt.date_time.replace('Z', '+00:00')); | |
| const aptEnd = new Date(aptStart.getTime() + (apt.duration_minutes || 30) * 60000); | |
| // Check for overlap | |
| if (!(proposedEnd <= aptStart || proposedStart >= aptEnd)) { | |
| conflicts.push(apt); | |
| } | |
| } catch { | |
| continue; | |
| } | |
| } | |
| return conflicts; | |
| }; | |
| // Static method to get appointment statistics | |
| // doctorId: optional - filter by doctor (for non-admin users) | |
| AppointmentSchema.statics.getStats = async function(doctorId = null) { | |
| const matchStage = doctorId ? { $match: { doctor_id: new mongoose.Types.ObjectId(doctorId) } } : null; | |
| const pipeline = []; | |
| if (matchStage) { | |
| pipeline.push(matchStage); | |
| } | |
| pipeline.push({ | |
| $group: { | |
| _id: '$status', | |
| count: { $sum: 1 } | |
| } | |
| }); | |
| const results = await this.aggregate(pipeline); | |
| const stats = { | |
| total: 0, | |
| by_status: {} | |
| }; | |
| for (const r of results) { | |
| stats.by_status[r._id] = r.count; | |
| stats.total += r.count; | |
| } | |
| return stats; | |
| }; | |
| // Method to cancel appointment | |
| AppointmentSchema.methods.cancel = function(reason, cancelledBy) { | |
| this.status = AppointmentStatus.CANCELLED; | |
| this.cancelled_reason = reason; | |
| this.cancelled_at = new Date(); | |
| this.cancelled_by = cancelledBy; | |
| return this.save(); | |
| }; | |
| // Method to complete appointment | |
| AppointmentSchema.methods.complete = function(notes, completedBy) { | |
| // Validate that appointment date has passed | |
| const appointmentDate = new Date(this.date_time); | |
| const now = new Date(); | |
| if (appointmentDate > now) { | |
| throw new Error(`Cannot complete this appointment yet. It is scheduled for ${appointmentDate.toISOString()}.`); | |
| } | |
| this.status = AppointmentStatus.COMPLETED; | |
| this.completion_notes = notes; | |
| this.completed_at = new Date(); | |
| this.completed_by = completedBy; | |
| return this.save(); | |
| }; | |
| const Appointment = mongoose.model('Appointment', AppointmentSchema); | |
| export default Appointment; | |