161 lines
5.9 KiB
JavaScript
161 lines
5.9 KiB
JavaScript
/**
|
|
* QueueManager: Handles the hybrid slot logic for Curio
|
|
* - 30-min slots
|
|
* - Max 3 online bookings (Hard Cap)
|
|
* - 5 Walk-in slots
|
|
*/
|
|
|
|
const { getTenantConfig } = require('./configManager');
|
|
const notificationManager = require('./notificationManager');
|
|
|
|
class QueueManager {
|
|
constructor() {
|
|
this.slots = {}; // Key: "tenantId:YYYY-MM-DD:HH:mm"
|
|
}
|
|
|
|
getSlotKey(tenantId, date, time) {
|
|
const config = getTenantConfig(tenantId);
|
|
const duration = config.slotDuration || 30;
|
|
const [hours, minutes] = time.split(':');
|
|
const roundedMins = parseInt(minutes) < duration ? '00' : duration;
|
|
return `${tenantId}:${date}:${hours}:${roundedMins}`;
|
|
}
|
|
|
|
isAvailable(tenantId, date, time) {
|
|
const config = getTenantConfig(tenantId);
|
|
const d = new Date(date + "T00:00:00");
|
|
|
|
// Check days off
|
|
if (config.daysOff && config.daysOff.includes(d.getDay())) return false;
|
|
|
|
// Check holidays
|
|
if (config.holidays && config.holidays.includes(date)) return false;
|
|
|
|
// Check working hours
|
|
if (config.workingHours) {
|
|
const [h, m] = time.split(':').map(Number);
|
|
const timeVal = h * 60 + m;
|
|
|
|
const isInRange = config.workingHours.some(range => {
|
|
const [sH, sM] = range.start.split(':').map(Number);
|
|
const [eH, eM] = range.end.split(':').map(Number);
|
|
const startVal = sH * 60 + sM;
|
|
const endVal = eH * 60 + eM;
|
|
return timeVal >= startVal && timeVal < endVal;
|
|
});
|
|
|
|
if (!isInRange) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
getSlotStatus(tenantId, date, time) {
|
|
const key = this.getSlotKey(tenantId, date, time);
|
|
if (!this.slots[key]) {
|
|
const config = getTenantConfig(tenantId);
|
|
this.slots[key] = {
|
|
online: 0,
|
|
walkin: 0,
|
|
maxOnline: config.onlineSlotLimit || 3,
|
|
maxWalkin: config.walkinSlotLimit || 5
|
|
};
|
|
}
|
|
return this.slots[key];
|
|
}
|
|
|
|
bookOnline(date, time, tenantId = 'default') {
|
|
if (!this.isAvailable(tenantId, date, time)) {
|
|
return { success: false, message: "The clinic is closed at this time / on this day." };
|
|
}
|
|
const status = this.getSlotStatus(tenantId, date, time);
|
|
const config = getTenantConfig(tenantId);
|
|
if (status.online < status.maxOnline) {
|
|
status.online++;
|
|
return {
|
|
success: true,
|
|
token: `ON-${status.online}`,
|
|
slot: this.getSlotKey(tenantId, date, time),
|
|
status: 'PENDING_PAYMENT',
|
|
fee: config.consultationFee
|
|
};
|
|
}
|
|
return { success: false, message: "Slot full for online booking. Try next slot." };
|
|
}
|
|
|
|
bookWalkin(date, time, tenantId = 'default') {
|
|
if (!this.isAvailable(tenantId, date, time)) {
|
|
return { success: false, message: "The clinic is closed at this time / on this day." };
|
|
}
|
|
const status = this.getSlotStatus(tenantId, date, time);
|
|
const config = getTenantConfig(tenantId);
|
|
if (status.walkin < status.maxWalkin) {
|
|
status.walkin++;
|
|
return {
|
|
success: true,
|
|
token: `WK-${status.walkin}`,
|
|
status: 'PENDING_PAYMENT',
|
|
fee: config.consultationFee
|
|
};
|
|
}
|
|
return { success: false, message: "Clinic is at full capacity for this slot." };
|
|
}
|
|
|
|
getDailyUpdates(patientId, tenantId = 'default') {
|
|
const config = getTenantConfig(tenantId);
|
|
return [
|
|
`Good morning! Your appointment with ${config.name} is scheduled for today.`,
|
|
"Queue Update: The clinic is running 10 mins behind schedule. Please plan accordingly.",
|
|
"Reminder: Don't forget to bring your previous reports."
|
|
];
|
|
}
|
|
|
|
cancelBooking(date, time, tenantId = 'default') {
|
|
const status = this.getSlotStatus(tenantId, date, time);
|
|
if (status.online > 0) {
|
|
status.online--;
|
|
this.notifyAvailableSlot(tenantId, date, time);
|
|
return { success: true, message: "Booking cancelled and slot opened." };
|
|
}
|
|
return { success: false, message: "No bookings found for this slot." };
|
|
}
|
|
|
|
notifyAvailableSlot(tenantId, date, time) {
|
|
const config = getTenantConfig(tenantId);
|
|
console.log(`[Queue] Triggering notifications for open slot: ${time}`);
|
|
|
|
// Mock finding a patient with a later slot
|
|
const mockLaterPatient = { phone: "9876543210", appointmentTime: "19:00" };
|
|
|
|
notificationManager.sendRescheduleAlert(
|
|
mockLaterPatient.phone,
|
|
config.name,
|
|
mockLaterPatient.appointmentTime,
|
|
time
|
|
);
|
|
}
|
|
|
|
rescheduleBooking(date, oldTime, newTime, tenantId = 'default') {
|
|
const newSlotStatus = this.getSlotStatus(tenantId, date, newTime);
|
|
const oldSlotStatus = this.getSlotStatus(tenantId, date, oldTime);
|
|
|
|
// Atomic Check: Ensure new slot hasn't been filled since notification
|
|
if (newSlotStatus.online < newSlotStatus.maxOnline) {
|
|
newSlotStatus.online++;
|
|
if (oldSlotStatus.online > 0) {
|
|
oldSlotStatus.online--;
|
|
}
|
|
return {
|
|
success: true,
|
|
message: `✅ *Success!* Your appointment has been moved to *${newTime}*. Your old slot at ${oldTime} has been released.`
|
|
};
|
|
}
|
|
return {
|
|
success: false,
|
|
message: "❌ *Slot Already Taken*: Sorry, another patient claimed that earlier slot just before you. You still have your original appointment at " + oldTime + "."
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = new QueueManager();
|