1155 lines
44 KiB
JavaScript
1155 lines
44 KiB
JavaScript
// Core UI & Modal Elements
|
|
const modal = document.getElementById("consultationModal");
|
|
const addMedBtn = document.getElementById("addMed");
|
|
const medList = document.getElementById("medicineList");
|
|
const sendRxBtn = document.getElementById("sendRx");
|
|
const closeModal = document.querySelector(".rx-modal .close-modal");
|
|
const cancelRxBtn = document.getElementById("cancelRx");
|
|
const startRecordBtn = document.getElementById("startRecording");
|
|
const recordingStatus = document.getElementById("recordingStatus");
|
|
const aiInsightsBox = document.getElementById("aiInsights");
|
|
const insightText = document.getElementById("insightText");
|
|
const queueDateInput = document.getElementById("queueDate");
|
|
const newWalkinBtn = document.getElementById("newWalkinBtn");
|
|
const labList = document.getElementById("labTestList");
|
|
const addLabBtn = document.getElementById("addLabTest");
|
|
|
|
// 1. App State & Stores
|
|
let isScribing = false;
|
|
let currentPatientId = "#012";
|
|
let recognition;
|
|
let mediaRecorder;
|
|
let audioChunks = [];
|
|
const patientDataStore = {
|
|
"#012": { notes: "", summary: "" },
|
|
"#013": { notes: "", summary: "" },
|
|
"#014": { notes: "", summary: "" }
|
|
};
|
|
|
|
const SECTION_META = {
|
|
overview: {
|
|
target: 'overview-section',
|
|
title: 'Clinic Overview',
|
|
subtitle: 'Live queue, stats, and AI insights for today.'
|
|
},
|
|
queue: {
|
|
target: 'overview-section',
|
|
title: 'Live Queue',
|
|
subtitle: 'Patients waiting and in consultation.'
|
|
},
|
|
patients: {
|
|
target: 'patients-section',
|
|
title: 'Patient Records',
|
|
subtitle: 'Search history, visits, and contact details.'
|
|
},
|
|
pharmacy: {
|
|
target: 'pharmacy-section',
|
|
title: 'Pharmacy',
|
|
subtitle: 'Inventory, prescriptions, and stock alerts.'
|
|
},
|
|
lab: {
|
|
target: 'lab-section',
|
|
title: 'Laboratory',
|
|
subtitle: 'Orders, sample collection, and results.'
|
|
},
|
|
zen: {
|
|
target: 'doctor-zen-section',
|
|
title: 'Clinical Workspace',
|
|
subtitle: 'Document visits, prescribe, and review vitals.'
|
|
},
|
|
settings: {
|
|
target: 'settings-section',
|
|
title: 'Preferences',
|
|
subtitle: 'Manage your profile and clinical tool settings.'
|
|
}
|
|
};
|
|
|
|
const ROLE_CONFIG = {
|
|
DOCTOR: {
|
|
hideSections: ['overview', 'billing', 'staff'],
|
|
defaultSection: 'zen',
|
|
displayName: 'Dr. Sharma'
|
|
},
|
|
OWNER: {
|
|
hideSections: [],
|
|
defaultSection: 'overview',
|
|
displayName: null
|
|
},
|
|
RECEPTIONIST: {
|
|
hideSections: ['pharmacy', 'lab'],
|
|
defaultSection: 'queue',
|
|
displayName: 'Reception'
|
|
}
|
|
};
|
|
|
|
function getUserRole() {
|
|
return localStorage.getItem('userRole');
|
|
}
|
|
|
|
function resolveSectionTarget(sectionId) {
|
|
const role = getUserRole();
|
|
const config = ROLE_CONFIG[role] || ROLE_CONFIG.OWNER;
|
|
|
|
if (sectionId === 'queue' && role === 'DOCTOR') return 'doctor-zen-section';
|
|
if (sectionId === 'zen') return 'doctor-zen-section';
|
|
|
|
const meta = SECTION_META[sectionId];
|
|
return meta ? meta.target : 'overview-section';
|
|
}
|
|
|
|
function updateSectionPageHeader(sectionId) {
|
|
const header = document.getElementById('section-page-header');
|
|
const titleEl = document.getElementById('sectionPageTitle');
|
|
const subtitleEl = document.getElementById('sectionPageSubtitle');
|
|
if (!header || !titleEl || !subtitleEl) return;
|
|
|
|
const isDoctor = getUserRole() === 'DOCTOR';
|
|
const isClinicalView = sectionId === 'zen' || sectionId === 'queue';
|
|
const meta = SECTION_META[sectionId];
|
|
|
|
if (isDoctor && !isClinicalView && meta) {
|
|
titleEl.textContent = meta.title;
|
|
subtitleEl.textContent = meta.subtitle;
|
|
header.classList.remove('hidden');
|
|
} else {
|
|
header.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function setActiveNavLink(sectionId) {
|
|
const isDoctor = getUserRole() === 'DOCTOR';
|
|
const activeKey = (sectionId === 'zen' || (sectionId === 'queue' && isDoctor)) ? 'queue' : sectionId;
|
|
|
|
document.querySelectorAll('.side-nav a[data-section]').forEach(link => {
|
|
link.classList.toggle('active', link.dataset.section === activeKey);
|
|
});
|
|
}
|
|
|
|
function applyRoleNav() {
|
|
const role = getUserRole();
|
|
const config = ROLE_CONFIG[role] || ROLE_CONFIG.OWNER;
|
|
|
|
document.querySelectorAll('.side-nav a[data-section]').forEach(link => {
|
|
const hidden = config.hideSections.includes(link.dataset.section);
|
|
link.style.display = hidden ? 'none' : '';
|
|
});
|
|
}
|
|
|
|
// 2. Initialization & Speech Recognition Setup
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const payload = requireLogin(['OWNER', 'DOCTOR', 'RECEPTIONIST']);
|
|
if (!payload) return;
|
|
|
|
const userRole = payload.role || getUserRole();
|
|
const userEmail = localStorage.getItem('userEmail') || 'admin@curio.app';
|
|
const config = ROLE_CONFIG[userRole] || ROLE_CONFIG.OWNER;
|
|
|
|
const roleDisplay = document.querySelector(".user-profile .info strong");
|
|
const emailDisplay = document.querySelector(".user-profile .info span");
|
|
|
|
if (roleDisplay) {
|
|
roleDisplay.innerText = config.displayName || (userRole.charAt(0) + userRole.slice(1).toLowerCase());
|
|
}
|
|
if (emailDisplay) emailDisplay.innerText = userEmail;
|
|
|
|
const sidebar = document.getElementById('sidebar');
|
|
if (userRole === 'DOCTOR') {
|
|
document.body.classList.add('role-doctor');
|
|
sidebar?.classList.add('compact-rail');
|
|
} else {
|
|
document.body.classList.remove('role-doctor');
|
|
sidebar?.classList.remove('compact-rail');
|
|
}
|
|
|
|
applyRoleNav();
|
|
|
|
document.getElementById('sideNav')?.addEventListener('click', (e) => {
|
|
const link = e.target.closest('a[data-section]');
|
|
if (!link || link.hasAttribute('data-external')) return;
|
|
e.preventDefault();
|
|
showSection(link.dataset.section);
|
|
});
|
|
|
|
showSection(config.defaultSection === 'zen' ? 'zen' : config.defaultSection);
|
|
|
|
// Zen 4.0 Macros & Nudge Logic
|
|
const MACROS = {
|
|
".fever": "Patient presents with elevated body temperature. Antipyretics advised. Rest and hydration recommended.",
|
|
".cough": "Patient reports persistent cough. Chest sounds clear bilaterally. Recommended symptomatic relief.",
|
|
".cold": "Patient reports nasal congestion and rhinorrhea. Viral etiology suspected. Supportive care advised."
|
|
};
|
|
|
|
const nudgeText = document.getElementById("aiNudgeText");
|
|
const tagsContainer = document.querySelector(".suggested-tags");
|
|
|
|
document.addEventListener("input", (e) => {
|
|
if (e.target && e.target.tagName === 'TEXTAREA' && e.target.id.startsWith('soap-')) {
|
|
let val = e.target.value;
|
|
let modified = false;
|
|
for (const [macro, text] of Object.entries(MACROS)) {
|
|
if (val.includes(macro)) {
|
|
val = val.replace(macro, text);
|
|
modified = true;
|
|
}
|
|
}
|
|
if (modified) {
|
|
e.target.value = val;
|
|
// Dispatch event so other listeners (if any) know the value changed
|
|
e.target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
return; // Prevent recursive trigger on this pass
|
|
}
|
|
|
|
// Nudge Logic update
|
|
if (nudgeText && tagsContainer) {
|
|
const lowerVal = val.toLowerCase();
|
|
if (lowerVal.includes("fever") || lowerVal.includes("temperature")) {
|
|
nudgeText.innerText = "✨ AI suggests: Patient has elevated temp. Consider prescribing Paracetamol and checking for throat infection.";
|
|
tagsContainer.innerHTML = '<span>#ViralFever</span><span>#InfectionCheck</span>';
|
|
} else if (lowerVal.includes("cough") || lowerVal.includes("breath")) {
|
|
nudgeText.innerText = "✨ AI suggests: Respiratory symptoms detected. Check SpO2 and listen for chest congestion.";
|
|
tagsContainer.innerHTML = '<span>#AsthmaProtocol</span><span>#Bronchitis</span>';
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Zen 4.0 Global Hotkeys
|
|
document.addEventListener("keydown", (e) => {
|
|
// Ctrl + Q to toggle queue sidebar
|
|
if (e.ctrlKey && e.key.toLowerCase() === 'q') {
|
|
e.preventDefault();
|
|
const layout = document.querySelector('.zen-layout-3col');
|
|
const pipeline = document.getElementById('zenPipeline');
|
|
if (layout && pipeline) {
|
|
layout.classList.toggle('collapsed-queue');
|
|
pipeline.classList.toggle('collapsed');
|
|
}
|
|
}
|
|
// Ctrl + Shift + S to toggle Scribe
|
|
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 's') {
|
|
e.preventDefault();
|
|
window.toggleZenScribe();
|
|
}
|
|
});
|
|
|
|
// Initialize Recognition
|
|
if ('webkitSpeechRecognition' in window) {
|
|
recognition = new webkitSpeechRecognition();
|
|
recognition.continuous = true;
|
|
recognition.interimResults = true;
|
|
recognition.maxAlternatives = 3;
|
|
|
|
recognition.onresult = (event) => {
|
|
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
|
if (event.results[i].isFinal) {
|
|
const transcript = event.results[i][0].transcript;
|
|
// Default streaming to Subjective section or active field
|
|
const activeField = document.activeElement;
|
|
if (activeField && activeField.tagName === 'TEXTAREA' && activeField.id.startsWith('soap-')) {
|
|
activeField.value += transcript + '. ';
|
|
} else {
|
|
const defaultField = document.getElementById("soap-s");
|
|
if (defaultField) defaultField.value += transcript + '. ';
|
|
}
|
|
}
|
|
}
|
|
};
|
|
recognition.onerror = () => { isScribing = false; updateScribeUI(); };
|
|
}
|
|
});
|
|
|
|
// 3. Section Switching Logic
|
|
window.showSection = (sectionId) => {
|
|
const sections = ['overview-section', 'patients-section', 'pharmacy-section', 'lab-section', 'doctor-zen-section', 'settings-section'];
|
|
sections.forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (el) el.classList.add('hidden');
|
|
});
|
|
|
|
const role = getUserRole();
|
|
if (sectionId === 'settings') {
|
|
const isDoctor = role === 'DOCTOR';
|
|
document.querySelector('#settings-section .doctor-only')?.classList.toggle('hidden', !isDoctor);
|
|
document.querySelector('#settings-section .admin-only')?.classList.toggle('hidden', isDoctor);
|
|
if (typeof loadSettings === 'function') loadSettings();
|
|
}
|
|
|
|
const target = resolveSectionTarget(sectionId);
|
|
const targetEl = document.getElementById(target);
|
|
if (targetEl) targetEl.classList.remove('hidden');
|
|
|
|
if (target === 'doctor-zen-section') {
|
|
document.body.classList.add('zen-mode-active');
|
|
} else {
|
|
document.body.classList.remove('zen-mode-active');
|
|
}
|
|
|
|
if (sectionId === 'queue' && target === 'overview-section') {
|
|
document.getElementById('queue-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
const headerSection = (sectionId === 'queue' && getUserRole() === 'DOCTOR') ? 'zen' : sectionId;
|
|
updateSectionPageHeader(headerSection);
|
|
setActiveNavLink(sectionId);
|
|
|
|
if (window.innerWidth <= 768) {
|
|
const sidebar = document.getElementById('sidebar');
|
|
sidebar?.classList.remove('open');
|
|
document.getElementById('sidebarBackdrop')?.classList.remove('visible');
|
|
document.getElementById('sidebarMenuBtn')?.setAttribute('aria-expanded', 'false');
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// 4. Clinical Context & Scribe Logic
|
|
window.toggleZenScribe = async () => {
|
|
if (!recognition) { alert("Speech Recognition not supported."); return; }
|
|
|
|
isScribing = !isScribing;
|
|
updateScribeUI();
|
|
|
|
if (isScribing) {
|
|
recognition.lang = document.getElementById("scribeLang")?.value || 'en-US';
|
|
recognition.start();
|
|
startDeepCapture();
|
|
} else {
|
|
recognition.stop();
|
|
if (mediaRecorder) mediaRecorder.stop();
|
|
}
|
|
};
|
|
|
|
function updateScribeUI() {
|
|
const btn = document.getElementById("zenScribeBtn");
|
|
const status = document.getElementById("scribeStatus");
|
|
if (!btn || !status) return;
|
|
|
|
if (isScribing) {
|
|
btn.innerHTML = "⏸️ Pause Scribe";
|
|
btn.style.background = "#EF4444";
|
|
btn.style.color = "white";
|
|
status.classList.remove("hidden");
|
|
} else {
|
|
btn.innerHTML = "🔴 Start Recording";
|
|
btn.style.background = ""; // Resets to CSS default
|
|
btn.style.color = "";
|
|
status.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
async function startDeepCapture() {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
mediaRecorder = new MediaRecorder(stream);
|
|
audioChunks = [];
|
|
mediaRecorder.ondataavailable = (e) => audioChunks.push(e.data);
|
|
mediaRecorder.onstop = () => {
|
|
const blob = new Blob(audioChunks, { type: 'audio/wav' });
|
|
processWithWhisper(blob);
|
|
};
|
|
mediaRecorder.start();
|
|
} catch (err) { console.warn("Mic access denied for Deep Scribe"); }
|
|
}
|
|
|
|
async function processWithWhisper(blob) {
|
|
const formData = new FormData();
|
|
formData.append("file", blob, "audio.wav");
|
|
formData.append("model", "tiny"); // Explicitly tell the server which model to use
|
|
|
|
try {
|
|
const res = await fetch("https://curio.applaude.net/api/whisper/v1/audio/transcriptions", {
|
|
method: "POST",
|
|
body: formData
|
|
});
|
|
const data = await res.json();
|
|
if (data.text) {
|
|
const activeField = document.activeElement;
|
|
if (activeField && activeField.tagName === 'TEXTAREA' && activeField.id.startsWith('soap-')) {
|
|
activeField.value += "\n[DEEP SCRIBE]: " + data.text;
|
|
} else {
|
|
const defaultField = document.getElementById("soap-s");
|
|
if (defaultField) defaultField.value += "\n[DEEP SCRIBE]: " + data.text;
|
|
}
|
|
generateSummary(true);
|
|
}
|
|
} catch (e) {
|
|
console.warn("Deep Scribe error, using fallback summary.", e);
|
|
generateSummary(document.getElementById("scribeLang")?.value === 'te-IN');
|
|
}
|
|
}
|
|
|
|
|
|
function generateSummary(isTelugu = false) {
|
|
const soapS = document.getElementById("soap-s")?.value || "";
|
|
const soapO = document.getElementById("soap-o")?.value || "";
|
|
const soapA = document.getElementById("soap-a")?.value || "";
|
|
const soapP = document.getElementById("soap-p")?.value || "";
|
|
|
|
const combinedNotes = [soapS, soapO, soapA, soapP].filter(Boolean).join("\n");
|
|
|
|
const notesEl = document.getElementById("consultationNotes");
|
|
if (notesEl) notesEl.value = combinedNotes;
|
|
|
|
const summary = isTelugu
|
|
? "📝 [TRANSLATED] CLINICAL SUMMARY: Patient presents in Telugu. Dry cough and fatigue noted. Recommended fluids and rest."
|
|
: "📋 CLINICAL SUMMARY: Patient presents with persistent dry cough. Recommended hydration and follow-up.";
|
|
|
|
const summaryTextEl = document.getElementById("summaryText");
|
|
if (summaryTextEl) summaryTextEl.innerText = summary;
|
|
|
|
const summaryCard = document.getElementById("aiSummaryCard");
|
|
if (summaryCard) summaryCard.classList.remove("hidden");
|
|
|
|
if (currentPatientId) {
|
|
patientDataStore[currentPatientId] = {
|
|
notes: combinedNotes,
|
|
soapS,
|
|
soapO,
|
|
soapA,
|
|
soapP,
|
|
summary
|
|
};
|
|
}
|
|
}
|
|
|
|
window.switchPatientContext = (name, token, triage) => {
|
|
if (currentPatientId) {
|
|
const notesVal = document.getElementById("consultationNotes")?.value || "";
|
|
const soapS = document.getElementById("soap-s")?.value || "";
|
|
const soapO = document.getElementById("soap-o")?.value || "";
|
|
const soapA = document.getElementById("soap-a")?.value || "";
|
|
const soapP = document.getElementById("soap-p")?.value || "";
|
|
const summary = document.getElementById("summaryText")?.innerText || "";
|
|
|
|
patientDataStore[currentPatientId] = {
|
|
notes: notesVal,
|
|
soapS,
|
|
soapO,
|
|
soapA,
|
|
soapP,
|
|
summary
|
|
};
|
|
}
|
|
|
|
isScribing = false;
|
|
updateScribeUI();
|
|
currentPatientId = token;
|
|
|
|
const data = patientDataStore[token] || { notes: "", soapS: "", soapO: "", soapA: "", soapP: "", summary: "" };
|
|
|
|
const notesEl = document.getElementById("consultationNotes");
|
|
if (notesEl) notesEl.value = data.notes || "";
|
|
|
|
const sField = document.getElementById("soap-s");
|
|
const oField = document.getElementById("soap-o");
|
|
const aField = document.getElementById("soap-a");
|
|
const pField = document.getElementById("soap-p");
|
|
|
|
if (sField) sField.value = data.soapS || "";
|
|
if (oField) oField.value = data.soapO || "";
|
|
if (aField) aField.value = data.soapA || "";
|
|
if (pField) pField.value = data.soapP || "";
|
|
|
|
const summaryTextEl = document.getElementById("summaryText");
|
|
if (summaryTextEl) summaryTextEl.innerText = data.summary || "";
|
|
|
|
const summaryCard = document.getElementById("aiSummaryCard");
|
|
if (summaryCard) {
|
|
if (data.summary) summaryCard.classList.remove("hidden");
|
|
else summaryCard.classList.add("hidden");
|
|
}
|
|
|
|
const patientHeading = document.querySelector(".patient-identity-row h1");
|
|
if (patientHeading) patientHeading.innerHTML = `${name} <small>(32y, Female)</small>`;
|
|
|
|
document.querySelectorAll('.pipeline-item').forEach(item => {
|
|
const itemToken = item.querySelector('.p-token')?.textContent?.trim();
|
|
item.classList.toggle('next', itemToken === token);
|
|
item.classList.toggle('active', itemToken === token);
|
|
});
|
|
};
|
|
|
|
// 5. Global Helpers & Modal Handlers
|
|
window.openHistory = () => {
|
|
const heading = document.querySelector(".patient-identity-row h1, .patient-identity h1");
|
|
const name = heading ? heading.innerText.split(' (')[0] : 'Patient';
|
|
alert(`📂 Loading History for ${name}...\n\n- Visit (05/12/26): Chronic Gastritis`);
|
|
};
|
|
|
|
window.openConsultation = () => {
|
|
const triagePills = document.querySelectorAll(".patient-identity .meta-pills .pill.time");
|
|
const triage = triagePills[triagePills.length - 1];
|
|
const rxField = document.getElementById("rxDiagnosis");
|
|
if (rxField && triage) rxField.value = triage.innerText;
|
|
if (modal) modal.classList.add('open');
|
|
};
|
|
|
|
window.shareSummary = () => alert("📤 Summary sent to Patient's WhatsApp.");
|
|
window.printSummary = () => alert("🖨️ Printing Handover Note...");
|
|
window.toggleSidebar = () => {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const backdrop = document.getElementById('sidebarBackdrop');
|
|
const menuBtn = document.getElementById('sidebarMenuBtn');
|
|
const isOpen = sidebar?.classList.toggle('open');
|
|
backdrop?.classList.toggle('visible', !!isOpen);
|
|
if (menuBtn) menuBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
};
|
|
|
|
function closeRxModal() {
|
|
if (modal) modal.classList.remove('open');
|
|
}
|
|
|
|
if (addMedBtn && medList) {
|
|
addMedBtn.addEventListener('click', () => {
|
|
const row = medList.querySelector('.med-row');
|
|
if (row) medList.appendChild(row.cloneNode(true));
|
|
});
|
|
}
|
|
|
|
if (addLabBtn && labList) {
|
|
addLabBtn.addEventListener('click', () => {
|
|
const row = labList.querySelector('.lab-row');
|
|
if (row) {
|
|
const clone = row.cloneNode(true);
|
|
const input = clone.querySelector('input');
|
|
if (input) input.value = '';
|
|
labList.appendChild(clone);
|
|
}
|
|
});
|
|
}
|
|
|
|
window.shareTip = (type) => {
|
|
const tips = {
|
|
diabetes: 'Maintain a balanced diet low in refined sugars.',
|
|
hypertension: 'Reduce salt intake and practice daily breathing exercises.',
|
|
viral: 'Stay hydrated and get adequate rest.'
|
|
};
|
|
alert(`📤 Health tip sent: ${tips[type] || 'General wellness advice.'}`);
|
|
};
|
|
|
|
window.printTips = () => alert('🖨️ Printing patient education handout…');
|
|
|
|
// Global Listeners
|
|
if (closeModal) closeModal.onclick = closeRxModal;
|
|
if (cancelRxBtn) cancelRxBtn.onclick = closeRxModal;
|
|
window.onclick = (e) => { if (e.target === modal) closeRxModal(); };
|
|
|
|
// AI Scribe Polling Logic
|
|
const submitAiBtn = document.getElementById("submitAiScribeBtn");
|
|
if (submitAiBtn) {
|
|
submitAiBtn.addEventListener("click", async () => {
|
|
const dictationInput = document.getElementById("aiDictationInput");
|
|
const statusDiv = document.getElementById("aiScribeStatus");
|
|
const statusText = document.getElementById("aiScribeStatusText");
|
|
|
|
if (!dictationInput || !dictationInput.value.trim()) {
|
|
alert("Please enter some dictation first.");
|
|
return;
|
|
}
|
|
|
|
// UI Reset
|
|
submitAiBtn.disabled = true;
|
|
statusDiv.classList.remove("hidden");
|
|
statusText.innerText = "Submitting to CuraFlow AI Engine...";
|
|
|
|
try {
|
|
// Start Job
|
|
const res = await fetch("/api/ai/scribe/start", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ dictation: dictationInput.value })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (!data.success) throw new Error(data.error);
|
|
|
|
const jobId = data.jobId;
|
|
statusText.innerText = "AI Scribe analyzing dictation (this may take a minute)...";
|
|
|
|
// Poll Status
|
|
const pollInterval = setInterval(async () => {
|
|
try {
|
|
const statusRes = await fetch(`/api/ai/scribe/status/${jobId}`);
|
|
const statusData = await statusRes.json();
|
|
|
|
if (statusData.status === 'COMPLETED') {
|
|
clearInterval(pollInterval);
|
|
statusText.innerText = "Analysis Complete! Populating fields...";
|
|
|
|
// Populate Fields from AI Output
|
|
const result = statusData.result;
|
|
if (result && result.clinical_note) {
|
|
const rxDiagnosis = document.getElementById("rxDiagnosis");
|
|
if (rxDiagnosis && result.clinical_note.assessment) {
|
|
rxDiagnosis.value = result.clinical_note.assessment;
|
|
}
|
|
|
|
// Log full output to insights box
|
|
const insightsBox = document.getElementById("aiInsights");
|
|
const insightText = document.getElementById("insightText");
|
|
if (insightsBox && insightText) {
|
|
insightsBox.classList.remove("hidden");
|
|
insightText.innerHTML = `
|
|
<strong>Symptoms:</strong> ${result.clinical_note.chief_complaint?.symptoms?.join(', ')} <br>
|
|
<strong>Billing Codes:</strong> ${result.billing_codes?.icd_10_codes?.map(c => c.code).join(', ') || 'None'} <br>
|
|
<em>(Check console for full JSON structure)</em>
|
|
`;
|
|
console.log("AI Scribe Full Output:", result);
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
statusDiv.classList.add("hidden");
|
|
submitAiBtn.disabled = false;
|
|
}, 2000);
|
|
|
|
} else if (statusData.status === 'FAILED') {
|
|
clearInterval(pollInterval);
|
|
statusText.innerText = "AI Analysis Failed. Please try again.";
|
|
submitAiBtn.disabled = false;
|
|
}
|
|
} catch (err) {
|
|
console.error("Polling error", err);
|
|
}
|
|
}, 5000); // Poll every 5 seconds
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
statusText.innerText = "Error submitting dictation.";
|
|
submitAiBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ==========================================
|
|
// AI Lab Anomaly Watcher Logic
|
|
// ==========================================
|
|
|
|
const startWatcherBtn = document.getElementById("startWatcherBtn");
|
|
const submitLabBtn = document.getElementById("submitLabBtn");
|
|
const labWatcherStatusBadge = document.getElementById("labWatcherStatusBadge");
|
|
const labResultInputSection = document.getElementById("labResultInputSection");
|
|
const aiAlertsContainer = document.getElementById("aiAlertsContainer");
|
|
let activeWatcherPatientId = null;
|
|
let alertPollingInterval = null;
|
|
|
|
if (startWatcherBtn) {
|
|
startWatcherBtn.addEventListener("click", async () => {
|
|
const patientId = document.getElementById("labPatientId").value.trim();
|
|
const baseline = document.getElementById("labBaseline").value.trim();
|
|
|
|
if (!patientId) return alert("Please enter a Patient ID");
|
|
|
|
startWatcherBtn.disabled = true;
|
|
startWatcherBtn.innerText = "Starting...";
|
|
|
|
try {
|
|
const res = await fetch("/api/ai/lab/watch/start", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
patientId: patientId,
|
|
baselineData: {
|
|
baseline: baseline || "Patient is healthy",
|
|
medications: []
|
|
}
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
activeWatcherPatientId = patientId;
|
|
labWatcherStatusBadge.innerText = "Active (Monitoring)";
|
|
labWatcherStatusBadge.classList.add('badge-active');
|
|
|
|
labResultInputSection.classList.remove("hidden");
|
|
startWatcherBtn.innerText = "Watcher Running";
|
|
|
|
// Start polling for alerts
|
|
if (alertPollingInterval) clearInterval(alertPollingInterval);
|
|
alertPollingInterval = setInterval(pollLabAlerts, 5000);
|
|
|
|
} else {
|
|
alert("Failed: " + data.error);
|
|
startWatcherBtn.disabled = false;
|
|
startWatcherBtn.innerText = "Start Watcher";
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Error starting watcher");
|
|
startWatcherBtn.disabled = false;
|
|
startWatcherBtn.innerText = "Start Watcher";
|
|
}
|
|
});
|
|
}
|
|
|
|
if (submitLabBtn) {
|
|
submitLabBtn.addEventListener("click", async () => {
|
|
if (!activeWatcherPatientId) return alert("Start watcher first");
|
|
|
|
const resultInput = document.getElementById("newLabResult");
|
|
const result = resultInput.value.trim();
|
|
if (!result) return alert("Enter a lab result");
|
|
|
|
submitLabBtn.disabled = true;
|
|
submitLabBtn.innerText = "Sending...";
|
|
|
|
try {
|
|
const res = await fetch(`/api/ai/lab/result/${activeWatcherPatientId}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ result })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
resultInput.value = "";
|
|
setTimeout(() => {
|
|
submitLabBtn.innerText = "Send to AI Watcher";
|
|
submitLabBtn.disabled = false;
|
|
}, 1000);
|
|
} else {
|
|
alert("Error: " + data.error);
|
|
submitLabBtn.disabled = false;
|
|
submitLabBtn.innerText = "Send to AI Watcher";
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Failed to submit result");
|
|
submitLabBtn.disabled = false;
|
|
submitLabBtn.innerText = "Send to AI Watcher";
|
|
}
|
|
});
|
|
}
|
|
|
|
async function pollLabAlerts() {
|
|
if (!activeWatcherPatientId) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/ai/lab/alerts/${activeWatcherPatientId}`);
|
|
const data = await res.json();
|
|
|
|
if (data.success && data.alerts && data.alerts.length > 0) {
|
|
renderAlerts(data.alerts);
|
|
}
|
|
} catch (err) {
|
|
console.error("Error polling alerts:", err);
|
|
}
|
|
}
|
|
|
|
function renderAlerts(alerts) {
|
|
if (!aiAlertsContainer) return;
|
|
|
|
let html = `<h3>⚠️ Critical AI Alerts (${alerts.length})</h3>`;
|
|
|
|
alerts.forEach((alert, index) => {
|
|
if (alert.is_dangerous) {
|
|
html += `
|
|
<div class="lab-alert-card">
|
|
<strong>Alert #${index + 1}</strong>
|
|
<p>${alert.danger_explanation}</p>
|
|
<p><strong>Action:</strong> ${alert.recommended_action}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
aiAlertsContainer.innerHTML = html;
|
|
}
|
|
|
|
// ==========================================
|
|
// Zen 4.0 Vitals Sync
|
|
// ==========================================
|
|
window.syncVitals = () => {
|
|
const temp = document.getElementById("vital-temp")?.innerText || "";
|
|
const bp = document.getElementById("vital-bp")?.innerText || "";
|
|
const spo2 = document.getElementById("vital-spo2")?.innerText || "";
|
|
const objectiveField = document.getElementById("soap-o");
|
|
if (objectiveField) {
|
|
objectiveField.value += `\n[Vitals Sync] Temp: ${temp}, BP: ${bp}, SpO2: ${spo2}\n`;
|
|
// Trigger input event to resize textarea or run macros if needed
|
|
objectiveField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
};
|
|
|
|
// ==========================================
|
|
// Settings UI Logic
|
|
// ==========================================
|
|
|
|
async function loadSettings() {
|
|
// 1. Load local preferences (Doctor profile & Scribe prefs)
|
|
const nameEl = document.getElementById("settingsName");
|
|
const specialtyEl = document.getElementById("settingsSpecialty");
|
|
const scribeLangEl = document.getElementById("settingsScribeLang");
|
|
const autoSummaryEl = document.getElementById("settingsAutoSummary");
|
|
|
|
if (nameEl) nameEl.value = localStorage.getItem("userName") || localStorage.getItem("userEmail") || "";
|
|
if (specialtyEl) specialtyEl.value = localStorage.getItem("userSpecialty") || "";
|
|
if (scribeLangEl) scribeLangEl.value = localStorage.getItem("scribeLangPref") || "en-US";
|
|
if (autoSummaryEl) autoSummaryEl.value = localStorage.getItem("autoSummaryPref") || "enabled";
|
|
|
|
// 2. Fetch remote clinic config (Admin config)
|
|
const tenantId = localStorage.getItem("Curio_tenant") || "default";
|
|
try {
|
|
const res = await fetch(`/api/config/${tenantId}`);
|
|
if (res.ok) {
|
|
const config = await res.json();
|
|
const feeEl = document.getElementById("settingsConsultationFee");
|
|
if (feeEl && config.consultationFee) {
|
|
feeEl.value = config.consultationFee;
|
|
}
|
|
if (specialtyEl && !specialtyEl.value && config.specialty) {
|
|
specialtyEl.value = config.specialty;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to load clinic settings from server:", err);
|
|
}
|
|
}
|
|
|
|
window.saveSettings = async () => {
|
|
const btn = document.getElementById("saveSettingsBtn");
|
|
const status = document.getElementById("settingsSaveStatus");
|
|
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.textContent = "Saving...";
|
|
}
|
|
if (status) {
|
|
status.textContent = "";
|
|
status.style.color = "inherit";
|
|
}
|
|
|
|
try {
|
|
// 1. Save local preferences
|
|
const nameVal = document.getElementById("settingsName")?.value.trim();
|
|
const specVal = document.getElementById("settingsSpecialty")?.value.trim();
|
|
const scribeLangVal = document.getElementById("settingsScribeLang")?.value;
|
|
const autoSumVal = document.getElementById("settingsAutoSummary")?.value;
|
|
|
|
if (nameVal) localStorage.setItem("userName", nameVal);
|
|
if (specVal) localStorage.setItem("userSpecialty", specVal);
|
|
if (scribeLangVal) localStorage.setItem("scribeLangPref", scribeLangVal);
|
|
if (autoSumVal) localStorage.setItem("autoSummaryPref", autoSumVal);
|
|
|
|
// 2. Save remote config if user is OWNER
|
|
const role = getUserRole();
|
|
if (role === "OWNER") {
|
|
const feeVal = document.getElementById("settingsConsultationFee")?.value;
|
|
const payload = {};
|
|
if (feeVal) payload.consultationFee = parseInt(feeVal);
|
|
|
|
const token = localStorage.getItem("curio_token");
|
|
if (token) {
|
|
const res = await fetch('/api/clinic/settings', {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.error || "Failed to update clinic settings");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (status) {
|
|
status.textContent = "✅ Settings saved successfully!";
|
|
status.style.color = "#10B981"; // Success green
|
|
}
|
|
|
|
// Apply any real-time UI updates
|
|
const roleDisplay = document.querySelector(".user-profile .info strong");
|
|
if (roleDisplay && nameVal && role === "DOCTOR") {
|
|
roleDisplay.innerText = nameVal;
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error("Error saving settings:", err);
|
|
if (status) {
|
|
status.textContent = "❌ " + err.message;
|
|
status.style.color = "#EF4444"; // Error red
|
|
}
|
|
} finally {
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.textContent = "Save Preferences";
|
|
}
|
|
|
|
// Hide status message after 3 seconds
|
|
setTimeout(() => {
|
|
if (status) status.textContent = "";
|
|
}, 3000);
|
|
}
|
|
};
|
|
// --- Prescription Builder Logic ---
|
|
function addMedicineRow() {
|
|
const container = document.getElementById('medicines-container');
|
|
if (!container) return;
|
|
const row = document.createElement('div');
|
|
row.className = 'medicine-row';
|
|
row.innerHTML = `
|
|
<input type="text" class="form-control med-name" placeholder="Medicine Name">
|
|
<input type="text" class="form-control med-dosage" placeholder="Dosage (e.g. 500mg)">
|
|
<input type="text" class="form-control med-freq" placeholder="Frequency (e.g. 1-0-1)">
|
|
<input type="text" class="form-control med-dur" placeholder="Duration (e.g. 5 days)">
|
|
<button class="btn btn-sm btn-secondary actions-row" onclick="this.parentElement.remove()">❌</button>
|
|
`;
|
|
container.appendChild(row);
|
|
}
|
|
|
|
// Initial setup when opening a patient
|
|
const originalOpenPatient = window.openPatient;
|
|
window.openPatient = function(name, id) {
|
|
if (originalOpenPatient) originalOpenPatient(name, id);
|
|
|
|
// Fill print details
|
|
const printName = document.getElementById('print-patient-name');
|
|
const printId = document.getElementById('print-patient-id');
|
|
const printDate = document.getElementById('print-date');
|
|
if (printName) printName.innerText = name;
|
|
if (printId) printId.innerText = id;
|
|
if (printDate) printDate.innerText = new Date().toLocaleDateString();
|
|
|
|
// Reset medicines
|
|
const medContainer = document.getElementById('medicines-container');
|
|
if (medContainer) {
|
|
medContainer.innerHTML = '';
|
|
addMedicineRow();
|
|
}
|
|
};
|
|
|
|
// If there was no original openPatient (it was inline or something), let's hook into the global DOM elements instead:
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// If medicines container exists, add the first row
|
|
const medContainer = document.getElementById('medicines-container');
|
|
if (medContainer && medContainer.children.length === 0) {
|
|
addMedicineRow();
|
|
}
|
|
});
|
|
|
|
async function submitPrescription() {
|
|
// Attempt to pull patient name from the main dashboard header (if openPatient didn't set it)
|
|
let patientName = document.getElementById('print-patient-name').innerText;
|
|
let patientId = document.getElementById('print-patient-id').innerText;
|
|
|
|
// Fallback: pull from the h1 element in the clinical workspace if it exists
|
|
const titleEl = document.querySelector('.patient-title-bar h1');
|
|
if (titleEl && (!patientName || patientName === 'Rahul Verma')) { // 'Rahul Verma' is the default template
|
|
patientName = titleEl.childNodes[0].textContent.trim();
|
|
patientId = 'N/A'; // We might not have it displayed easily
|
|
}
|
|
|
|
const diagnosis = document.getElementById('soap-a').value;
|
|
|
|
const medicines = [];
|
|
document.querySelectorAll('.medicine-row').forEach(row => {
|
|
const name = row.querySelector('.med-name').value;
|
|
if (name) {
|
|
medicines.push({
|
|
name: name,
|
|
dosage: row.querySelector('.med-dosage').value,
|
|
frequency: row.querySelector('.med-freq').value,
|
|
duration: row.querySelector('.med-dur').value
|
|
});
|
|
}
|
|
});
|
|
|
|
if (!patientName || medicines.length === 0) {
|
|
alert("Please fill in at least one medicine and ensure patient name is loaded.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await apiCall('/api/prescriptions', 'POST', {
|
|
patientId,
|
|
patientName,
|
|
diagnosis,
|
|
medicines,
|
|
labTests: []
|
|
});
|
|
|
|
if (res.success) {
|
|
alert('Prescription created and sent to pharmacy! ID: ' + res.prescriptionId);
|
|
// Optionally print right away
|
|
// window.print();
|
|
} else {
|
|
alert('Failed to create prescription.');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('An error occurred while creating the prescription.');
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// Live Queue & Walk-in UI Logic
|
|
// ==========================================
|
|
|
|
let liveQueuePollingInterval = null;
|
|
|
|
async function fetchLiveQueue() {
|
|
const token = localStorage.getItem("curio_token");
|
|
if (!token) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/clinic/queue', {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (res.ok) {
|
|
const queue = await res.json();
|
|
renderLiveQueueTable(queue);
|
|
renderZenPipeline(queue);
|
|
updateQueueStats(queue);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to fetch live queue:", err);
|
|
}
|
|
}
|
|
|
|
function updateQueueStats(queue) {
|
|
const counterBadge = document.querySelector('.counter-badge');
|
|
if (counterBadge) counterBadge.innerText = `Waiting: ${queue.filter(q => q.status === 'waiting').length}`;
|
|
|
|
const waitingSpan = document.querySelector('.pipeline-header .badge');
|
|
if (waitingSpan) waitingSpan.innerText = `${queue.filter(q => q.status === 'waiting').length} Waiting`;
|
|
}
|
|
|
|
function renderLiveQueueTable(queue) {
|
|
const tbody = document.getElementById("liveQueueTbody");
|
|
if (!tbody) return;
|
|
|
|
if (queue.length === 0) {
|
|
tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: var(--text-muted);">Queue is empty</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = queue.map(visit => {
|
|
const time = new Date(visit.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
const sourceClass = visit.type === 'online' ? 'online' : 'walkin';
|
|
const sourceText = visit.type === 'online' ? 'Online' : 'Walk-in';
|
|
|
|
let statusHtml = '';
|
|
if (visit.status === 'in-room') {
|
|
statusHtml = `<span class="status-pill in-room">In Room</span>`;
|
|
} else if (visit.payment_status === 'unpaid') {
|
|
statusHtml = `<span class="status-pill pending-pay">Unpaid (₹${visit.fee})</span>`;
|
|
} else {
|
|
statusHtml = `<span class="status-pill waiting">Paid (Waiting)</span>`;
|
|
}
|
|
|
|
let actionHtml = '';
|
|
if (visit.status === 'in-room') {
|
|
actionHtml = `<button class="btn-small" onclick="showSection('zen'); switchPatientContext('${visit.patient_name}', '${visit.token_number}', '${visit.triage_category}')">Consult</button>`;
|
|
} else if (visit.payment_status === 'unpaid') {
|
|
actionHtml = `<button class="btn-small secondary" onclick="alert('Collect Pay Flow...')">Collect Pay</button>`;
|
|
} else {
|
|
actionHtml = `<button class="btn-small secondary" onclick="alert('Notifying patient...')">Notify</button>`;
|
|
}
|
|
|
|
return `
|
|
<tr class="${visit.status === 'in-room' ? 'active-row' : ''}">
|
|
<td>${visit.token_number}</td>
|
|
<td><strong>${visit.patient_name}</strong><br><small class="text-muted">${visit.display_id}</small></td>
|
|
<td><span class="source-tag ${sourceClass}">${sourceText}</span></td>
|
|
<td><span class="lang-tag">EN</span></td>
|
|
<td><span class="triage-pill ${visit.triage_category === 'Urgent' ? 'urgent' : 'routine'}">${visit.triage_category || 'Routine'}</span></td>
|
|
<td>${statusHtml}</td>
|
|
<td>${time}</td>
|
|
<td>${actionHtml}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderZenPipeline(queue) {
|
|
const pipelineList = document.getElementById("livePipelineList");
|
|
if (!pipelineList) return;
|
|
|
|
if (queue.length === 0) {
|
|
pipelineList.innerHTML = `<div style="padding: 1rem; color: var(--text-muted); text-align: center; font-size: 0.9rem;">No patients waiting</div>`;
|
|
return;
|
|
}
|
|
|
|
pipelineList.innerHTML = queue.map((visit, index) => {
|
|
const isNext = index === 0 && visit.status !== 'in-room';
|
|
const isActive = visit.status === 'in-room';
|
|
const itemClass = isActive ? 'active' : (isNext ? 'next' : '');
|
|
const triageClass = visit.triage_category === 'Urgent' ? 'urgent' : 'routine';
|
|
|
|
return `
|
|
<div class="pipeline-item ${itemClass}" onclick="switchPatientContext('${visit.patient_name}', '${visit.token_number}', '${visit.triage_category}')">
|
|
<div class="p-token">${visit.token_number}</div>
|
|
<div class="p-info">
|
|
<strong>${visit.patient_name}</strong>
|
|
<span class="triage-pill ${triageClass}">${visit.triage_category || 'Routine'}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Walk-in Modal Handlers
|
|
window.openWalkinModal = () => {
|
|
document.getElementById('walkInModal')?.classList.add('open');
|
|
};
|
|
|
|
window.closeWalkinModal = () => {
|
|
document.getElementById('walkInModal')?.classList.remove('open');
|
|
document.getElementById('walkInForm')?.reset();
|
|
};
|
|
|
|
window.submitWalkIn = async () => {
|
|
const btn = document.getElementById("submitWalkInBtn");
|
|
const name = document.getElementById("walkInName").value.trim();
|
|
const phone = document.getElementById("walkInPhone").value.trim();
|
|
const age = document.getElementById("walkInAge").value;
|
|
const gender = document.getElementById("walkInGender").value;
|
|
const fee = document.getElementById("walkInFee").value;
|
|
|
|
if (!name || !phone) {
|
|
alert("Name and WhatsApp Number are required.");
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.innerText = "Registering...";
|
|
|
|
try {
|
|
const token = localStorage.getItem("curio_token");
|
|
const res = await fetch('/api/clinic/walk-in', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ name, whatsappNumber: phone, age, gender, fee: parseInt(fee) || 0 })
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
closeWalkinModal();
|
|
fetchLiveQueue(); // Instantly refresh
|
|
alert(`Token ${data.token} generated for ${name}!`);
|
|
} else {
|
|
alert("Error: " + data.error);
|
|
}
|
|
} catch (err) {
|
|
console.error("Walk-in error:", err);
|
|
alert("Failed to register walk-in.");
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerText = "Register & Print Token";
|
|
}
|
|
};
|
|
|
|
// Start Polling when DOM loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
fetchLiveQueue();
|
|
liveQueuePollingInterval = setInterval(fetchLiveQueue, 10000); // Poll every 10s
|
|
});
|