Spaces:
Paused
Paused
| // 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 = ` | |
| <div class="alert alert-danger m-3"> | |
| <i class="bi bi-exclamation-triangle-fill"></i> | |
| Xatolik yuz berdi. Iltimos, sahifani yangilang. | |
| </div> | |
| `; | |
| } | |
| } | |
| function renderCases(cases) { | |
| const container = document.getElementById('cases-container'); | |
| if (cases.length === 0) { | |
| container.innerHTML = ` | |
| <div class="text-center py-5"> | |
| <i class="bi bi-inbox" style="font-size: 4rem; color: #cbd5e0;"></i> | |
| <p class="mt-3 text-muted">Hozircha murojatlar yo'q</p> | |
| </div> | |
| `; | |
| 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 ` | |
| <div class="case-card" onclick="viewCaseDetails('${c.id}')"> | |
| <div class="d-flex justify-content-between align-items-start mb-2"> | |
| <div class="flex-grow-1"> | |
| <h6 class="mb-1"> | |
| <i class="bi bi-person-circle me-1 text-primary"></i> | |
| ${c.patient_full_name || 'Bemor #' + c.id} | |
| </h6> | |
| <small class="text-muted"> | |
| <i class="bi bi-clock me-1"></i>${timeAgo} | |
| </small> | |
| </div> | |
| <div class="text-end"> | |
| ${riskBadge} | |
| </div> | |
| </div> | |
| ${c.symptoms_text ? ` | |
| <p class="mb-2 small text-muted"> | |
| <i class="bi bi-file-medical me-1"></i> | |
| ${c.symptoms_text.substring(0, 80)}${c.symptoms_text.length > 80 ? '...' : ''} | |
| </p> | |
| ` : ''} | |
| <div class="d-flex justify-content-between align-items-center flex-wrap gap-2"> | |
| <div class="d-flex gap-1 flex-wrap"> | |
| ${typeBadge} | |
| ${statusBadge} | |
| ${c.district ? ` | |
| <span class="badge bg-secondary"> | |
| <i class="bi bi-geo-alt-fill"></i> ${c.district} | |
| </span> | |
| ` : ''} | |
| </div> | |
| <div class="d-flex gap-1 flex-wrap"> | |
| ${c.assigned_brigade_name ? ` | |
| <span class="badge bg-primary"> | |
| <i class="bi bi-ambulance"></i> ${c.assigned_brigade_name} | |
| </span> | |
| ` : ''} | |
| ${c.recommended_clinic_name ? ` | |
| <span class="badge bg-success"> | |
| <i class="bi bi-hospital"></i> ${c.recommended_clinic_name} | |
| </span> | |
| ` : ''} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function getRiskBadge(risk) { | |
| if (risk === 'qizil') { | |
| return '<span class="badge bg-danger badge-risk-qizil">π΄ QIZIL</span>'; | |
| } else if (risk === 'sariq') { | |
| return '<span class="badge bg-warning text-dark badge-risk-sariq">π‘ SARIQ</span>'; | |
| } else if (risk === 'yashil') { | |
| return '<span class="badge bg-success badge-risk-yashil">π’ YASHIL</span>'; | |
| } else { | |
| return '<span class="badge bg-secondary">βͺ Noma\'lum</span>'; | |
| } | |
| } | |
| function getTypeBadge(type) { | |
| if (type === 'emergency') { | |
| return '<span class="badge bg-danger">π Tez yordam</span>'; | |
| } else if (type === 'public_clinic') { | |
| return '<span class="badge bg-info">π₯ Davlat</span>'; | |
| } else if (type === 'private_clinic') { | |
| return '<span class="badge bg-success">π₯ Xususiy</span>'; | |
| } else if (type === 'uncertain') { | |
| return '<span class="badge bg-warning text-dark">β Noaniq</span>'; | |
| } else { | |
| return ''; | |
| } | |
| } | |
| function getStatusBadge(status) { | |
| const statusMap = { | |
| 'yangi': '<span class="badge bg-primary">Yangi</span>', | |
| 'qabul_qilindi': '<span class="badge bg-info">Qabul qilindi</span>', | |
| 'brigada_junatildi': '<span class="badge bg-warning text-dark">Brigada junatildi</span>', | |
| 'klinika_tavsiya_qilindi': '<span class="badge bg-success">Klinika tavsiya</span>', | |
| 'operator_kutilmoqda': '<span class="badge bg-danger">Operator kerak</span>', | |
| 'yopildi': '<span class="badge bg-secondary">Yopildi</span>' | |
| }; | |
| return statusMap[status] || '<span class="badge bg-secondary">Noma\'lum</span>'; | |
| } | |
| 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 = ` | |
| <div class="toast align-items-center ${bgClass} text-white border-0" role="alert" | |
| style="position: fixed; top: 80px; right: 20px; z-index: 9999;"> | |
| <div class="d-flex"> | |
| <div class="toast-body"> | |
| ${message} | |
| </div> | |
| <button type="button" class="btn-close btn-close-white me-2 m-auto" | |
| data-bs-dismiss="toast"></button> | |
| </div> | |
| </div> | |
| `; | |
| 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: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> | 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: `<div class="marker-cluster-custom marker-cluster-${size}"> | |
| <span>${count}</span> | |
| </div>`, | |
| 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: `<div class="clinic-marker-icon" style=" | |
| background: ${markerColor}; | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50% 50% 50% 0; | |
| border: 3px solid white; | |
| box-shadow: 0 3px 10px rgba(0,0,0,0.3); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 16px; | |
| transform: rotate(-45deg); | |
| position: relative; | |
| "> | |
| <i class="bi ${iconClass}" style="transform: rotate(45deg);"></i> | |
| </div>`, | |
| iconSize: [36, 36], | |
| iconAnchor: [18, 36], | |
| popupAnchor: [0, -36] | |
| }); | |
| const marker = L.marker([lat, lon], { icon: markerIcon }); | |
| const typeLabel = clinic.type === 'davlat' ? | |
| '<span class="badge bg-info">Davlat</span>' : | |
| '<span class="badge bg-success">Xususiy</span>'; | |
| const popupContent = ` | |
| <div class="clinic-popup"> | |
| <div class="d-flex justify-content-between align-items-start mb-2"> | |
| <h6 class="mb-0 flex-grow-1">${clinic.name}</h6> | |
| ${typeLabel} | |
| </div> | |
| <hr class="my-2"> | |
| <p class="mb-1 small text-muted"> | |
| <i class="bi bi-geo-alt-fill text-danger me-1"></i> | |
| ${clinic.district} | |
| </p> | |
| <p class="mb-1 small text-muted"> | |
| <i class="bi bi-telephone-fill text-primary me-1"></i> | |
| ${clinic.phone} | |
| </p> | |
| <p class="mb-2 small"> | |
| <i class="bi bi-star-fill text-warning me-1"></i> | |
| <strong>${clinic.rating}</strong>/5.0 | |
| <span class="text-muted">(${clinic.doctors_count} doktor)</span> | |
| </p> | |
| <button class="btn btn-sm btn-primary w-100" onclick="showClinicDetails('${clinic.id}')"> | |
| <i class="bi bi-info-circle me-1"></i> Batafsil | |
| </button> | |
| </div> | |
| `; | |
| 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 = ` | |
| <i class="bi bi-hospital-fill me-2"></i>${clinic.name} | |
| `; | |
| const modalBody = document.getElementById('clinicModalBody'); | |
| modalBody.innerHTML = ` | |
| ${clinic.banner_url ? ` | |
| <img src="${clinic.banner_url}" class="img-fluid rounded mb-3" alt="${clinic.name}"> | |
| ` : ''} | |
| <div class="row mb-3"> | |
| <div class="col-md-6"> | |
| <p class="mb-2"> | |
| <strong>Turi:</strong> | |
| <span class="badge ${clinic.type === 'davlat' ? 'bg-info' : 'bg-success'}"> | |
| ${clinic.type === 'davlat' ? 'Davlat' : 'Xususiy'} | |
| </span> | |
| </p> | |
| <p class="mb-2"><strong>Tuman:</strong> ${clinic.district}</p> | |
| <p class="mb-2"><strong>Manzil:</strong> ${clinic.address}</p> | |
| <p class="mb-2"> | |
| <strong>Telefon:</strong> | |
| <a href="tel:${clinic.phone}">${clinic.phone}</a> | |
| </p> | |
| </div> | |
| <div class="col-md-6"> | |
| <p class="mb-2"> | |
| <strong>Reyting:</strong> | |
| <span class="text-warning"> | |
| ${'β '.repeat(Math.floor(clinic.rating))}${'β'.repeat(5 - Math.floor(clinic.rating))} | |
| </span> | |
| ${clinic.rating}/5.0 | |
| </p> | |
| <p class="mb-2"><strong>Ish vaqti:</strong> ${clinic.working_hours}</p> | |
| <p class="mb-2"><strong>Ish kunlari:</strong> ${clinic.working_days.join(', ')}</p> | |
| <p class="mb-2"><strong>Doktorlar:</strong> ${clinic.doctors_count} ta</p> | |
| </div> | |
| </div> | |
| ${clinic.description ? ` | |
| <div class="alert alert-info mb-3"> | |
| <i class="bi bi-info-circle-fill me-1"></i> ${clinic.description} | |
| </div> | |
| ` : ''} | |
| <h6 class="mt-3 mb-2">Mutaxassisliklar</h6> | |
| <div class="mb-3"> | |
| ${clinic.specializations.map(spec => ` | |
| <span class="badge bg-primary me-1 mb-1">${spec}</span> | |
| `).join('')} | |
| </div> | |
| ${clinic.services && clinic.services.length > 0 ? ` | |
| <h6 class="mt-3 mb-2">Xizmatlar</h6> | |
| <ul class="list-group mb-3"> | |
| ${clinic.services.map(service => ` | |
| <li class="list-group-item d-flex justify-content-between align-items-center"> | |
| ${service.name} | |
| <span class="badge bg-success">${service.price}</span> | |
| </li> | |
| `).join('')} | |
| </ul> | |
| ` : ''} | |
| ${clinic.doctors && clinic.doctors.length > 0 ? ` | |
| <h6 class="mt-3 mb-2">Doktorlar (${clinic.doctors.length})</h6> | |
| <div class="row"> | |
| ${clinic.doctors.slice(0, 6).map(doctor => ` | |
| <div class="col-md-6 mb-3"> | |
| <div class="card h-100"> | |
| <div class="card-body p-2"> | |
| <div class="d-flex align-items-center"> | |
| <img src="${doctor.photo_url}" | |
| class="rounded-circle me-2" | |
| style="width: 50px; height: 50px; object-fit: cover;" | |
| alt="${doctor.full_name}"> | |
| <div> | |
| <h6 class="mb-0 small">${doctor.full_name}</h6> | |
| <p class="mb-0 text-muted" style="font-size: 0.75rem;"> | |
| ${doctor.specialty} | |
| </p> | |
| <p class="mb-0" style="font-size: 0.7rem;"> | |
| <span class="text-warning">β </span> ${doctor.rating} | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| ` : ''} | |
| ${clinic.photos && clinic.photos.length > 0 ? ` | |
| <h6 class="mt-3 mb-2">Fotogalereya</h6> | |
| <div class="row"> | |
| ${clinic.photos.map(photo => ` | |
| <div class="col-md-4 mb-2"> | |
| <img src="${photo.url}" class="img-fluid rounded" alt="${photo.caption}"> | |
| ${photo.caption ? `<p class="text-center small text-muted mt-1">${photo.caption}</p>` : ''} | |
| </div> | |
| `).join('')} | |
| </div> | |
| ` : ''} | |
| `; | |
| 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: `<div class="brigade-marker-icon" style=" | |
| background: ${color}; | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| border: 3px solid white; | |
| box-shadow: 0 3px 12px rgba(0,0,0,0.4); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 20px; | |
| animation: brigade-pulse 2s infinite; | |
| "> | |
| ${icon} | |
| </div>`, | |
| 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' ? | |
| '<span class="badge bg-danger">Bandlik</span>' : | |
| '<span class="badge bg-success">Bo\'sh</span>'; | |
| return ` | |
| <div class="brigade-popup"> | |
| <h6 class="mb-2"> | |
| <i class="bi bi-ambulance me-1"></i> | |
| ${brigade.name} | |
| </h6> | |
| <hr class="my-2"> | |
| <p class="mb-1 small"> | |
| <strong>Status:</strong> ${statusLabel} | |
| </p> | |
| <p class="mb-1 small text-muted"> | |
| <i class="bi bi-telephone-fill me-1"></i> ${brigade.phone} | |
| </p> | |
| <p class="mb-0 small text-muted"> | |
| <i class="bi bi-speedometer2 me-1"></i> ${brigade.speed_kmh} km/h | |
| </p> | |
| ${brigade.assigned_case_id ? ` | |
| <p class="mb-0 small mt-2"> | |
| <span class="badge bg-primary">Case: ${brigade.assigned_case_id}</span> | |
| </p> | |
| ` : ''} | |
| </div> | |
| `; | |
| } | |
| // ==================== 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); | |
| } | |
| } |