agdar/static/js/queue_ui.js
2025-11-02 14:35:35 +03:00

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;
}