new updates
Build Curio HMS / build-and-deploy (push) Successful in 40s
Details
Build Curio HMS / build-and-deploy (push) Successful in 40s
Details
This commit is contained in:
parent
e18d9254d0
commit
9a21b015d8
|
|
@ -1,106 +1,30 @@
|
||||||
const queueManager = require('./queueManager');
|
const clinicAgent = require('./clinicAgent');
|
||||||
const triageEngine = require('./triageEngine');
|
|
||||||
const i18n = require('./i18n');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BotLogic: Simulates the WhatsApp conversational flow
|
|
||||||
*/
|
|
||||||
class BotLogic {
|
class BotLogic {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.states = {}; // Track session state per patient
|
this.states = {}; // Context storage
|
||||||
|
}
|
||||||
|
|
||||||
|
setRescheduleOffer(patientId, offerData) {
|
||||||
|
this.states[`${patientId}_offer`] = offerData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleMessage(patientId, message) {
|
async handleMessage(patientId, message) {
|
||||||
const text = message.toLowerCase();
|
const context = this.states[patientId] || {};
|
||||||
let state = this.states[patientId] || 'IDLE';
|
|
||||||
let lang = this.states[`${patientId}_lang`] || 'en';
|
|
||||||
|
|
||||||
// 1. Language Selection (First time or reset)
|
// Delegate to the Agent
|
||||||
if (text.includes('hi') && !this.states[`${patientId}_lang`]) {
|
const response = await clinicAgent.processMessage(patientId, message, context);
|
||||||
this.states[patientId] = 'SELECT_LANG';
|
|
||||||
return {
|
// Update local context/state based on agent response
|
||||||
reply: "Welcome to Curio! Please select your language / भाषा चुनें / భాషను ఎంచుకోండి:\n\n1. English\n2. Hindi (हिंदी)\n3. Telugu (తెలుగు)",
|
if (response.nextStep) {
|
||||||
buttons: ["English", "Hindi", "Telugu"]
|
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 {
|
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 || []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -11,23 +11,40 @@ class ConfigManager {
|
||||||
primaryColor: "#0F172A",
|
primaryColor: "#0F172A",
|
||||||
secondaryColor: "#38BDF8",
|
secondaryColor: "#38BDF8",
|
||||||
consultationFee: 500,
|
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': {
|
'sharma-clinic': {
|
||||||
name: "Dr. Sharma's Cardiology",
|
name: "Dr. Sharma's Cardiology",
|
||||||
logo: "S",
|
logo: "S",
|
||||||
primaryColor: "#1E3A8A", // Deep Blue
|
primaryColor: "#1E3A8A",
|
||||||
secondaryColor: "#60A5FA",
|
secondaryColor: "#60A5FA",
|
||||||
consultationFee: 800,
|
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': {
|
'apollo-hospitals': {
|
||||||
name: "Apollo Multispecialty",
|
name: "Apollo Multispecialty",
|
||||||
logo: "A",
|
logo: "A",
|
||||||
primaryColor: "#065F46", // Dark Green
|
primaryColor: "#065F46",
|
||||||
secondaryColor: "#34D399",
|
secondaryColor: "#34D399",
|
||||||
consultationFee: 1200,
|
consultationFee: 1200,
|
||||||
currency: "₹"
|
currency: "₹",
|
||||||
|
workingHours: [
|
||||||
|
{ start: "09:00", end: "22:00" }
|
||||||
|
],
|
||||||
|
daysOff: [],
|
||||||
|
holidays: ["2024-10-02"]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const translations = {
|
||||||
en: {
|
en: {
|
||||||
welcome: "Welcome to Dr. Sharma's Clinic! 🏥 I can help you book a token or check your status.\n\nWould you like to:",
|
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: "Book for Today",
|
||||||
|
book_future: "For future bookings, please call our reception at +91-1234567890 📞",
|
||||||
check: "Check Queue Status",
|
check: "Check Queue Status",
|
||||||
tips: "Daily Health Tips",
|
tips: "Daily Health Tips",
|
||||||
select_slot: "Great! Please select your preferred time slot:",
|
select_slot: "Great! Please select your preferred time slot:",
|
||||||
|
|
@ -17,6 +18,7 @@ const translations = {
|
||||||
hi: {
|
hi: {
|
||||||
welcome: "डॉ. शर्मा के क्लिनिक में आपका स्वागत है! 🏥 मैं टोकन बुक करने या आपकी स्थिति की जांच करने में आपकी मदद कर सकता हूँ।\n\nक्या आप चाहेंगे:",
|
welcome: "डॉ. शर्मा के क्लिनिक में आपका स्वागत है! 🏥 मैं टोकन बुक करने या आपकी स्थिति की जांच करने में आपकी मदद कर सकता हूँ।\n\nक्या आप चाहेंगे:",
|
||||||
book: "आज के लिए बुक करें",
|
book: "आज के लिए बुक करें",
|
||||||
|
book_future: "भविष्य की बुकिंग के लिए, कृपया हमारे रिसेप्शन पर +91-1234567890 पर कॉल करें 📞",
|
||||||
check: "कतार की स्थिति जांचें",
|
check: "कतार की स्थिति जांचें",
|
||||||
tips: "स्वास्थ्य युक्तियाँ",
|
tips: "स्वास्थ्य युक्तियाँ",
|
||||||
select_slot: "बहुत बढ़िया! कृपया अपना पसंदीदा समय स्लॉट चुनें:",
|
select_slot: "बहुत बढ़िया! कृपया अपना पसंदीदा समय स्लॉट चुनें:",
|
||||||
|
|
@ -27,6 +29,7 @@ const translations = {
|
||||||
te: {
|
te: {
|
||||||
welcome: "డాక్టర్ శర్మ క్లినిక్కి స్వాగతం! 🏥 టోకెన్ బుక్ చేసుకోవడంలో లేదా మీ స్థితిని తనిఖీ చేయడంలో నేను మీకు సహాయపడగలను.\n\nమీరు వీటిని చేయాలనుకుంటున్నారా:",
|
welcome: "డాక్టర్ శర్మ క్లినిక్కి స్వాగతం! 🏥 టోకెన్ బుక్ చేసుకోవడంలో లేదా మీ స్థితిని తనిఖీ చేయడంలో నేను మీకు సహాయపడగలను.\n\nమీరు వీటిని చేయాలనుకుంటున్నారా:",
|
||||||
book: "ఈరోజు కోసం బుక్ చేయండి",
|
book: "ఈరోజు కోసం బుక్ చేయండి",
|
||||||
|
book_future: "భవిష్యత్తు బుకింగ్ల కోసం, దయచేసి మా రిసెప్షన్కు +91-1234567890 నంబర్కు కాల్ చేయండి 📞",
|
||||||
check: "క్యూ స్థితిని తనిఖీ చేయండి",
|
check: "క్యూ స్థితిని తనిఖీ చేయండి",
|
||||||
tips: "ఆరోగ్య చిట్కాలు",
|
tips: "ఆరోగ్య చిట్కాలు",
|
||||||
select_slot: "అద్భుతం! దయచేసి మీకు నచ్చిన సమయాన్ని ఎంచుకోండి:",
|
select_slot: "అద్భుతం! దయచేసి మీకు నచ్చిన సమయాన్ని ఎంచుకోండి:",
|
||||||
|
|
@ -49,7 +52,7 @@ class I18nManager {
|
||||||
|
|
||||||
getButtons(lang) {
|
getButtons(lang) {
|
||||||
const t = translations[lang] || translations['en'];
|
const t = translations[lang] || translations['en'];
|
||||||
return [t.book, t.check, t.tips];
|
return [t.book, t.book_future, t.check, t.tips];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -5,13 +5,14 @@
|
||||||
class PharmacyManager {
|
class PharmacyManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.inventory = [
|
this.inventory = [
|
||||||
{ id: 'M01', name: 'Paracetamol 500mg', stock: 120, price: 5 },
|
{ 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 },
|
{ 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: 4, price: 85 },
|
{ 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 },
|
{ 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 },
|
{ 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 }
|
{ id: 'M06', name: 'Insulin Glargine', stock: 12, price: 450, batch: 'I110', expiry: '2026-05-25', hsn: '3004', gst: 5 }
|
||||||
];
|
];
|
||||||
|
this.lowStockThreshold = 10;
|
||||||
this.pendingOrders = [
|
this.pendingOrders = [
|
||||||
{ id: 'ORD-901', patientName: 'Rahul Verma', medicines: 'Paracetamol, Cough Syrup', status: 'PENDING', timestamp: '2026-05-13T09:15:00Z' },
|
{ 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' }
|
{ 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);
|
const order = this.pendingOrders.find(o => o.id === orderId);
|
||||||
if (order) {
|
if (order) {
|
||||||
order.status = 'READY';
|
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: true, message: `Order for ${order.patientName} is ready for pickup.` };
|
||||||
}
|
}
|
||||||
return { success: false, message: "Order not found." };
|
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();
|
module.exports = new PharmacyManager();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class PrescriptionManager {
|
||||||
this.prescriptions = [];
|
this.prescriptions = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(patientId, doctorId, medicines, diagnosis) {
|
generate(patientId, doctorId, medicines, diagnosis, labTests = []) {
|
||||||
const id = `RX-${Date.now()}`;
|
const id = `RX-${Date.now()}`;
|
||||||
const prescription = {
|
const prescription = {
|
||||||
id,
|
id,
|
||||||
|
|
@ -15,16 +15,19 @@ class PrescriptionManager {
|
||||||
doctorId,
|
doctorId,
|
||||||
date: new Date().toISOString(),
|
date: new Date().toISOString(),
|
||||||
diagnosis,
|
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);
|
this.prescriptions.push(prescription);
|
||||||
|
|
||||||
// In a real app, you'd generate a PDF here
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
prescriptionId: id,
|
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._`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const configManager = require('./configManager');
|
const configManager = require('./configManager');
|
||||||
|
const notificationManager = require('./notificationManager');
|
||||||
|
|
||||||
class QueueManager {
|
class QueueManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -20,6 +21,35 @@ class QueueManager {
|
||||||
return `${tenantId}:${date}:${hours}:${roundedMins}`;
|
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) {
|
getSlotStatus(tenantId, date, time) {
|
||||||
const key = this.getSlotKey(tenantId, date, time);
|
const key = this.getSlotKey(tenantId, date, time);
|
||||||
if (!this.slots[key]) {
|
if (!this.slots[key]) {
|
||||||
|
|
@ -35,6 +65,9 @@ class QueueManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
bookOnline(date, time, tenantId = 'default') {
|
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 status = this.getSlotStatus(tenantId, date, time);
|
||||||
const config = configManager.getTenantConfig(tenantId);
|
const config = configManager.getTenantConfig(tenantId);
|
||||||
if (status.online < status.maxOnline) {
|
if (status.online < status.maxOnline) {
|
||||||
|
|
@ -51,6 +84,9 @@ class QueueManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
bookWalkin(date, time, tenantId = 'default') {
|
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 status = this.getSlotStatus(tenantId, date, time);
|
||||||
const config = configManager.getTenantConfig(tenantId);
|
const config = configManager.getTenantConfig(tenantId);
|
||||||
if (status.walkin < status.maxWalkin) {
|
if (status.walkin < status.maxWalkin) {
|
||||||
|
|
@ -73,6 +109,52 @@ class QueueManager {
|
||||||
"Reminder: Don't forget to bring your previous reports."
|
"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();
|
module.exports = new QueueManager();
|
||||||
|
|
|
||||||
123
backend_tests.js
123
backend_tests.js
|
|
@ -42,3 +42,126 @@ console.log("\nTesting Multi-tenant Booking (Sharma Clinic)");
|
||||||
const sharmaRes = queueManager.bookOnline(date, time, 'sharma-clinic');
|
const sharmaRes = queueManager.bookOnline(date, time, 'sharma-clinic');
|
||||||
console.log("Sharma Booking Result:", sharmaRes.success ? `Success - ${sharmaRes.token} (Fee: ${sharmaRes.fee})` : "Failed");
|
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();
|
||||||
|
|
|
||||||
153
dashboard.css
153
dashboard.css
|
|
@ -212,6 +212,34 @@
|
||||||
margin-right: 1rem;
|
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 {
|
.status-pill {
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.4rem 0.8rem;
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
|
|
@ -455,3 +483,128 @@
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 600;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
150
dashboard.html
150
dashboard.html
|
|
@ -10,7 +10,8 @@
|
||||||
<script src="tenantLoader.js"></script>
|
<script src="tenantLoader.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="dashboard-body">
|
<body class="dashboard-body">
|
||||||
<aside class="sidebar">
|
<div class="sidebar-mobile-toggle" onclick="toggleSidebar()">☰ Menu</div>
|
||||||
|
<aside class="sidebar" id="sidebar">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<span class="logo-icon">C</span>
|
<span class="logo-icon">C</span>
|
||||||
<span class="logo-text" data-tenant-name>Curio</span>
|
<span class="logo-text" data-tenant-name>Curio</span>
|
||||||
|
|
@ -19,8 +20,8 @@
|
||||||
<a href="#" class="active" onclick="showSection('overview')"><span>🏠</span> Overview</a>
|
<a href="#" class="active" onclick="showSection('overview')"><span>🏠</span> Overview</a>
|
||||||
<a href="#" onclick="showSection('queue')"><span>👥</span> Queue</a>
|
<a href="#" onclick="showSection('queue')"><span>👥</span> Queue</a>
|
||||||
<a href="#" onclick="showSection('patients')"><span>📂</span> Patients</a>
|
<a href="#" onclick="showSection('patients')"><span>📂</span> Patients</a>
|
||||||
<a href="pharmacy.html"><span>💊</span> Pharmacy</a>
|
<a href="#" onclick="showSection('pharmacy')"><span>💊</span> Pharmacy</a>
|
||||||
<a href="lab.html"><span>🔬</span> Lab</a>
|
<a href="#" onclick="showSection('lab')"><span>🔬</span> Lab</a>
|
||||||
<a href="billing.html"><span>💳</span> Billing & Accounts</a>
|
<a href="billing.html"><span>💳</span> Billing & Accounts</a>
|
||||||
<a href="staff.html"><span>👔</span> Staff & Payroll</a>
|
<a href="staff.html"><span>👔</span> Staff & Payroll</a>
|
||||||
<a href="settings.html"><span>⚙️</span> Settings</a>
|
<a href="settings.html"><span>⚙️</span> Settings</a>
|
||||||
|
|
@ -41,8 +42,12 @@
|
||||||
<input type="text" placeholder="Search patients by name or WhatsApp number...">
|
<input type="text" placeholder="Search patients by name or WhatsApp number...">
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<div class="date-picker-group">
|
||||||
|
<label for="queueDate">Select Date:</label>
|
||||||
|
<input type="date" id="queueDate" class="date-input">
|
||||||
|
</div>
|
||||||
<span class="badge counter-badge">Reception Counter: 1</span>
|
<span class="badge counter-badge">Reception Counter: 1</span>
|
||||||
<button class="btn-primary">+ New Walk-in (Print Token)</button>
|
<button class="btn-primary" id="newWalkinBtn">+ New Walk-in (Print Token)</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -170,21 +175,70 @@
|
||||||
<td>5</td>
|
<td>5</td>
|
||||||
<td><button class="btn-small secondary">View History</button></td>
|
<td><button class="btn-small secondary">View History</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>CF-1002</td>
|
|
||||||
<td><strong>Anjali Singh</strong></td>
|
|
||||||
<td>+91 98234 56789</td>
|
|
||||||
<td>Today</td>
|
|
||||||
<td>2</td>
|
|
||||||
<td><button class="btn-small secondary">View History</button></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div id="prescriptionModal" class="modal">
|
|
||||||
<div class="modal-content">
|
<div id="pharmacy-section" class="hidden">
|
||||||
<div class="modal-header">
|
<section class="section-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>💊 Pharmacy Inventory & Sales</h2>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn-secondary">Expiry Alerts (2)</button>
|
||||||
|
<button class="btn-primary">+ Add Stock</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-row">
|
||||||
|
<div class="stat-card"><span>Total Inventory Value</span><h3>₹4,25,000</h3></div>
|
||||||
|
<div class="stat-card"><span>Daily Sales (GST)</span><h3>₹12,450</h3></div>
|
||||||
|
</div>
|
||||||
|
<table class="queue-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Medicine</th>
|
||||||
|
<th>Batch</th>
|
||||||
|
<th>Expiry</th>
|
||||||
|
<th>Stock</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>M01</td><td><strong>Paracetamol 500mg</strong></td><td>B2201</td><td>Dec 2026</td><td>120</td><td>₹5</td><td><button class="btn-small">Edit</button></td></tr>
|
||||||
|
<tr><td>M03</td><td><strong>Cough Syrup</strong></td><td>S881</td><td><span class="text-accent">Apr 2026</span></td><td>4</td><td>₹85</td><td><button class="btn-small secondary">Restock</button></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="lab-section" class="hidden">
|
||||||
|
<section class="section-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>🔬 Laboratory Orders</h2>
|
||||||
|
<span class="badge">4 New Orders</span>
|
||||||
|
</div>
|
||||||
|
<table class="queue-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Order ID</th>
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Test Name</th>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>LAB-101</td><td><strong>Rahul Verma</strong></td><td>CBC (Complete Blood Count)</td><td><span class="triage-pill urgent">High</span></td><td><span class="status-pill waiting">Sample Awaited</span></td><td><button class="btn-small">Collect Sample</button></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div id="consultationModal" class="modal">
|
||||||
|
<div class="modal-content" style="width: 900px; max-width: 95%;">
|
||||||
|
<div class="modal-header">
|
||||||
<h2>Digital Prescription</h2>
|
<h2>Digital Prescription</h2>
|
||||||
<span class="close-modal">×</span>
|
<span class="close-modal">×</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -199,18 +253,64 @@
|
||||||
<p id="insightText"></p>
|
<p id="insightText"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="doctor-panels-grid" style="display: grid; grid-template-columns: 1.5fr 1fr; gap: 1.5rem; margin-top: 1rem;">
|
||||||
<label>Diagnosis</label>
|
<!-- Column 1: Clinical Actions -->
|
||||||
<input type="text" id="rxDiagnosis" placeholder="e.g. Viral Fever">
|
<div class="clinical-inputs">
|
||||||
</div>
|
<div class="input-group">
|
||||||
<div class="medicine-list" id="medicineList">
|
<label>Diagnosis</label>
|
||||||
<div class="med-row">
|
<input type="text" id="rxDiagnosis" placeholder="e.g. Viral Fever">
|
||||||
<input type="text" placeholder="Medicine Name" class="med-name">
|
</div>
|
||||||
<input type="text" placeholder="Dosage" class="med-dosage">
|
<div class="medicine-list" id="medicineList">
|
||||||
<input type="text" placeholder="Freq" class="med-freq">
|
<label>Medicines & Dosages</label>
|
||||||
|
<div class="med-row">
|
||||||
|
<input type="text" placeholder="Medicine Name" class="med-name">
|
||||||
|
<input type="text" placeholder="Dosage (e.g. 1-0-1)" class="med-dosage">
|
||||||
|
<input type="text" placeholder="Duration" class="med-duration">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-secondary" id="addMed">+ Add Medicine</button>
|
||||||
|
|
||||||
|
<div class="lab-requests-section" style="margin-top: 1.5rem;">
|
||||||
|
<label>🔬 Request Lab Tests</label>
|
||||||
|
<div id="labTestList">
|
||||||
|
<div class="lab-row" style="display: flex; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||||
|
<input type="text" placeholder="Test Name (e.g. CBC, Lipid)" class="lab-test-name" style="flex: 1; padding: 0.6rem; border-radius: 8px; border: 1px solid var(--border);">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-secondary" id="addLabTest">+ Add Lab Test</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 2: Quick Reference & Sharing -->
|
||||||
|
<div class="quick-reference-panel" style="background: var(--bg); padding: 1rem; border-radius: 12px; border: 1px solid var(--border);">
|
||||||
|
<div class="panel-section">
|
||||||
|
<label style="font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase;">📂 Patient History</label>
|
||||||
|
<div class="history-list" style="font-size: 0.85rem; margin-top: 0.5rem;">
|
||||||
|
<div class="history-item" style="padding: 0.5rem; border-bottom: 1px solid #e2e8f0;">
|
||||||
|
<strong>12 May 2026</strong>: Chronic Gastritis. Rx: Pantoprazole.
|
||||||
|
</div>
|
||||||
|
<div class="history-item" style="padding: 0.5rem; border-bottom: 1px solid #e2e8f0;">
|
||||||
|
<strong>05 Apr 2026</strong>: Lab Report: HbA1c 7.2% (High) 🔬
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-section" style="margin-top: 1.5rem;">
|
||||||
|
<label style="font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase;">⚡ Quick Share Health Tips</label>
|
||||||
|
<div class="tips-btns" style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
<button class="btn-small secondary" onclick="shareTip('diabetes')">🥗 Diabetes Diet</button>
|
||||||
|
<button class="btn-small secondary" onclick="shareTip('hypertension')">🧘 BP Control</button>
|
||||||
|
<button class="btn-small secondary" onclick="shareTip('viral')">💧 Hydration</button>
|
||||||
|
<button class="btn-small secondary" onclick="printTips()">🖨️ Print Tips</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-section" style="margin-top: 1.5rem;">
|
||||||
|
<label style="font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase;">📝 Note to Depts</label>
|
||||||
|
<textarea id="deptNote" placeholder="Note to Pharmacy or Lab..." style="width: 100%; height: 60px; margin-top: 0.5rem; padding: 0.5rem; border-radius: 8px; border: 1px solid #e2e8f0; font-size: 0.85rem;"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-secondary" id="addMed">+ Add Medicine</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn-primary" id="sendRx">Send to WhatsApp</button>
|
<button class="btn-primary" id="sendRx">Send to WhatsApp</button>
|
||||||
|
|
|
||||||
102
dashboard.js
102
dashboard.js
|
|
@ -1,4 +1,4 @@
|
||||||
const modal = document.getElementById("prescriptionModal");
|
const modal = document.getElementById("consultationModal");
|
||||||
const addMedBtn = document.getElementById("addMed");
|
const addMedBtn = document.getElementById("addMed");
|
||||||
const medList = document.getElementById("medicineList");
|
const medList = document.getElementById("medicineList");
|
||||||
const sendRxBtn = document.getElementById("sendRx");
|
const sendRxBtn = document.getElementById("sendRx");
|
||||||
|
|
@ -7,6 +7,33 @@ const startRecordBtn = document.getElementById("startRecording");
|
||||||
const recordingStatus = document.getElementById("recordingStatus");
|
const recordingStatus = document.getElementById("recordingStatus");
|
||||||
const aiInsightsBox = document.getElementById("aiInsights");
|
const aiInsightsBox = document.getElementById("aiInsights");
|
||||||
const insightText = document.getElementById("insightText");
|
const insightText = document.getElementById("insightText");
|
||||||
|
const queueDateInput = document.getElementById("queueDate");
|
||||||
|
const newWalkinBtn = document.getElementById("newWalkinBtn");
|
||||||
|
|
||||||
|
// Initialize Date Picker to Today
|
||||||
|
if (queueDateInput) {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
queueDateInput.value = today;
|
||||||
|
queueDateInput.min = today; // Prevent past dates
|
||||||
|
|
||||||
|
queueDateInput.addEventListener("change", (e) => {
|
||||||
|
console.log(`Switching view to date: ${e.target.value}`);
|
||||||
|
// In a real app, this would fetch the queue for that specific date
|
||||||
|
alert(`Now viewing appointments for: ${e.target.value}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle New Walk-in for selected date
|
||||||
|
if (newWalkinBtn) {
|
||||||
|
newWalkinBtn.addEventListener("click", () => {
|
||||||
|
const selectedDate = queueDateInput.value;
|
||||||
|
const name = prompt("Enter Patient Name:");
|
||||||
|
if (name) {
|
||||||
|
console.log(`Booking walk-in for ${name} on ${selectedDate}`);
|
||||||
|
alert(`Token generated for ${name} on ${selectedDate}. \n\nPlease print the token and hand it to the patient.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Open modal when clicking 'Prescribe'
|
// Open modal when clicking 'Prescribe'
|
||||||
document.querySelectorAll(".btn-small").forEach(btn => {
|
document.querySelectorAll(".btn-small").forEach(btn => {
|
||||||
|
|
@ -22,18 +49,32 @@ window.onclick = (event) => {
|
||||||
if (event.target == modal) modal.style.display = "none";
|
if (event.target == modal) modal.style.display = "none";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labList = document.getElementById("labTestList");
|
||||||
|
const addLabBtn = document.getElementById("addLabTest");
|
||||||
|
|
||||||
// Add new medicine row
|
// Add new medicine row
|
||||||
addMedBtn.onclick = () => {
|
addMedBtn.onclick = () => {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "med-row";
|
row.className = "med-row";
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<input type="text" placeholder="Medicine Name" class="med-name">
|
<input type="text" placeholder="Medicine Name" class="med-name">
|
||||||
<input type="text" placeholder="Dosage" class="med-dosage">
|
<input type="text" placeholder="Dosage (e.g. 1-0-1)" class="med-dosage">
|
||||||
<input type="text" placeholder="Freq" class="med-freq">
|
<input type="text" placeholder="Duration" class="med-duration">
|
||||||
`;
|
`;
|
||||||
medList.appendChild(row);
|
medList.appendChild(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add new lab test row
|
||||||
|
addLabBtn.onclick = () => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "lab-row";
|
||||||
|
row.style = "display: flex; gap: 0.5rem; margin-bottom: 0.5rem;";
|
||||||
|
row.innerHTML = `
|
||||||
|
<input type="text" placeholder="Test Name (e.g. CBC, Lipid)" class="lab-test-name" style="flex: 1; padding: 0.6rem; border-radius: 8px; border: 1px solid var(--border);">
|
||||||
|
`;
|
||||||
|
labList.appendChild(row);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle sending prescription
|
// Handle sending prescription
|
||||||
sendRxBtn.onclick = () => {
|
sendRxBtn.onclick = () => {
|
||||||
const diagnosis = document.getElementById("rxDiagnosis").value;
|
const diagnosis = document.getElementById("rxDiagnosis").value;
|
||||||
|
|
@ -44,7 +85,7 @@ sendRxBtn.onclick = () => {
|
||||||
meds.push({
|
meds.push({
|
||||||
name,
|
name,
|
||||||
dosage: row.querySelector(".med-dosage").value,
|
dosage: row.querySelector(".med-dosage").value,
|
||||||
frequency: row.querySelector(".med-freq").value
|
duration: row.querySelector(".med-duration").value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -65,10 +106,30 @@ sendRxBtn.onclick = () => {
|
||||||
modal.style.display = "none";
|
modal.style.display = "none";
|
||||||
sendRxBtn.innerText = "Send to WhatsApp";
|
sendRxBtn.innerText = "Send to WhatsApp";
|
||||||
sendRxBtn.style.background = "";
|
sendRxBtn.style.background = "";
|
||||||
alert("Prescription has been sent to the patient's WhatsApp.");
|
|
||||||
|
const note = document.getElementById("deptNote").value;
|
||||||
|
if (note) {
|
||||||
|
console.log("Dept Note:", note);
|
||||||
|
alert("Prescription sent. Note forwarded to Pharmacy/Lab.");
|
||||||
|
} else {
|
||||||
|
alert("Prescription has been sent to the patient's WhatsApp.");
|
||||||
|
}
|
||||||
}, 1500);
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.shareTip = (type) => {
|
||||||
|
const tips = {
|
||||||
|
diabetes: "🥗 Healthy Diet Tip: Reduce white rice, add more greens.",
|
||||||
|
hypertension: "🧘 BP Control: Limit salt intake to 5g/day.",
|
||||||
|
viral: "💧 Hydration: Drink 3L of warm water daily."
|
||||||
|
};
|
||||||
|
alert(`📤 Sent to Patient's WhatsApp:\n\n${tips[type]}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.printTips = () => {
|
||||||
|
alert("🖨️ Printing General Health Guidelines for this patient...");
|
||||||
|
};
|
||||||
|
|
||||||
// Voice AI Logic
|
// Voice AI Logic
|
||||||
startRecordBtn.onclick = () => {
|
startRecordBtn.onclick = () => {
|
||||||
startRecordBtn.classList.add("hidden");
|
startRecordBtn.classList.add("hidden");
|
||||||
|
|
@ -97,8 +158,11 @@ startRecordBtn.onclick = () => {
|
||||||
// Section Switching Logic
|
// Section Switching Logic
|
||||||
window.showSection = (sectionId) => {
|
window.showSection = (sectionId) => {
|
||||||
// Hide all sections
|
// Hide all sections
|
||||||
document.getElementById('overview-section').classList.add('hidden');
|
const sections = ['overview-section', 'patients-section', 'pharmacy-section', 'lab-section'];
|
||||||
document.getElementById('patients-section').classList.add('hidden');
|
sections.forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
// Show target section
|
// Show target section
|
||||||
if (sectionId === 'overview') {
|
if (sectionId === 'overview') {
|
||||||
|
|
@ -108,6 +172,10 @@ window.showSection = (sectionId) => {
|
||||||
document.getElementById('queue-section').scrollIntoView({ behavior: 'smooth' });
|
document.getElementById('queue-section').scrollIntoView({ behavior: 'smooth' });
|
||||||
} else if (sectionId === 'patients') {
|
} else if (sectionId === 'patients') {
|
||||||
document.getElementById('patients-section').classList.remove('hidden');
|
document.getElementById('patients-section').classList.remove('hidden');
|
||||||
|
} else if (sectionId === 'pharmacy') {
|
||||||
|
document.getElementById('pharmacy-section').classList.remove('hidden');
|
||||||
|
} else if (sectionId === 'lab') {
|
||||||
|
document.getElementById('lab-section').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update active state in nav
|
// Update active state in nav
|
||||||
|
|
@ -117,4 +185,24 @@ window.showSection = (sectionId) => {
|
||||||
link.classList.add('active');
|
link.classList.add('active');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-hide sidebar on mobile after clicking
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
document.getElementById('sidebar').style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleSidebar = () => {
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
if (sidebar.style.display === 'flex') {
|
||||||
|
sidebar.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
sidebar.style.display = 'flex';
|
||||||
|
sidebar.style.position = 'fixed';
|
||||||
|
sidebar.style.top = '50px';
|
||||||
|
sidebar.style.left = '0';
|
||||||
|
sidebar.style.width = '100%';
|
||||||
|
sidebar.style.height = 'calc(100vh - 50px)';
|
||||||
|
sidebar.style.zIndex = '999';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
23
index.css
23
index.css
|
|
@ -15,6 +15,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -155,7 +156,12 @@ h1 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, background 0.3s;
|
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s;
|
||||||
|
user-select: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
|
|
@ -163,6 +169,10 @@ h1 {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: white;
|
background: white;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
|
|
@ -172,7 +182,12 @@ h1 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
user-select: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
|
|
@ -180,6 +195,10 @@ h1 {
|
||||||
border-color: var(--text-muted);
|
border-color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-secondary:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-lg {
|
.btn-lg {
|
||||||
padding: 1.25rem 2.5rem;
|
padding: 1.25rem 2.5rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* Patient Journey Simulation:
|
||||||
|
* 1. OPD Payment
|
||||||
|
* 2. Doctor Consultation (Prescription + Lab Request)
|
||||||
|
* 3. Pharmacy Dispensing (Stock + GST Bill)
|
||||||
|
* 4. Lab Forwarding
|
||||||
|
*/
|
||||||
|
|
||||||
|
const queueManager = require('./backend/queueManager');
|
||||||
|
const prescriptionManager = require('./backend/prescriptionManager');
|
||||||
|
const pharmacyManager = require('./backend/pharmacyManager');
|
||||||
|
const billingManager = require('./backend/billingManager');
|
||||||
|
|
||||||
|
async function startJourney() {
|
||||||
|
console.log("=== 🏥 CURIO PATIENT JOURNEY START ===\n");
|
||||||
|
const patientId = "P-8899";
|
||||||
|
const patientName = "Rahul Verma";
|
||||||
|
|
||||||
|
// 1. OPD Registration & Payment
|
||||||
|
console.log("Step 1: Patient arrives at OPD and pays consultation fee.");
|
||||||
|
const opdToken = queueManager.bookOnline("2026-05-14", "10:00");
|
||||||
|
console.log(`[Billing] OPD Invoice Generated: ₹${opdToken.fee}. Status: PAID ✅`);
|
||||||
|
console.log(`[Queue] Token Issued: ${opdToken.token}. Patient is in queue.\n`);
|
||||||
|
|
||||||
|
// 2. Doctor Consultation
|
||||||
|
console.log("Step 2: Doctor sees patient and writes prescription + lab tests.");
|
||||||
|
const medicines = [
|
||||||
|
{ name: "Paracetamol", dosage: "500mg", frequency: "1-0-1", duration: "5 Days", qty: 10 },
|
||||||
|
{ name: "Cough Syrup", dosage: "10ml", frequency: "0-0-1", duration: "3 Days", qty: 1 }
|
||||||
|
];
|
||||||
|
const labTests = [
|
||||||
|
{ testName: "Complete Blood Count (CBC)", instructions: "Fasting not required" }
|
||||||
|
];
|
||||||
|
const rx = prescriptionManager.generate(patientId, "DR-SHARMA", medicines, "Viral Fever", labTests);
|
||||||
|
console.log("[Doctor] Prescription Generated. Sent to patient via WhatsApp.");
|
||||||
|
console.log(rx.whatsappMessage + "\n");
|
||||||
|
|
||||||
|
// 3. Pharmacy Fulfillment
|
||||||
|
console.log("Step 3: Patient goes to Pharmacy.");
|
||||||
|
const fulfillment = pharmacyManager.fulfillPrescription(rx.data);
|
||||||
|
|
||||||
|
console.log("\n--- 💳 Pharmacy GST Bill ---");
|
||||||
|
fulfillment.billItems.forEach(item => {
|
||||||
|
console.log(`${item.itemName} | HSN: ${item.hsn} | Qty: ${item.qty} | GST: ${item.gstRate}% | Total: ₹${item.total.toFixed(2)}`);
|
||||||
|
});
|
||||||
|
console.log(`GRAND TOTAL: ₹${fulfillment.totalBill.toFixed(2)}`);
|
||||||
|
console.log("Status: PAID ✅\n");
|
||||||
|
|
||||||
|
// 4. Lab Integration
|
||||||
|
console.log("Step 4: Checking Lab Status...");
|
||||||
|
if (fulfillment.labStatus === "FORWARDED") {
|
||||||
|
console.log("✅ SUCCESS: Lab tests have been automatically synced with the Lab department.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n=== 🏁 PATIENT JOURNEY COMPLETE ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
startJourney();
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* Final Integration Test: Curio HMS Ecosystem
|
||||||
|
* Validates: Family Profiles, Agentic Bot, Pharmacy Autonomy, GST Billing, and Lab Handover.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const botLogic = require('./backend/botLogic');
|
||||||
|
const pharmacyAgent = require('./backend/pharmacyAgent');
|
||||||
|
const pharmacyManager = require('./backend/pharmacyManager');
|
||||||
|
const patientManager = require('./backend/patientManager');
|
||||||
|
const queueManager = require('./backend/queueManager');
|
||||||
|
|
||||||
|
async function runAudit() {
|
||||||
|
console.log("=== 🛡️ CURIO FULL SYSTEM AUDIT START ===\n");
|
||||||
|
|
||||||
|
const phone = "+91-9876543210";
|
||||||
|
|
||||||
|
// 1. TEST: AGENTIC FAMILY MANAGEMENT
|
||||||
|
console.log("Scenario 1: Family Member Management");
|
||||||
|
const r1 = await botLogic.handleMessage(phone, "Hi");
|
||||||
|
console.log("Input: 'Hi' -> Bot:", r1.reply);
|
||||||
|
|
||||||
|
const r2 = await botLogic.handleMessage(phone, "Add new member: Arya Verma");
|
||||||
|
console.log("Input: 'Add member: Arya Verma' -> Bot:", r2.reply);
|
||||||
|
|
||||||
|
// 2. TEST: CLINICAL WORKFLOW (Prescription to Lab)
|
||||||
|
console.log("\nScenario 2: Clinical Workflow & GST Billing");
|
||||||
|
const medicines = [
|
||||||
|
{ name: "Amoxicillin", qty: 15, dosage: "250mg", duration: "5 Days" },
|
||||||
|
{ name: "Insulin", qty: 2, dosage: "10 units", duration: "Monthly" }
|
||||||
|
];
|
||||||
|
const labTests = [{ testName: "HbA1c" }];
|
||||||
|
|
||||||
|
// Simulate pharmacy fulfillment of this Rx
|
||||||
|
const mockRx = { id: "RX-AUDIT-001", patientId: "Arya-1", medicines, labTests };
|
||||||
|
const fulfillment = pharmacyManager.fulfillPrescription(mockRx);
|
||||||
|
|
||||||
|
console.log(`[Pharmacy] Bill Generated: ₹${fulfillment.totalBill.toFixed(2)} (GST Included)`);
|
||||||
|
console.log(`[Lab] Verification: Lab Status is ${fulfillment.labStatus}`);
|
||||||
|
|
||||||
|
// 3. TEST: AUTONOMOUS PHARMACY (The "Agentic" Part)
|
||||||
|
console.log("\nScenario 3: Autonomous Inventory Management");
|
||||||
|
// Force low stock for Azithromycin
|
||||||
|
const inventory = pharmacyManager.getInventory();
|
||||||
|
const azithro = inventory.find(i => i.name.includes("Azithromycin"));
|
||||||
|
azithro.stock = 5;
|
||||||
|
console.log(`[System] Manually dropped ${azithro.name} stock to 5.`);
|
||||||
|
|
||||||
|
await pharmacyAgent.analyzeInventory();
|
||||||
|
|
||||||
|
// 4. TEST: EXPIRY SENTINEL
|
||||||
|
console.log("\nScenario 4: Proactive Expiry Detection");
|
||||||
|
pharmacyAgent.analyzeExpiries(inventory);
|
||||||
|
|
||||||
|
// 5. TEST: AGENTIC NATURAL LANGUAGE QUERY
|
||||||
|
console.log("\nScenario 5: Natural Language Medicine Inquiry");
|
||||||
|
const r3 = await botLogic.handleMessage(phone, "Do you have Paracetamol?");
|
||||||
|
console.log("Input: 'Do you have Paracetamol?' -> Bot:", r3.reply);
|
||||||
|
|
||||||
|
console.log("\n=== 🏁 AUDIT COMPLETE: ALL MODULES FUNCTIONAL ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
runAudit();
|
||||||
Loading…
Reference in New Issue