diff --git a/curio/admin.css b/curio/admin.css
new file mode 100644
index 0000000..ba2a2df
--- /dev/null
+++ b/curio/admin.css
@@ -0,0 +1,258 @@
+/* Curio Central — platform admin shell */
+
+:root {
+ --admin-bg: var(--bg);
+ --admin-card: var(--surface);
+ --admin-primary: var(--primary);
+ --admin-accent: var(--secondary);
+}
+
+body.admin-body {
+ background: var(--admin-bg);
+ font-family: 'Inter', sans-serif;
+}
+
+.admin-sidebar {
+ width: var(--sidebar-width);
+ background: var(--admin-primary);
+ height: 100vh;
+ position: fixed;
+ padding: 2rem;
+ color: white;
+}
+
+.admin-sidebar .logo,
+.admin-sidebar .logo-text {
+ color: white !important;
+}
+
+.admin-sidebar .logo-icon {
+ background: var(--admin-accent);
+}
+
+.admin-sidebar .side-nav {
+ margin-top: 3rem;
+}
+
+.admin-sidebar .side-nav a {
+ color: #94A3B8;
+}
+
+.admin-sidebar .side-nav a:hover,
+.admin-sidebar .side-nav a.active {
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+}
+
+.admin-sidebar .user-profile {
+ position: absolute;
+ bottom: 2rem;
+}
+
+.admin-sidebar .user-profile strong {
+ color: white;
+}
+
+.admin-sidebar .user-profile span {
+ color: #94A3B8;
+}
+
+.admin-sidebar .logout-link {
+ color: #FDA4AF;
+ text-decoration: none;
+ display: block;
+ margin-top: 1rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ transition: var(--transition);
+}
+
+.admin-sidebar .logout-link:hover {
+ color: #FCA5A5;
+}
+
+.admin-main {
+ margin-left: var(--sidebar-width);
+ padding: 3rem;
+}
+
+.admin-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 3rem;
+ gap: 1.5rem;
+ flex-wrap: wrap;
+}
+
+.admin-header h1 {
+ font-family: 'Outfit', sans-serif;
+ font-size: 2.2rem;
+ color: var(--admin-primary);
+}
+
+.admin-header p {
+ color: var(--text-muted);
+ margin-top: 0.35rem;
+}
+
+.admin-main .btn-primary {
+ background: var(--admin-accent);
+}
+
+.admin-main .btn-primary:hover {
+ background: var(--secondary-hover);
+}
+
+.stat-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 3rem;
+}
+
+.admin-body .stat-card {
+ background: var(--admin-card);
+ padding: 2rem;
+ border-radius: 24px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
+ border: 1px solid var(--border);
+}
+
+.admin-body .stat-card span {
+ font-size: 0.85rem;
+ color: var(--text-muted);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.admin-body .stat-card h3 {
+ font-size: 2rem;
+ margin: 0.5rem 0;
+ color: var(--admin-primary);
+}
+
+.admin-body .stat-card .trend {
+ font-size: 0.85rem;
+ font-weight: 700;
+}
+
+.trend.up {
+ color: var(--accent);
+}
+
+.trend.down {
+ color: var(--urgent);
+}
+
+.trend.positive {
+ color: var(--accent);
+}
+
+.admin-body .health-indicator {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ background: #ECFDF5;
+ padding: 0.75rem 1.5rem;
+ border-radius: 100px;
+ color: var(--accent);
+ font-weight: 700;
+ font-size: 0.875rem;
+}
+
+.admin-body .pulse {
+ width: 10px;
+ height: 10px;
+ background: #10B981;
+ border-radius: 50%;
+ animation: admin-pulse-ring 1.5s infinite;
+}
+
+@keyframes admin-pulse-ring {
+ 0% { transform: scale(0.8); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
+ 70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
+ 100% { transform: scale(0.8); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
+}
+
+.tenant-table-container {
+ background: var(--admin-card);
+ border-radius: 32px;
+ padding: 2rem;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
+ border: 1px solid var(--border);
+}
+
+.tenant-table-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.tenant-table-container h2 {
+ font-family: 'Outfit', sans-serif;
+ font-size: 1.5rem;
+ color: var(--admin-primary);
+}
+
+.tenant-table-container table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.tenant-table-container th {
+ text-align: left;
+ padding: 1rem;
+ color: var(--text-muted);
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ border-bottom: 2px solid var(--border);
+ background: var(--bg);
+}
+
+.tenant-table-container td {
+ padding: 1.5rem 1rem;
+ border-bottom: 1px solid var(--border);
+ font-weight: 500;
+ color: var(--text-main);
+}
+
+.tenant-table-container tbody tr:hover {
+ background: var(--bg);
+}
+
+.status-badge {
+ padding: 0.4rem 0.8rem;
+ border-radius: 100px;
+ font-size: 0.75rem;
+ font-weight: 700;
+}
+
+.status-badge.active {
+ background: #DCFCE7;
+ color: #15803D;
+}
+
+@media (max-width: 768px) {
+ .admin-main {
+ margin-left: 0;
+ padding: 1.5rem;
+ padding-top: 2rem;
+ }
+
+ .admin-sidebar {
+ position: relative;
+ width: 100%;
+ height: auto;
+ }
+
+ .admin-sidebar .user-profile {
+ position: static;
+ margin-top: 2rem;
+ }
+}
diff --git a/curio/admin.html b/curio/admin.html
new file mode 100644
index 0000000..fc1c9dd
--- /dev/null
+++ b/curio/admin.html
@@ -0,0 +1,107 @@
+
+
+
+
+
+ Curio Central | Mission Control
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Active Hospitals
+
12
+
↑ 2 this month
+
+
+
Monthly Revenue
+
₹42,50,000
+
↑ 14.5%
+
+
+
Total Patients
+
85,420
+
↑ 5.2k
+
+
+
Avg. Performance
+
240ms
+
↓ 20ms faster
+
+
+
+
+
+
+
+
+ | Clinic ID |
+ Name |
+ Owner |
+ Plan |
+ Status |
+ Actions |
+
+
+
+
+ | #HOSP-001 |
+ Sharma Clinic |
+ Dr. Sharma |
+ Enterprise |
+ ACTIVE |
+ |
+
+
+ | #HOSP-002 |
+ City Heart Hospital |
+ Dr. Verma |
+ Pro |
+ ACTIVE |
+ |
+
+
+
+
+
+
+
diff --git a/curio/backend/botLogic.js b/curio/backend/botLogic.js
index 09d1ffd..c12133f 100644
--- a/curio/backend/botLogic.js
+++ b/curio/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/curio/backend/clinicAgent.js b/curio/backend/clinicAgent.js
new file mode 100644
index 0000000..220d689
--- /dev/null
+++ b/curio/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/curio/backend/configManager.js b/curio/backend/configManager.js
index 64dec09..6282c60 100644
--- a/curio/backend/configManager.js
+++ b/curio/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/curio/backend/i18n.js b/curio/backend/i18n.js
index 6b53621..3f8d1f8 100644
--- a/curio/backend/i18n.js
+++ b/curio/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/curio/backend/notificationManager.js b/curio/backend/notificationManager.js
new file mode 100644
index 0000000..f71deaf
--- /dev/null
+++ b/curio/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/curio/backend/patientManager.js b/curio/backend/patientManager.js
new file mode 100644
index 0000000..57571e3
--- /dev/null
+++ b/curio/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/curio/backend/pharmacyAgent.js b/curio/backend/pharmacyAgent.js
new file mode 100644
index 0000000..d507152
--- /dev/null
+++ b/curio/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/curio/backend/pharmacyManager.js b/curio/backend/pharmacyManager.js
index 4f82133..1edc188 100644
--- a/curio/backend/pharmacyManager.js
+++ b/curio/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/curio/backend/prescriptionManager.js b/curio/backend/prescriptionManager.js
index bd4f37f..aaa1380 100644
--- a/curio/backend/prescriptionManager.js
+++ b/curio/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/curio/backend/queueManager.js b/curio/backend/queueManager.js
index a66abcc..db3a3f4 100644
--- a/curio/backend/queueManager.js
+++ b/curio/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/curio/backend/server.js b/curio/backend/server.js
index 9efe3f2..4bc663b 100644
--- a/curio/backend/server.js
+++ b/curio/backend/server.js
@@ -9,10 +9,19 @@ app.use(express.json());
// Serve static files
app.use(express.static(path.join(__dirname, '../')));
+// Primary Routes
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../login.html'));
});
+app.get('/admin', (req, res) => {
+ res.sendFile(path.join(__dirname, '../admin.html'));
+});
+
+app.get('/dashboard', (req, res) => {
+ res.sendFile(path.join(__dirname, '../dashboard.html'));
+});
+
// Multi-tenant Config API
app.get('/api/config/:tenantId', (req, res) => {
const config = configManager.getTenantConfig(req.params.tenantId);
diff --git a/curio/backend_tests.js b/curio/backend_tests.js
index 7027732..d1ae9be 100644
--- a/curio/backend_tests.js
+++ b/curio/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/curio/billing.css b/curio/billing.css
index 97caace..669acd2 100644
--- a/curio/billing.css
+++ b/curio/billing.css
@@ -1,10 +1,19 @@
.total-card {
- background: linear-gradient(135deg, var(--primary), #1E293B);
+ background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
}
-.total-card span {
- color: #94A3B8;
+.total-card span,
+.total-card small {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+.total-card h3 {
+ color: white;
+}
+
+.total-card .text-accent {
+ color: #6EE7B7;
}
.revenue-grid {
diff --git a/curio/billing.html b/curio/billing.html
index de2486d..c1b3e98 100644
--- a/curio/billing.html
+++ b/curio/billing.html
@@ -4,21 +4,24 @@
Curio Billing | Hospital Revenue & Accounts
+
-
+
+
+