// static/dispatcher/script.js - TO'LIQ TAYYOR VERSIYA // Brigade Tracking + Statistics + Clinic Map // ==================== GLOBAL STATE ==================== let ws = null; let reconnectInterval = null; let currentFilter = null; let clinicMap = null; let clinicMarkers = []; let markerClusterGroup = null; let currentMapLayer = 'all'; let brigadeMarkers = {}; let brigadeUpdateInterval = null; let casesHourlyChart = null; let riskDistributionChart = null; // ==================== INITIALIZATION ==================== document.addEventListener('DOMContentLoaded', function () { console.log('πŸš€ Dispatcher panel ishga tushdi'); loadCases(); loadStatistics(); connectWebSocket(); initializeMap(); startBrigadeTracking(); initializeCharts(); setInterval(() => { loadCases(); loadStatistics(); updateStatisticsCharts(); updateBrigadeStatistics(); }, 30000); }); // ==================== WEBSOCKET ==================== function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/ws/dispatcher`; console.log('πŸ”Œ WebSocket ulanish...', wsUrl); ws = new WebSocket(wsUrl); ws.onopen = function () { console.log('βœ… WebSocket ulandi'); if (reconnectInterval) { clearInterval(reconnectInterval); reconnectInterval = null; } }; ws.onmessage = function (event) { try { const data = JSON.parse(event.data); console.log('πŸ“¨ WebSocket xabar:', data); handleWebSocketMessage(data); } catch (e) { console.error('❌ WebSocket xabar parse qilishda xatolik:', e); } }; ws.onerror = function (error) { console.error('❌ WebSocket xatolik:', error); }; ws.onclose = function () { console.log('πŸ”Œ WebSocket uzildi, qayta ulanish...'); if (!reconnectInterval) { reconnectInterval = setInterval(() => { connectWebSocket(); }, 5000); } }; } function handleWebSocketMessage(data) { const type = data.type; switch (type) { case 'new_case': console.log('πŸ†• Yangi case:', data.case); showNotification('Yangi murojat keldi!', 'bg-danger'); loadCases(); loadStatistics(); break; case 'brigade_assigned': console.log('πŸš‘ Brigada tayinlandi:', data.case); showNotification('Brigada tayinlandi', 'bg-success'); loadCases(); break; case 'name_received': console.log('πŸ‘€ Ism qabul qilindi:', data.case); loadCases(); break; case 'operator_needed': console.log('🎧 Operator kerak:', data.case); showNotification('Operator kerak!', 'bg-warning'); loadCases(); break; case 'clinic_recommended': console.log('πŸ₯ Klinika tavsiya qilindi:', data.case); showNotification('Klinikaga yo\'naltirildi', 'bg-info'); loadCases(); break; default: console.log('ℹ️ Noma\'lum xabar turi:', type); } } // ==================== CASES MANAGEMENT ==================== async function loadCases() { try { const url = currentFilter ? `/api/cases?status=${currentFilter}` : '/api/cases'; const response = await fetch(url); if (!response.ok) { throw new Error('Cases yuklanmadi'); } const cases = await response.json(); console.log(`πŸ“‹ ${cases.length} ta case yuklandi`); renderCases(cases); } catch (error) { console.error('❌ Cases yuklashda xatolik:', error); document.getElementById('cases-container').innerHTML = `
Xatolik yuz berdi. Iltimos, sahifani yangilang.
`; } } function renderCases(cases) { const container = document.getElementById('cases-container'); if (cases.length === 0) { container.innerHTML = `

Hozircha murojatlar yo'q

`; return; } container.innerHTML = cases.map(c => renderCase(c)).join(''); } function renderCase(c) { const riskBadge = getRiskBadge(c.risk_level); const typeBadge = getTypeBadge(c.type); const statusBadge = getStatusBadge(c.status); const timeAgo = getTimeAgo(c.created_at); return `
${c.patient_full_name || 'Bemor #' + c.id}
${timeAgo}
${riskBadge}
${c.symptoms_text ? `

${c.symptoms_text.substring(0, 80)}${c.symptoms_text.length > 80 ? '...' : ''}

` : ''}
${typeBadge} ${statusBadge} ${c.district ? ` ${c.district} ` : ''}
${c.assigned_brigade_name ? ` ${c.assigned_brigade_name} ` : ''} ${c.recommended_clinic_name ? ` ${c.recommended_clinic_name} ` : ''}
`; } function getRiskBadge(risk) { if (risk === 'qizil') { return 'πŸ”΄ QIZIL'; } else if (risk === 'sariq') { return '🟑 SARIQ'; } else if (risk === 'yashil') { return '🟒 YASHIL'; } else { return 'βšͺ Noma\'lum'; } } function getTypeBadge(type) { if (type === 'emergency') { return 'πŸš‘ Tez yordam'; } else if (type === 'public_clinic') { return 'πŸ₯ Davlat'; } else if (type === 'private_clinic') { return 'πŸ₯ Xususiy'; } else if (type === 'uncertain') { return '❓ Noaniq'; } else { return ''; } } function getStatusBadge(status) { const statusMap = { 'yangi': 'Yangi', 'qabul_qilindi': 'Qabul qilindi', 'brigada_junatildi': 'Brigada junatildi', 'klinika_tavsiya_qilindi': 'Klinika tavsiya', 'operator_kutilmoqda': 'Operator kerak', 'yopildi': 'Yopildi' }; return statusMap[status] || 'Noma\'lum'; } function getTimeAgo(timestamp) { const now = new Date(); const created = new Date(timestamp); const diffMs = now - created; const diffMins = Math.floor(diffMs / 60000); if (diffMins < 1) return 'Hozir'; if (diffMins < 60) return `${diffMins} daqiqa oldin`; const diffHours = Math.floor(diffMins / 60); if (diffHours < 24) return `${diffHours} soat oldin`; const diffDays = Math.floor(diffHours / 24); return `${diffDays} kun oldin`; } function filterCases(risk) { currentFilter = risk; document.querySelectorAll('.btn-group button').forEach(btn => { btn.classList.remove('active'); }); event.target.classList.add('active'); loadCases(); } function viewCaseDetails(caseId) { console.log('View case details:', caseId); alert(`Case details: ${caseId}\n(Bu funksiya keyinroq qo'shiladi)`); } // ==================== STATISTICS ==================== async function loadStatistics() { try { const response = await fetch('/api/cases'); if (!response.ok) { throw new Error('Statistics yuklanmadi'); } const cases = await response.json(); const emergency = cases.filter(c => c.type === 'emergency').length; const uncertain = cases.filter(c => c.type === 'uncertain').length; const clinic = cases.filter(c => c.type === 'public_clinic' || c.type === 'private_clinic').length; const total = cases.length; document.getElementById('stat-emergency').textContent = emergency; document.getElementById('stat-uncertain').textContent = uncertain; document.getElementById('stat-clinic').textContent = clinic; document.getElementById('stat-total').textContent = total; } catch (error) { console.error('❌ Statistics yuklashda xatolik:', error); } } // ==================== NOTIFICATIONS ==================== function showNotification(message, bgClass = 'bg-info') { const toastHtml = ` `; const toastElement = document.createElement('div'); toastElement.innerHTML = toastHtml; document.body.appendChild(toastElement); const toast = new bootstrap.Toast(toastElement.firstElementChild, { delay: 3000 }); toast.show(); toastElement.firstElementChild.addEventListener('hidden.bs.toast', () => { document.body.removeChild(toastElement); }); } // ==================== MAP FUNCTIONS ==================== function initializeMap() { console.log('πŸ—ΊοΈ Xarita ishga tushmoqda...'); clinicMap = L.map('clinic-map', { zoomControl: true, scrollWheelZoom: true }).setView([41.2995, 69.2401], 11); L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OSM | CartoDB', maxZoom: 19, subdomains: 'abcd' }).addTo(clinicMap); markerClusterGroup = L.markerClusterGroup({ maxClusterRadius: 50, spiderfyOnMaxZoom: true, showCoverageOnHover: false, zoomToBoundsOnClick: true, iconCreateFunction: function (cluster) { const count = cluster.getChildCount(); let size = 'small'; if (count > 10) size = 'large'; else if (count > 5) size = 'medium'; return L.divIcon({ html: `
${count}
`, className: 'marker-cluster-wrapper', iconSize: L.point(40, 40) }); } }); clinicMap.addLayer(markerClusterGroup); loadClinicsOnMap(); console.log('βœ… Xarita tayyor'); } async function loadClinicsOnMap() { try { console.log('πŸ“ Klinikalarni yuklanmoqda...'); const response = await fetch('/api/clinics'); if (!response.ok) { throw new Error('Klinikalar yuklanmadi'); } const clinics = await response.json(); console.log(`πŸ“‹ ${clinics.length} ta klinika topildi`); if (markerClusterGroup) { markerClusterGroup.clearLayers(); } clinicMarkers = []; clinics.forEach(clinic => { addClinicMarker(clinic); }); } catch (error) { console.error('❌ Klinikalarni yuklashda xatolik:', error); } } function addClinicMarker(clinic) { const lat = clinic.gps?.lat; const lon = clinic.gps?.lon; if (!lat || !lon) { console.warn(`⚠️ GPS yo'q: ${clinic.name}`); return; } const markerColor = clinic.type === 'davlat' ? '#3498db' : '#27ae60'; const iconClass = clinic.type === 'davlat' ? 'bi-hospital' : 'bi-hospital-fill'; const markerIcon = L.divIcon({ className: 'custom-clinic-marker', html: `
`, iconSize: [36, 36], iconAnchor: [18, 36], popupAnchor: [0, -36] }); const marker = L.marker([lat, lon], { icon: markerIcon }); const typeLabel = clinic.type === 'davlat' ? 'Davlat' : 'Xususiy'; const popupContent = `
${clinic.name}
${typeLabel}

${clinic.district}

${clinic.phone}

${clinic.rating}/5.0 (${clinic.doctors_count} doktor)

`; marker.bindPopup(popupContent, { maxWidth: 250, className: 'custom-popup' }); marker.clinicType = clinic.type; clinicMarkers.push(marker); if (markerClusterGroup) { markerClusterGroup.addLayer(marker); } } function toggleMapLayer(layer) { console.log(`πŸ”„ Layer o'zgartirildi: ${layer}`); currentMapLayer = layer; document.querySelectorAll('#btn-all, #btn-davlat, #btn-xususiy').forEach(btn => { btn.classList.remove('active'); }); document.getElementById(`btn-${layer}`).classList.add('active'); if (markerClusterGroup) { markerClusterGroup.clearLayers(); } clinicMarkers.forEach(marker => { if (layer === 'all') { markerClusterGroup.addLayer(marker); } else if (layer === 'davlat' && marker.clinicType === 'davlat') { markerClusterGroup.addLayer(marker); } else if (layer === 'xususiy' && marker.clinicType === 'xususiy') { markerClusterGroup.addLayer(marker); } }); } async function showClinicDetails(clinicId) { try { console.log(`πŸ“‹ Klinika ma'lumotlari yuklanmoqda: ${clinicId}`); const response = await fetch(`/api/clinics/${clinicId}`); if (!response.ok) { throw new Error('Klinika topilmadi'); } const clinic = await response.json(); document.getElementById('clinicModalTitle').innerHTML = ` ${clinic.name} `; const modalBody = document.getElementById('clinicModalBody'); modalBody.innerHTML = ` ${clinic.banner_url ? ` ${clinic.name} ` : ''}

Turi: ${clinic.type === 'davlat' ? 'Davlat' : 'Xususiy'}

Tuman: ${clinic.district}

Manzil: ${clinic.address}

Telefon: ${clinic.phone}

Reyting: ${'β˜…'.repeat(Math.floor(clinic.rating))}${'β˜†'.repeat(5 - Math.floor(clinic.rating))} ${clinic.rating}/5.0

Ish vaqti: ${clinic.working_hours}

Ish kunlari: ${clinic.working_days.join(', ')}

Doktorlar: ${clinic.doctors_count} ta

${clinic.description ? `
${clinic.description}
` : ''}
Mutaxassisliklar
${clinic.specializations.map(spec => ` ${spec} `).join('')}
${clinic.services && clinic.services.length > 0 ? `
Xizmatlar
` : ''} ${clinic.doctors && clinic.doctors.length > 0 ? `
Doktorlar (${clinic.doctors.length})
${clinic.doctors.slice(0, 6).map(doctor => `
${doctor.full_name}
${doctor.full_name}

${doctor.specialty}

β˜… ${doctor.rating}

`).join('')}
` : ''} ${clinic.photos && clinic.photos.length > 0 ? `
Fotogalereya
${clinic.photos.map(photo => `
${photo.caption} ${photo.caption ? `

${photo.caption}

` : ''}
`).join('')}
` : ''} `; const modal = new bootstrap.Modal(document.getElementById('clinicModal')); modal.show(); } catch (error) { console.error('❌ Klinika ma\'lumotlarini yuklashda xatolik:', error); alert('Klinika ma\'lumotlarini yuklashda xatolik yuz berdi'); } } // ==================== BRIGADE TRACKING ==================== function startBrigadeTracking() { console.log('πŸš‘ Brigade tracking boshlandi'); updateBrigadeMarkers(); brigadeUpdateInterval = setInterval(() => { updateBrigadeMarkers(); }, 3000); } async function updateBrigadeMarkers() { try { const response = await fetch('/api/brigades/live'); if (!response.ok) { throw new Error('Brigadalar yuklanmadi'); } const brigades = await response.json(); brigades.forEach(brigade => { updateBrigadeMarker(brigade); }); } catch (error) { console.error('❌ Brigade tracking xatolik:', error); } } function updateBrigadeMarker(brigade) { const brigadeId = brigade.brigade_id; const currentLat = brigade.current_lat; const currentLon = brigade.current_lon; if (!currentLat || !currentLon) return; if (brigadeMarkers[brigadeId]) { const marker = brigadeMarkers[brigadeId]; marker.setLatLng([currentLat, currentLon]); marker.setPopupContent(getBrigadePopupContent(brigade)); } else { const marker = createBrigadeMarker(brigade); brigadeMarkers[brigadeId] = marker; marker.addTo(clinicMap); } } function createBrigadeMarker(brigade) { const currentLat = brigade.current_lat; const currentLon = brigade.current_lon; const status = brigade.current_status; const color = status === 'busy' ? '#dc3545' : '#28a745'; const icon = status === 'busy' ? 'πŸš‘' : '🟒'; const markerIcon = L.divIcon({ className: 'brigade-marker', html: `
${icon}
`, iconSize: [40, 40], iconAnchor: [20, 20] }); const marker = L.marker([currentLat, currentLon], { icon: markerIcon, zIndexOffset: 1000 }); marker.bindPopup(getBrigadePopupContent(brigade)); return marker; } function getBrigadePopupContent(brigade) { const statusLabel = brigade.current_status === 'busy' ? 'Bandlik' : 'Bo\'sh'; return `
${brigade.name}

Status: ${statusLabel}

${brigade.phone}

${brigade.speed_kmh} km/h

${brigade.assigned_case_id ? `

Case: ${brigade.assigned_case_id}

` : ''}
`; } // ==================== STATISTICS CHARTS ==================== function initializeCharts() { console.log('πŸ“Š Grafiklar ishga tushmoqda...'); const ctxHourly = document.getElementById('casesHourlyChart'); if (ctxHourly) { casesHourlyChart = new Chart(ctxHourly, { type: 'line', data: { labels: ['00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00'], datasets: [{ label: 'Murojatlar', data: [5, 3, 7, 12, 18, 15, 10, 8], borderColor: '#667eea', backgroundColor: 'rgba(102, 126, 234, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 5 } } } } }); } const ctxRisk = document.getElementById('riskDistributionChart'); if (ctxRisk) { riskDistributionChart = new Chart(ctxRisk, { type: 'doughnut', data: { labels: ['Qizil', 'Sariq', 'Yashil'], datasets: [{ data: [0, 0, 0], backgroundColor: [ '#dc3545', '#ffc107', '#28a745' ], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } } }); } updateStatisticsCharts(); } async function updateStatisticsCharts() { try { const response = await fetch('/api/cases'); if (!response.ok) return; const cases = await response.json(); const qizil = cases.filter(c => c.risk_level === 'qizil').length; const sariq = cases.filter(c => c.risk_level === 'sariq').length; const yashil = cases.filter(c => c.risk_level === 'yashil').length; if (riskDistributionChart) { riskDistributionChart.data.datasets[0].data = [qizil, sariq, yashil]; riskDistributionChart.update(); } await updateBrigadeStatistics(); } catch (error) { console.error('❌ Statistika yangilashda xatolik:', error); } } async function updateBrigadeStatistics() { try { const response = await fetch('/api/brigades/live'); if (!response.ok) return; const brigades = await response.json(); const busy = brigades.filter(b => b.current_status === 'busy').length; const available = brigades.filter(b => b.current_status === 'available').length; const total = brigades.length; document.getElementById('active-brigades-count').textContent = total; document.getElementById('busy-brigades').textContent = busy; document.getElementById('available-brigades').textContent = available; } catch (error) { console.error('❌ Brigade statistika xatolik:', error); } }