diff --git a/backend/botLogic.js b/backend/botLogic.js index 09d1ffd..c12133f 100644 --- a/backend/botLogic.js +++ b/backend/botLogic.js @@ -1,106 +1,30 @@ -const queueManager = require('./queueManager'); -const triageEngine = require('./triageEngine'); -const i18n = require('./i18n'); +const clinicAgent = require('./clinicAgent'); -/** - * BotLogic: Simulates the WhatsApp conversational flow - */ class BotLogic { constructor() { - this.states = {}; // Track session state per patient + this.states = {}; // Context storage + } + + setRescheduleOffer(patientId, offerData) { + this.states[`${patientId}_offer`] = offerData; } async handleMessage(patientId, message) { - const text = message.toLowerCase(); - let state = this.states[patientId] || 'IDLE'; - let lang = this.states[`${patientId}_lang`] || 'en'; - - // 1. Language Selection (First time or reset) - if (text.includes('hi') && !this.states[`${patientId}_lang`]) { - this.states[patientId] = 'SELECT_LANG'; - return { - reply: "Welcome to Curio! Please select your language / भाषा चुनें / భాషను ఎంచుకోండి:\n\n1. English\n2. Hindi (हिंदी)\n3. Telugu (తెలుగు)", - buttons: ["English", "Hindi", "Telugu"] - }; + const context = this.states[patientId] || {}; + + // Delegate to the Agent + const response = await clinicAgent.processMessage(patientId, message, context); + + // Update local context/state based on agent response + if (response.nextStep) { + this.states[patientId] = { lastStep: response.nextStep }; + } else { + // Reset or keep context as needed } - if (state === 'SELECT_LANG') { - if (text.includes('hindi')) lang = 'hi'; - if (text.includes('telugu')) lang = 'te'; - this.states[`${patientId}_lang`] = lang; - this.states[patientId] = 'IDLE'; - state = 'IDLE'; // Continue to welcome - } - - // 2. Initial Greeting / Booking Intent - if (state === 'IDLE') { - return { - reply: i18n.t(lang, 'welcome'), - buttons: i18n.getButtons(lang) - }; - } - - // 2. Booking Flow - if (text.includes('book for today')) { - return { - reply: "Great! Please select your preferred time slot:", - buttons: ["10:00 AM", "10:30 AM", "11:00 AM"] - }; - } - - if (text === '10:00 am' || text === '10:30 am') { - const date = new Date().toISOString().split('T')[0]; - const result = queueManager.bookOnline(date, text.replace(' am', '')); - - if (result.success) { - this.states[patientId] = 'TRIAGE_START'; - return { - reply: i18n.t(lang, 'triage_ask', { token: result.token }) + - ` \n\n💳 *Payment Required*: Please pay ₹${result.fee} to activate your token: http://razorpay.me/Curio_mock`, - }; - } else { - return { - reply: "❌ Sorry, the 10:00 AM online slots are full (Max 3/slot). Would you like to try 11:30 AM?", - buttons: ["Try 11:30 AM", "Waitlist Me"] - }; - } - } - - // 3. Triage State Handling - if (this.states[patientId] === 'TRIAGE_START') { - const analysis = await triageEngine.analyzeSymptoms(text); - this.states[patientId] = 'TRIAGE_FOLLOWUP'; - this.states[`${patientId}_summary`] = analysis.summary; - - return { - reply: `Thank you. AI Analysis: _${analysis.category}_.\n\nQuick follow-up: ${analysis.followUp}` - }; - } - - if (this.states[patientId] === 'TRIAGE_FOLLOWUP') { - this.states[patientId] = 'IDLE'; - return { - reply: i18n.t(lang, 'triage_done') - }; - } - - // 4. Daily Updates / Status - if (text.includes('status') || text.includes('queue') || text.includes('कतार') || text.includes('స్థితి')) { - return { - reply: i18n.t(lang, 'status_update', { current: 'WK-3', pos: 'ON-1', wait: '8' }) - }; - } - - if (text.includes('tips') || text.includes('update')) { - const updates = queueManager.getDailyUpdates(patientId); - return { - reply: "🔔 *Daily Updates for You:*\n\n" + updates.join('\n\n') - }; - } - - // 4. Default Fallback return { - reply: "I'm sorry, I didn't quite catch that. You can type 'Hi' to see the main menu." + reply: response.reply, + buttons: response.buttons || [] }; } } diff --git a/backend/clinicAgent.js b/backend/clinicAgent.js new file mode 100644 index 0000000..220d689 --- /dev/null +++ b/backend/clinicAgent.js @@ -0,0 +1,98 @@ +/** + * ClinicAgent: An agentic orchestrator for Curio. + * It uses natural language understanding to map user requests to hospital tools. + */ + +const patientManager = require('./patientManager'); +const queueManager = require('./queueManager'); +const pharmacyAgent = require('./pharmacyAgent'); +const configManager = require('./configManager'); + +class ClinicAgent { + constructor() { + this.systemPrompt = ` + You are 'Curio AI', the assistant for Curio Health. + Your goal is to help patients book appointments, manage family members, and check clinic status. + + TOOLS: + - list_family: Shows all members linked to the phone. + - add_member: Registers a new family member name. + - book_today: Books an appointment for today. + - check_status: Checks current queue position. + + RULES: + 1. Always be polite and professional. + 2. If a patient asks for someone not in their family list, offer to add them. + 3. If multiple people have same name, ask for clarification. + `; + } + + async processMessage(phone, message, context = {}) { + const text = message.toLowerCase(); + console.log(`[Agent] Processing for ${phone}: "${message}"`); + + // Mock LLM Intent Extraction & Tool Execution + // In a real app, this would be: const response = await llm.call({ prompt: ..., tools: [...] }); + + const family = patientManager.getProfiles(phone); + + // Scenario 1: Greeting + Profile Check + if (text.includes("hi") || text.includes("hello")) { + if (family.length === 0) { + return { + reply: "Hello! I'm Curio AI. I don't see a profile for this number yet. What is your full name so I can get you started?", + nextStep: "AWAIT_REGISTRATION" + }; + } + return { + reply: `Hello again! I see profiles for ${family.map(p => p.name).join(', ')}. Who can I help you with today?`, + buttons: [...family.map(p => p.name), "+ Add New Member"] + }; + } + + // Scenario 2: Adding a Member + if (context.lastStep === "AWAIT_REGISTRATION" || text.includes("add new member")) { + if (text.includes("add new member")) { + return { reply: "Sure! What is the name of the family member you'd like to add?" }; + } + const newId = patientManager.addProfile(phone, { name: message, relation: "Family" }); + return { + reply: `Perfect! I've added ${message} to your family account. Would you like to book an appointment for them today?`, + buttons: ["Book for Today", "Check Queue"] + }; + } + + // Scenario 3: Booking Intent + if (text.includes("book") || text.includes("today")) { + return { + reply: "I can help with that. Which time works best for you?", + buttons: ["10:00 AM", "11:00 AM", "06:00 PM"] + }; + } + + // Scenario 3.5: Medicine Query + if (text.includes("have") || text.includes("medicine") || text.includes("syrup") || text.includes("tablet")) { + const pharmacyReply = pharmacyAgent.handlePatientQuery(message); + return { + reply: "💊 *Pharmacy Update*: " + pharmacyReply, + buttons: ["Book for Today", "Check Other Meds"] + }; + } + + // Scenario 4: Natural Language Match for Family Members + const matchedMember = family.find(p => text.includes(p.name.toLowerCase())); + if (matchedMember) { + return { + reply: `I've switched context to ${matchedMember.name}. What would you like to do for them?`, + buttons: ["Book for Today", "View History"] + }; + } + + // Fallback to "Smart" LLM reply + return { + reply: "I'm not sure how to help with that yet, but I'm learning! You can ask me to book an appointment or add a family member." + }; + } +} + +module.exports = new ClinicAgent(); diff --git a/backend/configManager.js b/backend/configManager.js index 64dec09..6282c60 100644 --- a/backend/configManager.js +++ b/backend/configManager.js @@ -11,23 +11,40 @@ class ConfigManager { primaryColor: "#0F172A", secondaryColor: "#38BDF8", consultationFee: 500, - currency: "₹" + currency: "₹", + workingHours: [ + { start: "09:00", end: "13:00" }, + { start: "17:00", end: "22:00" } + ], + daysOff: [0], // 0 = Sunday + holidays: ["2024-12-25", "2025-01-01"] }, 'sharma-clinic': { name: "Dr. Sharma's Cardiology", logo: "S", - primaryColor: "#1E3A8A", // Deep Blue + primaryColor: "#1E3A8A", secondaryColor: "#60A5FA", consultationFee: 800, - currency: "₹" + currency: "₹", + workingHours: [ + { start: "10:00", end: "14:00" }, + { start: "18:00", end: "21:00" } + ], + daysOff: [0, 6], // Sat, Sun + holidays: [] }, 'apollo-hospitals': { name: "Apollo Multispecialty", logo: "A", - primaryColor: "#065F46", // Dark Green + primaryColor: "#065F46", secondaryColor: "#34D399", consultationFee: 1200, - currency: "₹" + currency: "₹", + workingHours: [ + { start: "09:00", end: "22:00" } + ], + daysOff: [], + holidays: ["2024-10-02"] } }; } diff --git a/backend/i18n.js b/backend/i18n.js index 6b53621..3f8d1f8 100644 --- a/backend/i18n.js +++ b/backend/i18n.js @@ -7,6 +7,7 @@ const translations = { en: { welcome: "Welcome to Dr. Sharma's Clinic! 🏥 I can help you book a token or check your status.\n\nWould you like to:", book: "Book for Today", + book_future: "For future bookings, please call our reception at +91-1234567890 📞", check: "Check Queue Status", tips: "Daily Health Tips", select_slot: "Great! Please select your preferred time slot:", @@ -17,6 +18,7 @@ const translations = { hi: { welcome: "डॉ. शर्मा के क्लिनिक में आपका स्वागत है! 🏥 मैं टोकन बुक करने या आपकी स्थिति की जांच करने में आपकी मदद कर सकता हूँ।\n\nक्या आप चाहेंगे:", book: "आज के लिए बुक करें", + book_future: "भविष्य की बुकिंग के लिए, कृपया हमारे रिसेप्शन पर +91-1234567890 पर कॉल करें 📞", check: "कतार की स्थिति जांचें", tips: "स्वास्थ्य युक्तियाँ", select_slot: "बहुत बढ़िया! कृपया अपना पसंदीदा समय स्लॉट चुनें:", @@ -27,6 +29,7 @@ const translations = { te: { welcome: "డాక్టర్ శర్మ క్లినిక్‌కి స్వాగతం! 🏥 టోకెన్ బుక్ చేసుకోవడంలో లేదా మీ స్థితిని తనిఖీ చేయడంలో నేను మీకు సహాయపడగలను.\n\nమీరు వీటిని చేయాలనుకుంటున్నారా:", book: "ఈరోజు కోసం బుక్ చేయండి", + book_future: "భవిష్యత్తు బుకింగ్‌ల కోసం, దయచేసి మా రిసెప్షన్‌కు +91-1234567890 నంబర్‌కు కాల్ చేయండి 📞", check: "క్యూ స్థితిని తనిఖీ చేయండి", tips: "ఆరోగ్య చిట్కాలు", select_slot: "అద్భుతం! దయచేసి మీకు నచ్చిన సమయాన్ని ఎంచుకోండి:", @@ -49,7 +52,7 @@ class I18nManager { getButtons(lang) { const t = translations[lang] || translations['en']; - return [t.book, t.check, t.tips]; + return [t.book, t.book_future, t.check, t.tips]; } } diff --git a/backend/notificationManager.js b/backend/notificationManager.js new file mode 100644 index 0000000..f71deaf --- /dev/null +++ b/backend/notificationManager.js @@ -0,0 +1,30 @@ +/** + * NotificationManager: Handles mock WhatsApp notifications for Curio + */ + +class NotificationManager { + constructor() { + this.notificationsSent = []; + } + + sendRescheduleAlert(patientPhone, tenantName, oldTime, newTime) { + const message = `🔔 *Curio Alert*: A slot has just opened up at ${tenantName} for today at ${newTime}! + +Would you like to move your ${oldTime} appointment to this earlier slot? + +Reply: +1. Yes, move me! +2. No, keep my current time.`; + + console.log(`[WhatsApp to ${patientPhone}]: ${message}`); + this.notificationsSent.push({ phone: patientPhone, message, timestamp: new Date() }); + + return { success: true, message: "Notification sent." }; + } + + getRecentNotifications() { + return this.notificationsSent; + } +} + +module.exports = new NotificationManager(); diff --git a/backend/patientManager.js b/backend/patientManager.js new file mode 100644 index 0000000..57571e3 --- /dev/null +++ b/backend/patientManager.js @@ -0,0 +1,30 @@ +/** + * PatientManager: Manages family profiles associated with a single phone number. + */ + +class PatientManager { + constructor() { + this.profiles = { + '9876543210': [ + { id: 'P1', name: "Rahul Verma", age: 34, relation: "Self" }, + { id: 'P2', name: "Suman Verma", age: 32, relation: "Spouse" }, + { id: 'P3', name: "Aryan Verma", age: 8, relation: "Child" } + ] + }; + } + + getProfiles(phone) { + return this.profiles[phone] || []; + } + + addProfile(phone, profile) { + if (!this.profiles[phone]) { + this.profiles[phone] = []; + } + const id = `P${Math.floor(Math.random() * 10000)}`; + this.profiles[phone].push({ id, ...profile }); + return id; + } +} + +module.exports = new PatientManager(); diff --git a/backend/pharmacyAgent.js b/backend/pharmacyAgent.js new file mode 100644 index 0000000..d507152 --- /dev/null +++ b/backend/pharmacyAgent.js @@ -0,0 +1,72 @@ +/** + * PharmacyAgent: An autonomous agent that manages hospital inventory + * and interacts with vendors and patients. + */ + +const pharmacyManager = require('./pharmacyManager'); + +class PharmacyAgent { + constructor() { + this.vendorContact = "+91-9998887776 (Global Pharma)"; + } + + async analyzeInventory() { + console.log("[PharmacyAgent] Scanning inventory for discrepancies..."); + const inventory = pharmacyManager.getInventory(); + const lowStockItems = inventory.filter(item => item.stock < 10); + + if (lowStockItems.length > 0) { + console.log(`[PharmacyAgent] Found ${lowStockItems.length} items needing attention.`); + for (const item of lowStockItems) { + await this.placeOrder(item.name, 100); + } + } + this.analyzeExpiries(inventory); + } + + analyzeExpiries(inventory) { + const today = new Date(); + const ninetyDaysOut = new Date(); + ninetyDaysOut.setDate(today.getDate() + 90); + + const nearingExpiry = inventory.filter(item => { + const exp = new Date(item.expiry); + return exp > today && exp < ninetyDaysOut; + }); + + if (nearingExpiry.length > 0) { + console.log(`[PharmacyAgent] 🚨 EXPIRY ALERT: ${nearingExpiry.length} items expiring within 90 days.`); + nearingExpiry.forEach(item => { + console.log(`[PharmacyAgent] Suggestion: Run a 'Clearance Sale' or push ${item.name} (Batch: ${item.batch}) to front counter.`); + }); + } + } + + async placeOrder(medName, qty) { + console.log(`[PharmacyAgent] 🤖 AUTONOMOUS ACTION: Sending WhatsApp to Vendor ${this.vendorContact}`); + console.log(`[WhatsApp to Vendor]: "Urgent Order from Curio Health: Need ${qty} units of ${medName}. Please confirm delivery."`); + + // Mocking vendor response and auto-restock after 2 seconds + setTimeout(() => { + console.log(`[PharmacyAgent] 🚚 Vendor confirmed delivery for ${medName}.`); + pharmacyManager.restock(medName, qty); + }, 3000); + } + + handlePatientQuery(query) { + const text = query.toLowerCase(); + const inventory = pharmacyManager.getInventory(); + const found = inventory.find(i => text.includes(i.name.toLowerCase())); + + if (found) { + if (found.stock > 0) { + return `Yes, we have ${found.name} in stock. It costs ₹${found.price} per unit. Would you like me to reserve some for your visit?`; + } else { + return `I'm sorry, ${found.name} is currently out of stock. I've already notified our supplier, and it should be back in 2-3 days. Can I suggest an alternative?`; + } + } + return "I'm not sure if we carry that specific medication. Let me check with our pharmacist and get back to you."; + } +} + +module.exports = new PharmacyAgent(); diff --git a/backend/pharmacyManager.js b/backend/pharmacyManager.js index 4f82133..1edc188 100644 --- a/backend/pharmacyManager.js +++ b/backend/pharmacyManager.js @@ -5,13 +5,14 @@ class PharmacyManager { constructor() { this.inventory = [ - { id: 'M01', name: 'Paracetamol 500mg', stock: 120, price: 5 }, - { id: 'M02', name: 'Amoxicillin 250mg', stock: 45, price: 12 }, - { id: 'M03', name: 'Cough Syrup', stock: 4, price: 85 }, - { id: 'M04', name: 'Vitamin C', stock: 200, price: 3 }, - { id: 'M05', name: 'Azithromycin', stock: 30, price: 150 }, - { id: 'M06', name: 'Insulin Glargine', stock: 12, price: 450 } + { id: 'M01', name: 'Paracetamol 500mg', stock: 120, price: 5, batch: 'B2201', expiry: '2026-12-30', hsn: '3004', gst: 12 }, + { id: 'M02', name: 'Amoxicillin 250mg', stock: 45, price: 12, batch: 'B2205', expiry: '2026-06-15', hsn: '3004', gst: 12 }, + { id: 'M03', name: 'Cough Syrup', stock: 104, price: 85, batch: 'S881', expiry: '2026-04-10', hsn: '3004', gst: 12 }, + { id: 'M04', name: 'Vitamin C', stock: 200, price: 3, batch: 'V009', expiry: '2027-01-01', hsn: '2936', gst: 18 }, + { id: 'M05', name: 'Azithromycin', stock: 30, price: 150, batch: 'A772', expiry: '2026-08-20', hsn: '3004', gst: 12 }, + { id: 'M06', name: 'Insulin Glargine', stock: 12, price: 450, batch: 'I110', expiry: '2026-05-25', hsn: '3004', gst: 5 } ]; + this.lowStockThreshold = 10; this.pendingOrders = [ { id: 'ORD-901', patientName: 'Rahul Verma', medicines: 'Paracetamol, Cough Syrup', status: 'PENDING', timestamp: '2026-05-13T09:15:00Z' }, { id: 'ORD-902', patientName: 'Anjali Singh', medicines: 'Amoxicillin', status: 'READY', timestamp: '2026-05-13T09:45:00Z' } @@ -36,11 +37,88 @@ class PharmacyManager { const order = this.pendingOrders.find(o => o.id === orderId); if (order) { order.status = 'READY'; - // In a real app, update inventory stock here + // Deduct stock (simplified for now) + const meds = order.medicines.split(', '); + meds.forEach(m => this.deductStock(m, 1)); return { success: true, message: `Order for ${order.patientName} is ready for pickup.` }; } return { success: false, message: "Order not found." }; } + + deductStock(medName, qty) { + const med = this.inventory.find(i => i.name.includes(medName)); + if (med) { + med.stock -= qty; + if (med.stock < this.lowStockThreshold) { + console.log(`[Pharmacy] ⚠️ ALERT: Low stock for ${med.name} (${med.stock} left)`); + return { lowStock: true, medName: med.name }; + } + } + return { lowStock: false }; + } + + restock(medName, qty) { + const med = this.inventory.find(i => i.name.includes(medName)); + if (med) { + med.stock += qty; + console.log(`[Pharmacy] ✅ Restocked ${med.name}. New stock: ${med.stock}`); + } + } + + calculateBill(medId, qty) { + const item = this.inventory.find(i => i.id === medId); + if (!item) return null; + + const subtotal = item.price * qty; + const gstAmount = (subtotal * item.gst) / 100; + const total = subtotal + gstAmount; + + return { + itemName: item.name, + qty, + rate: item.price, + subtotal, + gstRate: item.gst, + gstAmount, + total, + hsn: item.hsn + }; + } + + fulfillPrescription(prescription) { + console.log(`[Pharmacy] Fulfilling Rx: ${prescription.id} for Patient ${prescription.patientId}`); + + const billItems = []; + let totalBill = 0; + + prescription.medicines.forEach(m => { + const med = this.inventory.find(i => i.name.toLowerCase().includes(m.name.toLowerCase())); + if (med) { + const itemBill = this.calculateBill(med.id, m.qty || 10); + billItems.push(itemBill); + totalBill += itemBill.total; + this.deductStock(med.name, m.qty || 10); + } + }); + + // Forward Lab Tests + if (prescription.labTests && prescription.labTests.length > 0) { + this.forwardToLab(prescription.patientId, prescription.labTests); + } + + return { + billItems, + totalBill, + labStatus: prescription.labTests.length > 0 ? "FORWARDED" : "NONE" + }; + } + + forwardToLab(patientId, tests) { + console.log(`[Pharmacy] 🔬 AUTONOMOUS ACTION: Forwarding ${tests.length} tests to Lab for Patient ${patientId}`); + tests.forEach(t => { + console.log(`[Lab] 📥 Received Request: ${t.testName} for Patient ${patientId}`); + }); + } } module.exports = new PharmacyManager(); diff --git a/backend/prescriptionManager.js b/backend/prescriptionManager.js index bd4f37f..aaa1380 100644 --- a/backend/prescriptionManager.js +++ b/backend/prescriptionManager.js @@ -7,7 +7,7 @@ class PrescriptionManager { this.prescriptions = []; } - generate(patientId, doctorId, medicines, diagnosis) { + generate(patientId, doctorId, medicines, diagnosis, labTests = []) { const id = `RX-${Date.now()}`; const prescription = { id, @@ -15,16 +15,19 @@ class PrescriptionManager { doctorId, date: new Date().toISOString(), diagnosis, - medicines, // Array of { name, dosage, frequency, duration } + medicines, // Array of { name, dosage, frequency, duration, qty } + labTests // Array of { testName, instructions } }; this.prescriptions.push(prescription); - // In a real app, you'd generate a PDF here return { success: true, prescriptionId: id, - whatsappMessage: `💊 *Digital Prescription from Dr. Sharma*\n\n*Diagnosis:* ${diagnosis}\n\n*Medicines:*\n${medicines.map(m => `- ${m.name}: ${m.dosage} (${m.frequency}) for ${m.duration}`).join('\n')}\n\n_You can show this message at our pharmacy for a 10% discount._` + data: prescription, + whatsappMessage: `💊 *Digital Prescription from Dr. Sharma*\n\n*Diagnosis:* ${diagnosis}\n\n*Medicines:*\n${medicines.map(m => `- ${m.name}: ${m.dosage} (${m.frequency}) for ${m.duration}`).join('\n')}\n\n` + + (labTests.length > 0 ? `🔬 *Lab Tests Recommended:*\n${labTests.map(t => `- ${t.testName}`).join('\n')}\n\n` : "") + + `_You can show this message at our pharmacy for a 10% discount._` }; } } diff --git a/backend/queueManager.js b/backend/queueManager.js index a66abcc..db3a3f4 100644 --- a/backend/queueManager.js +++ b/backend/queueManager.js @@ -6,6 +6,7 @@ */ const configManager = require('./configManager'); +const notificationManager = require('./notificationManager'); class QueueManager { constructor() { @@ -20,6 +21,35 @@ class QueueManager { return `${tenantId}:${date}:${hours}:${roundedMins}`; } + isAvailable(tenantId, date, time) { + const config = configManager.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]) { @@ -35,6 +65,9 @@ class QueueManager { } 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 = configManager.getTenantConfig(tenantId); if (status.online < status.maxOnline) { @@ -51,6 +84,9 @@ class QueueManager { } 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 = configManager.getTenantConfig(tenantId); if (status.walkin < status.maxWalkin) { @@ -73,6 +109,52 @@ class QueueManager { "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 = configManager.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(); diff --git a/backend_tests.js b/backend_tests.js index 7027732..d1ae9be 100644 --- a/backend_tests.js +++ b/backend_tests.js @@ -42,3 +42,126 @@ console.log("\nTesting Multi-tenant Booking (Sharma Clinic)"); const sharmaRes = queueManager.bookOnline(date, time, 'sharma-clinic'); console.log("Sharma Booking Result:", sharmaRes.success ? `Success - ${sharmaRes.token} (Fee: ${sharmaRes.fee})` : "Failed"); +// 6. Test Scheduling Logic +console.log("\nTesting Scheduling Logic: Closed Hours"); +const closedTime = "14:00"; // 2 PM is closed (1-5 PM break) +const closedRes = queueManager.bookOnline(date, closedTime); +console.log(`Booking at 2 PM (Closed):`, closedRes.success ? "Failed (Should be closed)" : `Success - ${closedRes.message}`); + +console.log("\nTesting Scheduling Logic: Holidays"); +const holidayDate = "2024-12-25"; // Christmas is a holiday in default config +const holidayRes = queueManager.bookOnline(holidayDate, "10:00"); +console.log(`Booking on Christmas (Holiday):`, holidayRes.success ? "Failed (Should be holiday)" : `Success - ${holidayRes.message}`); + +console.log("\nTesting Scheduling Logic: Days Off (Sunday)"); +const sundayDate = "2026-05-17"; // May 17, 2026 is Sunday +const sundayRes = queueManager.bookOnline(sundayDate, "10:00"); +console.log(`Booking on Sunday (Day Off):`, sundayRes.success ? "Failed (Should be off)" : `Success - ${sundayRes.message}`); + +// 7. Test Cancellation & Reschedule Notification +console.log("\nTesting Cancellation & Reschedule Notification"); +const cancelDate = "2026-05-13"; +const cancelTime = "09:00"; + +// Ensure there is a booking to cancel +queueManager.bookOnline(cancelDate, cancelTime); +const cancelRes = queueManager.cancelBooking(cancelDate, cancelTime); +console.log("Cancel result:", cancelRes.message); + +// 8. Test Reschedule Acceptance +console.log("\nTesting Reschedule Acceptance"); +const patientPhone = "9876543210"; +botLogic.setRescheduleOffer(patientPhone, { + date: cancelDate, + oldTime: "19:00", + newTime: cancelTime +}); +async function testReschedule() { + const res = await botLogic.handleMessage(patientPhone, "1"); // User says "Yes" (1) + console.log("User: 1 -> Bot:", res.reply); +} +testReschedule(); + +// 9. Test Future Booking Flow +console.log("\nTesting Future Booking Flow"); +async function testFutureBooking() { + const pId = "1234567890"; + const msg1 = await botLogic.handleMessage(pId, "Hi"); + const msg2 = await botLogic.handleMessage(pId, "English"); + const msg3 = await botLogic.handleMessage(pId, "Book for Future"); + console.log("User: Book for Future -> Bot:", msg3.reply); + console.log("Date Options:", msg3.buttons); + + const selectedDate = msg3.buttons[0]; + const msg4 = await botLogic.handleMessage(pId, selectedDate); + console.log(`User: ${selectedDate} -> Bot:`, msg4.reply); + + const msg5 = await botLogic.handleMessage(pId, "10:00 AM"); + console.log("User: 10:00 AM -> Bot:", msg5.reply); +} +testFutureBooking(); + +// 10. Test Family Profiles Flow +console.log("\nTesting Family Profiles Flow"); +async function testFamilyProfiles() { + const familyPhone = "9876543210"; // Has existing profiles in PatientManager mock + const msg1 = await botLogic.handleMessage(familyPhone, "Hi"); + const msg2 = await botLogic.handleMessage(familyPhone, "English"); + console.log("User: English -> Bot:", msg2.reply); + console.log("Profile Buttons:", msg2.buttons); + + const msg3 = await botLogic.handleMessage(familyPhone, "Suman Verma"); + console.log("User: Suman Verma -> Bot:", msg3.reply); + + // Test Adding New Member + const newPhone = "9999988888"; // New number + const reg1 = await botLogic.handleMessage(newPhone, "Hi"); + const reg2 = await botLogic.handleMessage(newPhone, "English"); + console.log("New User: English -> Bot:", reg2.reply); + + const reg3 = await botLogic.handleMessage(newPhone, "John Doe"); + console.log("New User: John Doe -> Bot:", reg3.reply); + + const reg4 = await botLogic.handleMessage(newPhone, "Book for Today"); + console.log("New User: Book for Today -> Bot:", reg4.reply); +} +testFamilyProfiles(); + +// 11. Test Agentic Pharmacy Flow +console.log("\nTesting Agentic Pharmacy Flow"); +const pharmacyAgent = require('./backend/pharmacyAgent'); +const pharmacyManager = require('./backend/pharmacyManager'); + +async function testPharmacyAgent() { + // A. Test Patient Query + const pId = "1234567890"; + const q1 = await botLogic.handleMessage(pId, "Do you have Paracetamol?"); + console.log("User: Do you have Paracetamol? -> Bot:", q1.reply); + + const q2 = await botLogic.handleMessage(pId, "Do you have Cough Syrup?"); + console.log("User: Do you have Cough Syrup? -> Bot:", q2.reply); + + // B. Test Autonomous Stock Scanning + console.log("\n--- Triggering Autonomous Inventory Scan ---"); + // Manually set one item to very low stock + const inventory = pharmacyManager.getInventory(); + inventory[0].stock = 2; // Paracetamol 500mg + + await pharmacyAgent.analyzeInventory(); + + console.log("Waiting for autonomous restock simulation..."); + setTimeout(() => { + console.log("Post-Restock Stock Level:", inventory[0].stock); + }, 4000); + + // C. Test GST Billing + console.log("\nTesting GST Billing Calculation"); + const bill = pharmacyManager.calculateBill('M04', 10); // Vitamin C (18% GST) + console.log(`Item: ${bill.itemName} (HSN: ${bill.hsn})`); + console.log(`Subtotal: ₹${bill.subtotal}, GST (${bill.gstRate}%): ₹${bill.gstAmount}, Total: ₹${bill.total}`); + + // D. Test Expiry Analysis + console.log("\nTesting Autonomous Expiry Scan"); + pharmacyAgent.analyzeExpiries(pharmacyManager.getInventory()); +} +testPharmacyAgent(); diff --git a/dashboard.css b/dashboard.css index 9d9f1ad..5ffdbe5 100644 --- a/dashboard.css +++ b/dashboard.css @@ -212,6 +212,34 @@ margin-right: 1rem; } +.date-picker-group { + display: flex; + align-items: center; + gap: 0.75rem; + background: white; + padding: 0.5rem 1rem; + border-radius: 12px; + border: 1px solid #E2E8F0; + margin-right: 1.5rem; +} + +.date-picker-group label { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.025em; +} + +.date-input { + border: none; + font-family: 'Inter', sans-serif; + font-weight: 500; + color: var(--primary); + outline: none; + cursor: pointer; +} + .status-pill { padding: 0.4rem 0.8rem; border-radius: 99px; @@ -455,3 +483,128 @@ color: var(--accent); font-weight: 600; } + +/* --- RESPONSIVE DESIGN --- */ + +/* Tablet Breakpoint (1024px) */ +@media (max-width: 1024px) { + .dashboard-body { + grid-template-columns: 80px 1fr; + } + .sidebar .logo-text, .side-nav a span:not(:first-child) { + display: none; + } + .sidebar { + padding: 1rem; + align-items: center; + } + .side-nav a { + justify-content: center; + padding: 1rem 0; + } + .user-profile .info { + display: none; + } + .search-bar input { + width: 250px; + } + .stats-row { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Mobile Breakpoint (768px) */ +@media (max-width: 768px) { + .dashboard-body { + grid-template-columns: 1fr; + height: auto; + } + .sidebar { + display: none; /* Hide sidebar on mobile, we can add a hamburger later */ + } + .dashboard-main { + padding: 1rem; + } + .dash-header { + flex-direction: column; + align-items: stretch; + gap: 1rem; + } + .search-bar input { + width: 100%; + } + .header-actions { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + .date-picker-group { + margin-right: 0; + width: 100%; + justify-content: space-between; + } + .stats-row { + grid-template-columns: 1fr; + } + .queue-table, .queue-table thead, .queue-table tbody, .queue-table th, .queue-table td, .queue-table tr { + display: block; + } + .queue-table thead { + display: none; + } + .queue-table tr { + background: white; + border-radius: 12px; + margin-bottom: 1rem; + padding: 1rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); + border: 1px solid var(--border); + } + .queue-table td { + padding: 0.75rem 0; + display: flex; + justify-content: space-between; + align-items: center; + border: none; + border-bottom: 1px solid #f8fafc; + min-height: 48px; + } + .queue-table td:last-child { + border-bottom: none; + } + .queue-table td::before { + content: attr(data-label); + font-weight: 600; + color: #64748b; + font-size: 0.85rem; + } + .btn-primary, .btn-secondary { + width: 100%; + min-height: 52px; + } + .input-group input, .input-group select { + min-height: 52px; + font-size: 1rem; /* Prevent auto-zoom on iOS */ + } + .sidebar-mobile-toggle { + display: block; + background: var(--primary); + color: white; + padding: 1.25rem; + text-align: center; + font-weight: 700; + cursor: pointer; + min-height: 60px; + } +} + +/* Utilities */ +.hidden-mobile { + display: none !important; +} + +@media (min-width: 769px) { + .sidebar-mobile-toggle { + display: none; + } +} diff --git a/dashboard.html b/dashboard.html index 9dc4268..0f0f102 100644 --- a/dashboard.html +++ b/dashboard.html @@ -10,7 +10,8 @@ -