curaflow/backend/queueManager.js

179 lines
6.6 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];
}
async bookOnline(date, time, patientPhone, 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++;
const token = `ON-${status.online}`;
if (patientPhone) {
await notificationManager.sendTokenNotification(patientPhone, {
token,
clinicName: config.name,
slotTime: time,
fee: config.consultationFee
});
}
return {
success: true,
token,
slot: this.getSlotKey(tenantId, date, time),
status: 'PENDING_PAYMENT',
fee: config.consultationFee
};
}
return { success: false, message: "Slot full for online booking. Try next slot." };
}
async bookWalkin(date, time, patientPhone, 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++;
const token = `WK-${status.walkin}`;
if (patientPhone) {
await notificationManager.sendTokenNotification(patientPhone, {
token,
clinicName: config.name,
slotTime: time,
fee: config.consultationFee
});
}
return {
success: true,
token,
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."
];
}
async cancelBooking(date, time, tenantId = 'default') {
const status = this.getSlotStatus(tenantId, date, time);
if (status.online > 0) {
status.online--;
await this.notifyAvailableSlot(tenantId, date, time);
return { success: true, message: "Booking cancelled and slot opened." };
}
return { success: false, message: "No bookings found for this slot." };
}
async 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: "+918500176938", appointmentTime: "19:00" };
await 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();