314 lines
9.2 KiB
JavaScript
314 lines
9.2 KiB
JavaScript
/**
|
|
* Queue UI Manager
|
|
* Handles UI updates and animations for queue management
|
|
* Part of Phase 11: Advanced Queue Management
|
|
*/
|
|
|
|
class QueueUIManager {
|
|
constructor(options = {}) {
|
|
this.animationDuration = options.animationDuration || 300;
|
|
this.soundEnabled = options.soundEnabled || false;
|
|
this.sounds = {
|
|
patientCalled: options.sounds?.patientCalled || null,
|
|
positionChange: options.sounds?.positionChange || null,
|
|
queueUpdate: options.sounds?.queueUpdate || null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update queue statistics display
|
|
*/
|
|
updateStatistics(stats) {
|
|
this.updateElement('current-size', stats.current_size);
|
|
this.updateElement('avg-wait-time', stats.average_wait_time);
|
|
this.updateElement('load-factor', `${stats.load_factor.toFixed(2)}x`);
|
|
|
|
// Update load bar
|
|
this.updateLoadBar(stats);
|
|
|
|
// Animate changes
|
|
this.animateStatChange('current-size');
|
|
}
|
|
|
|
/**
|
|
* Update load bar visualization
|
|
*/
|
|
updateLoadBar(stats) {
|
|
const loadBar = document.getElementById('load-bar');
|
|
if (!loadBar) return;
|
|
|
|
const utilization = (stats.current_size / stats.max_size) * 100;
|
|
loadBar.style.width = `${utilization}%`;
|
|
|
|
// Update color based on utilization
|
|
loadBar.className = 'load-bar';
|
|
if (utilization < 50) {
|
|
loadBar.classList.add('load-normal');
|
|
} else if (utilization < 75) {
|
|
loadBar.classList.add('load-moderate');
|
|
} else {
|
|
loadBar.classList.add('load-high');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update queue list via HTMX
|
|
*/
|
|
refreshQueueList() {
|
|
const container = document.getElementById('queue-list-container');
|
|
if (container && typeof htmx !== 'undefined') {
|
|
htmx.trigger(container, 'refresh');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show patient called notification
|
|
*/
|
|
showPatientCalled(data) {
|
|
const message = `${data.patient_name} has been called (Position ${data.position})`;
|
|
|
|
// Show toast notification
|
|
if (window.HospitalApp && window.HospitalApp.utils) {
|
|
window.HospitalApp.utils.showToast(message, 'success');
|
|
}
|
|
|
|
// Play sound if enabled
|
|
if (this.soundEnabled && this.sounds.patientCalled) {
|
|
this.playSound(this.sounds.patientCalled);
|
|
}
|
|
|
|
// Highlight the called patient
|
|
this.highlightPatient(data.entry_id);
|
|
}
|
|
|
|
/**
|
|
* Show position change notification
|
|
*/
|
|
showPositionChange(data) {
|
|
const message = `Position changed for ${data.patient_name}: ${data.old_position} → ${data.new_position}`;
|
|
|
|
// Show toast notification
|
|
if (window.HospitalApp && window.HospitalApp.utils) {
|
|
window.HospitalApp.utils.showToast(message, 'info');
|
|
}
|
|
|
|
// Play sound if enabled
|
|
if (this.soundEnabled && this.sounds.positionChange) {
|
|
this.playSound(this.sounds.positionChange);
|
|
}
|
|
|
|
// Animate position change
|
|
this.animatePositionChange(data.entry_id, data.old_position, data.new_position);
|
|
}
|
|
|
|
/**
|
|
* Highlight a patient entry
|
|
*/
|
|
highlightPatient(entryId) {
|
|
const row = document.querySelector(`[data-entry-id="${entryId}"]`);
|
|
if (row) {
|
|
row.classList.add('table-success');
|
|
setTimeout(() => {
|
|
row.classList.remove('table-success');
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Animate position change
|
|
*/
|
|
animatePositionChange(entryId, oldPosition, newPosition) {
|
|
const row = document.querySelector(`[data-entry-id="${entryId}"]`);
|
|
if (!row) return;
|
|
|
|
// Add animation class
|
|
row.style.transition = `all ${this.animationDuration}ms ease`;
|
|
|
|
if (newPosition < oldPosition) {
|
|
// Moving up - flash green
|
|
row.classList.add('bg-success', 'bg-opacity-25');
|
|
} else {
|
|
// Moving down - flash warning
|
|
row.classList.add('bg-warning', 'bg-opacity-25');
|
|
}
|
|
|
|
setTimeout(() => {
|
|
row.classList.remove('bg-success', 'bg-warning', 'bg-opacity-25');
|
|
}, this.animationDuration * 2);
|
|
}
|
|
|
|
/**
|
|
* Animate statistic change
|
|
*/
|
|
animateStatChange(elementId) {
|
|
const element = document.getElementById(elementId);
|
|
if (!element) return;
|
|
|
|
element.classList.add('pulse');
|
|
setTimeout(() => {
|
|
element.classList.remove('pulse');
|
|
}, this.animationDuration);
|
|
}
|
|
|
|
/**
|
|
* Update element text content
|
|
*/
|
|
updateElement(elementId, value) {
|
|
const element = document.getElementById(elementId);
|
|
if (element) {
|
|
element.textContent = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Play sound notification
|
|
*/
|
|
playSound(soundUrl) {
|
|
if (!soundUrl) return;
|
|
|
|
try {
|
|
const audio = new Audio(soundUrl);
|
|
audio.volume = 0.5;
|
|
audio.play().catch(error => {
|
|
console.warn('Could not play sound:', error);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error playing sound:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show loading indicator
|
|
*/
|
|
showLoading(containerId) {
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
|
|
const loader = document.createElement('div');
|
|
loader.className = 'text-center py-4';
|
|
loader.innerHTML = `
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
`;
|
|
loader.id = `${containerId}-loader`;
|
|
|
|
container.appendChild(loader);
|
|
}
|
|
|
|
/**
|
|
* Hide loading indicator
|
|
*/
|
|
hideLoading(containerId) {
|
|
const loader = document.getElementById(`${containerId}-loader`);
|
|
if (loader) {
|
|
loader.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update WebSocket status indicator
|
|
*/
|
|
updateWSStatus(status) {
|
|
const statusEl = document.getElementById('ws-status');
|
|
const statusTextEl = document.getElementById('ws-status-text');
|
|
|
|
if (!statusEl || !statusTextEl) return;
|
|
|
|
statusEl.className = 'ws-status';
|
|
|
|
switch (status) {
|
|
case 'connected':
|
|
statusEl.classList.add('ws-connected');
|
|
statusTextEl.textContent = 'Connected';
|
|
break;
|
|
case 'connecting':
|
|
statusEl.classList.add('ws-connecting');
|
|
statusTextEl.textContent = 'Connecting...';
|
|
break;
|
|
case 'disconnected':
|
|
statusEl.classList.add('ws-disconnected');
|
|
statusTextEl.textContent = 'Disconnected';
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format time duration
|
|
*/
|
|
formatDuration(minutes) {
|
|
if (minutes < 60) {
|
|
return `${minutes}min`;
|
|
}
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
return `${hours}h ${mins}min`;
|
|
}
|
|
|
|
/**
|
|
* Create patient card element
|
|
*/
|
|
createPatientCard(entry) {
|
|
const card = document.createElement('div');
|
|
card.className = 'patient-card mb-2';
|
|
card.dataset.entryId = entry.id;
|
|
|
|
card.innerHTML = `
|
|
<div class="d-flex align-items-center p-3 border rounded">
|
|
<div class="queue-position me-3">${entry.queue_position}</div>
|
|
<div class="flex-fill">
|
|
<div class="fw-bold">${entry.patient_name}</div>
|
|
<small class="text-muted">Wait: ${this.formatDuration(entry.wait_time_minutes)}</small>
|
|
</div>
|
|
<div>
|
|
<span class="badge bg-${this.getPriorityColor(entry.priority_score)}">
|
|
Priority: ${entry.priority_score.toFixed(1)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return card;
|
|
}
|
|
|
|
/**
|
|
* Get priority color based on score
|
|
*/
|
|
getPriorityColor(score) {
|
|
if (score >= 8) return 'danger';
|
|
if (score >= 5) return 'warning';
|
|
return 'success';
|
|
}
|
|
|
|
/**
|
|
* Scroll to element smoothly
|
|
*/
|
|
scrollToElement(elementId) {
|
|
const element = document.getElementById(elementId);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable/disable sound notifications
|
|
*/
|
|
toggleSound(enabled) {
|
|
this.soundEnabled = enabled;
|
|
localStorage.setItem('queueSoundEnabled', enabled);
|
|
}
|
|
|
|
/**
|
|
* Get sound preference from storage
|
|
*/
|
|
getSoundPreference() {
|
|
const stored = localStorage.getItem('queueSoundEnabled');
|
|
return stored !== null ? stored === 'true' : this.soundEnabled;
|
|
}
|
|
}
|
|
|
|
// Export for use in other modules
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = QueueUIManager;
|
|
}
|