/** * 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();