Open raw ↗// HAD Digital MVP - Frontend Application
const API = '/api';
let currentUser = null;
let currentView = 'login';
// === API Helpers ===
async function api(method, path, body) {
const opts = { method, headers: {'Content-Type':'application/json'}, credentials:'same-origin' };
if (body) opts.body = JSON.stringify(body);
const r = await fetch(API + path, opts);
if (r.status === 401) { currentUser = null; render(); return null; }
const data = await r.json();
if (!r.ok) throw new Error(data.error || 'Request failed');
return data;
}
// === Toast ===
function toast(msg, type='info') {
const t = document.createElement('div');
t.className = 'toast toast-' + type;
t.textContent = msg;
document.getElementById('toast-container').appendChild(t);
setTimeout(() => t.remove(), 4000);
}
// === Routing ===
function navigate(view, params) {
currentView = view;
window._params = params || {};
render();
}
// === Render ===
function render() {
const app = document.getElementById('app');
if (!currentUser) { app.innerHTML = renderLogin(); bindLogin(); return; }
app.innerHTML = renderShell();
bindShell();
const content = document.getElementById('main-content');
switch(currentView) {
case 'dashboard': content.innerHTML = renderDashboard(); bindDashboard(); break;
case 'report': content.innerHTML = renderReportForm(); bindReportForm(); break;
case 'observation': content.innerHTML = renderObservationForm(); bindObservationForm(); break;
case 'timeline': content.innerHTML = renderTimeline(); bindTimeline(); break;
case 'alerts': content.innerHTML = renderAlerts(); bindAlerts(); break;
case 'treatment': content.innerHTML = renderTreatmentPlan(); bindTreatmentPlan(); break;
case 'messages': content.innerHTML = renderMessages(); bindMessages(); break;
case 'export': content.innerHTML = renderExport(); bindExport(); break;
case 'patients': content.innerHTML = renderPatients(); bindPatients(); break;
case 'patient-detail': content.innerHTML = renderPatientDetail(); bindPatientDetail(); break;
default: content.innerHTML = renderDashboard(); bindDashboard();
}
}
// === Login ===
function renderLogin() {
return '<div class="login-page"><div class="login-card">'
+ '<div class="login-header"><h1>HAD Digital</h1><p>Oncology Home Care Platform</p></div>'
+ '<form id="login-form">'
+ '<div class="form-group"><label>Username</label><input type="text" id="username" autocomplete="username" required></div>'
+ '<div class="form-group"><label>Password</label><input type="password" id="password" autocomplete="current-password" required></div>'
+ '<button type="submit" class="btn btn-primary btn-block">Sign In</button>'
+ '<div id="login-error" class="error-text"></div>'
+ '</form>'
+ '<div class="login-demo"><strong>Demo Credentials:</strong><br>'
+ 'Admin: admin / admin123<br>'
+ 'Oncologist: dr.martin / demo123<br>'
+ 'HAD Nurse: inf.moret / demo123<br>'
+ 'Patient: patient.durand / demo123</div>'
+ '</div></div>';
}
function bindLogin() {
document.getElementById('login-form').onsubmit = async (e) => {
e.preventDefault();
try {
const data = await api('POST', '/login', {
username: document.getElementById('username').value,
password: document.getElementById('password').value
});
if (data && (data.ok || data.user)) { currentUser = data.user || data; navigate('dashboard'); }
else document.getElementById('login-error').textContent = data.error || 'Login failed';
} catch(err) { document.getElementById('login-error').textContent = err.message; }
};
}
// === Shell (Navigation) ===
function renderShell() {
const role = currentUser?.role || 'patient';
const isPatient = role === 'patient' || role === 'caregiver';
const navItems = isPatient
? [{v:'dashboard',i:'■',l:'Home'},{v:'report',i:'✚',l:'Report Symptoms'},{v:'timeline',i:'☰',l:'Timeline'},{v:'treatment',i:'★',l:'Treatment'},{v:'messages',i:'✉',l:'Messages'}]
: [{v:'dashboard',i:'■',l:'Dashboard'},{v:'patients',i:'♟',l:'Patients'},{v:'observation',i:'✚',l:'New Observation'},{v:'alerts',i:'⚠',l:'Alerts'},{v:'timeline',i:'☰',l:'Timeline'},{v:'treatment',i:'★',l:'Treatment'},{v:'messages',i:'✉',l:'Messages'},{v:'export',i:'📄',l:'Export'}];
let nav = navItems.map(n => '<a href="#" class="nav-item'+(currentView===n.v?' active':'')+'" data-view="'+n.v+'">'+n.i+' <span>'+n.l+'</span></a>').join('');
return '<div class="app-shell">'
+ '<header class="app-header"><div class="header-brand"><h1>HAD Digital</h1></div>'
+ '<div class="header-user"><span class="user-role">'+role+'</span> <span class="user-name">'+(currentUser.display_name || currentUser.username || currentUser.user || 'User')+'</span>'
+ ' <button class="btn btn-sm btn-ghost" id="logout-btn">Logout</button></div></header>'
+ '<nav class="app-nav">'+nav+'</nav>'
+ '<main class="app-main"><div id="main-content" class="content-area"></div></main></div>';
}
function bindShell() {
document.querySelectorAll('.nav-item').forEach(a => {
a.onclick = (e) => { e.preventDefault(); navigate(a.dataset.view); };
});
document.getElementById('logout-btn').onclick = async () => {
await api('POST', '/logout');
currentUser = null; navigate('login');
};
}
// === Dashboard ===
function renderDashboard() {
const role = currentUser?.role || 'patient';
const isPatient = role === 'patient' || role === 'caregiver';
if (isPatient) return renderPatientDashboard();
return renderClinicianDashboard();
}
function renderPatientDashboard() {
const name = currentUser?.display_name || currentUser?.username || currentUser?.user || 'Patient';
return '<div class="dashboard">'
+ '<div class="welcome-banner"><h2>Welcome, ' + name + '</h2><p>How are you feeling today?</p></div>'
+ '<div class="quick-actions">'
+ '<button class="action-card action-primary" onclick="navigate(\'report\')"><span class="action-icon">✚</span><span class="action-label">Report Symptoms</span></button>'
+ '<button class="action-card" onclick="navigate(\'timeline\')"><span class="action-icon">☰</span><span class="action-label">View Timeline</span></button>'
+ '<button class="action-card" onclick="navigate(\'treatment\')"><span class="action-icon">★</span><span class="action-label">Treatment Plan</span></button>'
+ '</div>'
+ '<div class="dashboard-grid"><div class="card"><h3>Recent Reports</h3><div id="recent-reports"><p class="text-muted">Loading...</p></div></div>'
+ '<div class="card"><h3>Active Alerts</h3><div id="active-alerts"><p class="text-muted">Loading...</p></div></div></div></div>';
}
function renderClinicianDashboard() {
const name = currentUser?.display_name || currentUser?.username || currentUser?.user || 'Clinician';
return '<div class="dashboard">'
+ '<div class="welcome-banner"><h2>Clinician Dashboard</h2><p>Welcome, ' + name + '</p></div>'
+ '<div class="stats-grid">'
+ '<div class="stat-card"><div class="stat-value" id="stat-patients">-</div><div class="stat-label">Active Patients</div></div>'
+ '<div class="stat-card stat-urgent"><div class="stat-value" id="stat-alerts">-</div><div class="stat-label">Pending Alerts</div></div>'
+ '<div class="stat-card"><div class="stat-value" id="stat-reports">-</div><div class="stat-label">Reports Today</div></div>'
+ '</div>'
+ '<div class="dashboard-grid"><div class="card"><h3>Active Alerts</h3><div id="alert-list"><p class="text-muted">Loading...</p></div></div>'
+ '<div class="card"><h3>Recent Activity</h3><div id="recent-activity"><p class="text-muted">Loading...</p></div></div></div></div>';
}
async function bindDashboard() {
try {
const alerts = await api('GET', '/alerts');
if (alerts && alerts.alerts) {
const alertDiv = document.getElementById('alert-list') || document.getElementById('active-alerts');
if (alertDiv) {
if (alerts.alerts.length === 0) { alertDiv.innerHTML = '<p class="text-muted">No active alerts</p>'; }
else { alertDiv.innerHTML = alerts.alerts.slice(0,10).map(a => '<div class="alert-item alert-'+a.tier+'"><span class="alert-tier">'+a.tier+'</span><span class="alert-text">'+a.message+'</span><span class="alert-time">'+formatDate(a.created_at)+'</span></div>').join(''); }
}
const statAlerts = document.getElementById('stat-alerts');
if (statAlerts) statAlerts.textContent = alerts.alerts.length;
}
} catch(e) { console.error('Dashboard load error:', e); }
}
// === Report Form (Patient Self-Report) ===
function renderReportForm() {
return '<div class="page-header"><h2>Report Symptoms</h2><p>Report any symptoms you are experiencing</p></div>'
+ '<form id="report-form" class="clinical-form">'
+ '<div class="form-section"><h3>Gastrointestinal</h3>'
+ renderSymptomQuestion('nausea', 'Nausea', ['None','Mild','Moderate','Severe'])
+ renderSymptomQuestion('vomiting', 'Vomiting', ['None','1-2 episodes/day','3-5 episodes/day','>6 episodes/day'])
+ renderSymptomQuestion('diarrhea', 'Diarrhea', ['None','2-3 stools/day','4-6 stools/day','>7 stools/day'])
+ '</div>'
+ '<div class="form-section"><h3>Hematologic</h3>'
+ renderSymptomQuestion('fatigue', 'Fatigue', ['None','Mild fatigue','Moderate fatigue','Severe fatigue'])
+ renderSymptomQuestion('bleeding', 'Bleeding/Bruising', ['None','Minor','Moderate','Severe'])
+ renderSymptomQuestion('infection_signs', 'Signs of Infection', ['None','Mild fever','Fever >38.5C','Fever with chills'])
+ '</div>'
+ '<div class="form-section"><h3>Dermatologic</h3>'
+ renderSymptomQuestion('rash', 'Skin Rash', ['None','Mild rash','Moderate rash','Severe rash'])
+ renderSymptomQuestion('skin_changes', 'Skin Changes', ['None','Dryness','Peeling','Ulceration'])
+ '</div>'
+ '<div class="form-section"><h3>Neurologic</h3>'
+ renderSymptomQuestion('numbness', 'Numbness/Tingling', ['None','Mild','Moderate','Severe'])
+ renderSymptomQuestion('weakness', 'Muscle Weakness', ['None','Mild','Moderate','Severe'])
+ '</div>'
+ '<div class="form-section"><h3>Constitutional</h3>'
+ renderSymptomQuestion('fever', 'Fever', ['None','37.5-38C','38-39C','>39C'])
+ renderSymptomQuestion('weight_loss', 'Weight Loss', ['None','<5%','5-10%','>10%'])
+ '</div>'
+ '<div class="form-group"><label>Additional Notes</label><textarea id="report-notes" rows="3" placeholder="Describe any other symptoms..."></textarea></div>'
+ '<button type="submit" class="btn btn-primary btn-lg">Submit Report</button></form>';
}
function renderSymptomQuestion(id, label, options) {
let html = '<div class="symptom-question"><label class="symptom-label">' + label + '</label><div class="symptom-options">';
options.forEach((opt, i) => {
html += '<label class="radio-card"><input type="radio" name="symptom_'+id+'" value="'+i+'"'+(i===0?' checked':'')+'>'
+ '<span class="radio-card-body">'+opt+'</span></label>';
});
html += '</div></div>';
return html;
}
function bindReportForm() {
document.getElementById('report-form').onsubmit = async (e) => {
e.preventDefault();
const symptoms = {};
const form = e.target;
['nausea','vomiting','diarrhea','fatigue','bleeding','infection_signs','rash','skin_changes','numbness','weakness','fever','weight_loss'].forEach(s => {
const val = form.querySelector('input[name="symptom_'+s+'"]:checked');
symptoms[s] = val ? parseInt(val.value) : 0;
});
try {
const result = await api('POST', '/reports', { symptoms, notes: document.getElementById('report-notes').value });
if (result) {
showGradingResult(result);
}
} catch(err) { toast(err.message, 'error'); }
};
}
function showGradingResult(result) {
let gradesHtml = '';
if (result.grades && result.grades.length > 0) {
gradesHtml = '<div class="grading-results"><h3>Assessment Results</h3>';
result.grades.forEach(g => {
const gradeClass = g.grade >= 3 ? 'grade-high' : g.grade >= 2 ? 'grade-medium' : 'grade-low';
gradesHtml += '<div class="grade-item '+gradeClass+'"><span class="grade-term">'+g.term+'</span>'
+ '<span class="grade-value">Grade '+g.grade+'</span>'
+ (g.provisional ? '<span class="grade-provisional">Provisional</span>' : '')
+ '</div>';
});
gradesHtml += '</div>';
}
if (result.guidance) {
gradesHtml += '<div class="guidance-box"><h3>Guidance</h3><p>'+result.guidance+'</p></div>';
}
if (result.alert) {
gradesHtml += '<div class="alert-box alert-'+result.alert.tier+'"><h3>Alert: '+result.alert.tier.toUpperCase()+'</h3><p>'+result.alert.message+'</p></div>';
}
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = '<div class="modal"><div class="modal-header"><h2>Report Submitted</h2><button class="modal-close">×</button></div>'
+ '<div class="modal-body">' + gradesHtml + '</div>'
+ '<div class="modal-footer"><button class="btn btn-primary" onclick="this.closest(\'.modal-overlay\').remove();navigate(\'dashboard\')">Done</button></div></div>';
document.getElementById('modal-root').appendChild(modal);
modal.querySelector('.modal-close').onclick = () => modal.remove();
}
// === Observation Form (Clinician) ===
function renderObservationForm() {
return '<div class="page-header"><h2>Clinician Observation</h2><p>Record clinical findings from home visit</p></div>'
+ '<form id="observation-form" class="clinical-form">'
+ '<div class="form-section"><h3>Patient</h3>'
+ '<div class="form-group"><label>Select Patient</label><select id="obs-patient"><option value="">Loading patients...</option></select></div>'
+ '</div>'
+ '<div class="form-section"><h3>Vital Signs</h3>'
+ '<div class="form-row"><div class="form-group"><label>Temperature (°C)</label><input type="number" id="obs-temp" step="0.1" min="35" max="42" placeholder="36.5"></div>'
+ '<div class="form-group"><label>Blood Pressure</label><input type="text" id="obs-bp" placeholder="120/80"></div>'
+ '<div class="form-group"><label>Heart Rate (bpm)</label><input type="number" id="obs-hr" min="30" max="200" placeholder="72"></div>'
+ '<div class="form-group"><label>SpO2 (%)</label><input type="number" id="obs-spo2" min="50" max="100" placeholder="98"></div></div>'
+ '<div class="form-group"><label>Weight (kg)</label><input type="number" id="obs-weight" step="0.1" min="20" max="200" placeholder="70"></div>'
+ '</div>'
+ '<div class="form-section"><h3>Observed Toxicity Signs</h3>'
+ renderSymptomQuestion('obs_nausea', 'Nausea', ['None','Mild','Moderate','Severe'])
+ renderSymptomQuestion('obs_mucositis', 'Mucositis', ['None','Erythema','Ulceration','Unable to eat'])
+ renderSymptomQuestion('obs_skin', 'Skin Toxicity', ['None','Rash','Desquamation','Necrosis'])
+ '</div>'
+ '<div class="form-group"><label>Clinical Notes</label><textarea id="obs-notes" rows="4" placeholder="Additional clinical observations..."></textarea></div>'
+ '<button type="submit" class="btn btn-primary btn-lg">Submit Observation</button></form>';
}
async function bindObservationForm() {
try {
const data = await api('GET', '/patients');
const sel = document.getElementById('obs-patient');
if (data && data.patients) {
sel.innerHTML = '<option value="">Select patient...</option>' + data.patients.map(p => '<option value="'+p.id+'">'+p.name+'</option>').join('');
}
} catch(e) { console.error(e); }
document.getElementById('observation-form').onsubmit = async (e) => {
e.preventDefault();
try {
const symptoms = {};
['obs_nausea','obs_mucositis','obs_skin'].forEach(s => {
const val = e.target.querySelector('input[name="symptom_'+s+'"]:checked');
symptoms[s.replace('obs_','')] = val ? parseInt(val.value) : 0;
});
await api('POST', '/reports', {
patient_id: document.getElementById('obs-patient').value,
symptoms,
vital_signs: {
temp: document.getElementById('obs-temp').value,
bp: document.getElementById('obs-bp').value,
hr: document.getElementById('obs-hr').value,
spo2: document.getElementById('obs-spo2').value,
weight: document.getElementById('obs-weight').value
},
notes: document.getElementById('obs-notes').value,
type: 'clinician_observation'
});
toast('Observation submitted successfully', 'success');
navigate('dashboard');
} catch(err) { toast(err.message, 'error'); }
};
}
// === Timeline ===
function renderTimeline() {
return '<div class="page-header"><h2>Care Coordination Timeline</h2></div>'
+ '<div class="filter-bar"><select id="timeline-filter"><option value="">All Events</option><option value="report">Reports</option><option value="observation">Observations</option><option value="alert">Alerts</option><option value="grade">Grades</option><option value="message">Messages</option></select></div>'
+ '<div id="timeline-container"><p class="text-muted">Loading timeline...</p></div>';
}
async function bindTimeline() {
try {
const data = await api('GET', '/timeline');
const container = document.getElementById('timeline-container');
if (data && data.events && data.events.length > 0) {
renderTimelineEvents(data.events);
} else {
container.innerHTML = '<p class="text-muted">No events found</p>';
}
const filterEl = document.getElementById('timeline-filter');
if (filterEl) {
filterEl.onchange = async (e) => {
const filter = e.target.value;
try {
const d = await api('GET', '/timeline' + (filter ? '?type=' + filter : ''));
if (d && d.events) renderTimelineEvents(d.events);
} catch(err) { toast(err.message, 'error'); }
};
}
} catch(e) { document.getElementById('timeline-container').innerHTML = '<p class="text-danger">Failed to load timeline</p>'; }
}
function renderTimelineEvents(events) {
const container = document.getElementById('timeline-container');
if (!events || events.length === 0) { container.innerHTML = '<p class="text-muted">No events found</p>'; return; }
container.innerHTML = '<div class="timeline">' + events.map(ev => {
const typeClass = 'event-' + (ev.event_type || ev.type || 'default');
const titleHtml = ev.title ? '<strong>' + ev.title + '</strong><br>' : '';
const bodyText = ev.description || ev.message || '';
return '<div class="timeline-item ' + typeClass + '"><div class="timeline-marker"></div>'
+ '<div class="timeline-content"><div class="timeline-header"><span class="event-type-badge">' + (ev.event_type || ev.type || 'event') + '</span>'
+ '<span class="event-time">' + formatDate(ev.event_date || ev.created_at) + '</span></div>'
+ '<div class="event-body">' + titleHtml + bodyText + '</div></div></div>';
}).join('') + '</div>';
}
// === Alerts ===
function renderAlerts() {
return '<div class="page-header"><h2>Alerts</h2></div>'
+ '<div class="filter-bar"><select id="alert-filter"><option value="">All Alerts</option><option value="routine">Routine</option><option value="urgent">Urgent</option><option value="emergency">Emergency</option></select></div>'
+ '<div id="alert-container"><p class="text-muted">Loading alerts...</p></div>';
}
async function bindAlerts() {
try {
const data = await api('GET', '/alerts');
if (data && data.alerts) {
renderAlertList(data.alerts);
document.getElementById('alert-filter').onchange = async (e) => {
const filter = e.target.value;
try {
const d = await api('GET', '/alerts' + (filter ? '?tier=' + filter : ''));
if (d && d.alerts) renderAlertList(d.alerts);
} catch(err) { toast(err.message, 'error'); }
};
}
} catch(e) { document.getElementById('alert-container').innerHTML = '<p class="text-danger">Failed to load alerts</p>'; }
}
function renderAlertList(alerts) {
const container = document.getElementById('alert-container');
if (alerts.length === 0) { container.innerHTML = '<p class="text-muted">No alerts</p>'; return; }
container.innerHTML = alerts.map(a => {
return '<div class="alert-card alert-'+a.tier+'"><div class="alert-header"><span class="alert-tier-badge">'+a.tier.toUpperCase()+'</span>'
+ '<span class="alert-time">'+formatDate(a.created_at)+'</span></div>'
+ '<div class="alert-body"><p class="alert-message">'+a.message+'</p>'
+ (a.patient_name ? '<p class="alert-patient">Patient: '+a.patient_name+'</p>' : '') + '</div>'
+ '<div class="alert-actions">'
+ (a.status !== 'acknowledged' ? '<button class="btn btn-sm btn-primary" onclick="acknowledgeAlert('+a.id+')">Acknowledge</button>' : '<span class="badge badge-success">Acknowledged</span>')
+ '</div></div>';
}).join('');
}
async function acknowledgeAlert(id) {
try {
await api('POST', '/alerts/' + id + '/acknowledge');
toast('Alert acknowledged', 'success');
navigate('alerts');
} catch(err) { toast(err.message, 'error'); }
}
// === Treatment Plan ===
function renderTreatmentPlan() {
return '<div class="page-header"><h2>Treatment Plan</h2></div>'
+ '<div id="treatment-container"><p class="text-muted">Loading treatment plan...</p></div>';
}
async function bindTreatmentPlan() {
try {
const data = await api('GET', '/treatment-plan');
const container = document.getElementById('treatment-container');
if (data && data.treatment_plan) {
const tp = data.treatment_plan;
let drugsHtml = '';
if (tp.drugs) {
try {
const drugs = typeof tp.drugs === 'string' ? JSON.parse(tp.drugs) : tp.drugs;
if (Array.isArray(drugs)) {
drugsHtml = '<div class="treatment-drugs" style="margin-top:1rem;"><h4>Prescribed Regimen</h4><ul>'
+ drugs.map(d => '<li><strong>' + (d.name || d) + '</strong>: ' + (d.dose || '') + ' ' + (d.schedule || '') + '</li>').join('')
+ '</ul></div>';
}
} catch(e) {}
}
container.innerHTML = '<div class="card"><h3>' + (tp.protocol_name || 'Treatment Protocol') + '</h3>'
+ '<p><strong>Cycle:</strong> ' + (tp.current_cycle || 1) + ' of ' + (tp.total_cycles || 'N/A') + '</p>'
+ '<p><strong>Intent:</strong> ' + (tp.intent || 'Curative') + '</p>'
+ drugsHtml + '</div>';
} else {
container.innerHTML = '<p class="text-muted">No active treatment plan found</p>';
}
} catch(e) {
document.getElementById('treatment-container').innerHTML = '<p class="text-danger">Failed to load treatment plan</p>';
}
}
// === Messages ===
function renderMessages() {
return '<div class="page-header"><h2>Messages</h2></div>'
+ '<div class="messages-layout"><div class="message-list" id="message-list"><p class="text-muted">Loading...</p></div>'
+ '<div class="message-compose"><textarea id="message-text" rows="3" placeholder="Type a message..."></textarea>'
+ '<button class="btn btn-primary" id="send-message-btn">Send</button></div></div>';
}
async function bindMessages() {
try {
const data = await api('GET', '/messages');
const list = document.getElementById('message-list');
if (data && data.messages && data.messages.length > 0) {
list.innerHTML = data.messages.map(m => {
return '<div class="message-item'+(m.sender === currentUser.user ? ' message-self' : '')+'">'
+ '<div class="message-header"><span class="message-sender">'+m.sender+'</span><span class="message-time">'+formatDate(m.created_at)+'</span></div>'
+ '<div class="message-body">'+m.content+'</div></div>';
}).join('');
} else {
list.innerHTML = '<p class="text-muted">No messages yet</p>';
}
document.getElementById('send-message-btn').onclick = async () => {
const text = document.getElementById('message-text').value.trim();
if (!text) return;
try {
await api('POST', '/messages', { content: text });
document.getElementById('message-text').value = '';
toast('Message sent', 'success');
bindMessages();
} catch(err) { toast(err.message, 'error'); }
};
} catch(e) { document.getElementById('message-list').innerHTML = '<p class="text-danger">Failed to load messages</p>'; }
}
// === Export ===
function renderExport() {
return '<div class="page-header"><h2>Toxicity Summary Export</h2><p>Generate a structured summary for clinical records</p></div>'
+ '<div class="export-controls"><div class="form-row">'
+ '<div class="form-group"><label>Start Date</label><input type="date" id="export-start"></div>'
+ '<div class="form-group"><label>End Date</label><input type="date" id="export-end"></div>'
+ '<div class="form-group"><label>Patient</label><select id="export-patient"><option value="">All Patients</option></select></div>'
+ '</div><button class="btn btn-primary" id="export-btn">Generate Summary</button></div>'
+ '<div id="export-preview" class="export-preview"></div>';
}
async function bindExport() {
try {
const data = await api('GET', '/patients');
const sel = document.getElementById('export-patient');
if (data && data.patients) {
sel.innerHTML = '<option value="">All Patients</option>' + data.patients.map(p => '<option value="'+p.id+'">'+p.name+'</option>').join('');
}
} catch(e) { console.error(e); }
const today = new Date().toISOString().split('T')[0];
const weekAgo = new Date(Date.now() - 7*24*60*60*1000).toISOString().split('T')[0];
document.getElementById('export-start').value = weekAgo;
document.getElementById('export-end').value = today;
document.getElementById('export-btn').onclick = async () => {
try {
const params = new URLSearchParams({
start: document.getElementById('export-start').value,
end: document.getElementById('export-end').value,
patient_id: document.getElementById('export-patient').value
});
const data = await api('GET', '/export/summary?' + params);
if (data) {
document.getElementById('export-preview').innerHTML = '<div class="export-result">'
+ '<h3>Summary Generated</h3><pre>'+JSON.stringify(data, null, 2)+'</pre>'
+ '<button class="btn btn-secondary" onclick="downloadExport()">Download JSON</button></div>';
window._exportData = data;
}
} catch(err) { toast(err.message, 'error'); }
};
}
function downloadExport() {
if (!window._exportData) return;
const blob = new Blob([JSON.stringify(window._exportData, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'toxicity_summary.json'; a.click();
URL.revokeObjectURL(url);
}
// === Patients (Clinician) ===
function renderPatients() {
return '<div class="page-header"><h2>Patients</h2></div>'
+ '<div id="patient-list"><p class="text-muted">Loading patients...</p></div>';
}
async function bindPatients() {
try {
const data = await api('GET', '/patients');
const container = document.getElementById('patient-list');
if (data && data.patients && data.patients.length > 0) {
container.innerHTML = '<div class="patient-grid">' + data.patients.map(p => {
return '<div class="patient-card" onclick="navigate(\'patient-detail\', {id:'+p.id+'})">'
+ '<h3>'+p.name+'</h3>'
+ '<p class="patient-protocol">'+(p.treatment_protocol||'N/A')+'</p>'
+ '<p class="patient-cycle">'+(p.current_cycle||'')+'</p>'
+ '</div>';
}).join('') + '</div>';
} else {
container.innerHTML = '<p class="text-muted">No patients found</p>';
}
} catch(e) { document.getElementById('patient-list').innerHTML = '<p class="text-danger">Failed to load patients</p>'; }
}
// === Patient Detail ===
function renderPatientDetail() {
return '<div class="page-header"><button class="btn btn-ghost" onclick="navigate(\'patients\')">← Back</button><h2 id="patient-name">Loading...</h2></div>'
+ '<div id="patient-detail-content"><p class="text-muted">Loading patient details...</p></div>';
}
async function bindPatientDetail() {
const id = window._params?.id;
if (!id) { navigate('patients'); return; }
try {
const data = await api('GET', '/patients/' + id);
if (data) {
document.getElementById('patient-name').textContent = data.name || 'Patient';
const c = document.getElementById('patient-detail-content');
c.innerHTML = '<div class="patient-detail-grid">'
+ '<div class="card"><h3>Treatment</h3><p><strong>Protocol:</strong> '+(data.treatment_protocol||'N/A')+'</p>'
+ '<p><strong>Cycle:</strong> '+(data.current_cycle||'N/A')+'</p></div>'
+ '<div class="card"><h3>Recent Grades</h3><div id="patient-grades"><p class="text-muted">Loading...</p></div></div>'
+ '<div class="card"><h3>Alerts</h3><div id="patient-alerts"><p class="text-muted">Loading...</p></div></div>'
+ '</div>';
// Load grades and alerts
try {
const grades = await api('GET', '/grades?patient_id=' + id);
if (grades && grades.grades) {
document.getElementById('patient-grades').innerHTML = grades.grades.slice(0,5).map(g =>
'<div class="grade-item grade-'+(g.grade>=3?'high':g.grade>=2?'medium':'low')+'"><span>'+g.term+'</span><span>Grade '+g.grade+'</span></div>'
).join('') || '<p class="text-muted">No grades</p>';
}
} catch(e) {}
try {
const alerts = await api('GET', '/alerts?patient_id=' + id);
if (alerts && alerts.alerts) {
document.getElementById('patient-alerts').innerHTML = alerts.alerts.slice(0,5).map(a =>
'<div class="alert-item alert-'+a.tier+'"><span class="alert-tier">'+a.tier+'</span><span>'+a.message+'</span></div>'
).join('') || '<p class="text-muted">No alerts</p>';
}
} catch(e) {}
}
} catch(e) { toast('Failed to load patient', 'error'); }
}
// === Utility ===
function formatDate(d) {
if (!d) return '';
try { const dt = new Date(d); return dt.toLocaleDateString() + ' ' + dt.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}); }
catch(e) { return d; }
}
// === Init ===
async function init() {
try {
const data = await api('GET', '/whoami');
if (data && data.authenticated && data.user) {
currentUser = data.user;
navigate('dashboard');
} else {
navigate('login');
}
} catch(e) { navigate('login'); }
}
init();