patient / models /Appointment.js
aliroohan179's picture
first
5266fc5
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;