// 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 = `
`;
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 = `
`;
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 ? `
` : ''}
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.services.map(service => `
-
${service.name}
${service.price}
`).join('')}
` : ''}
${clinic.doctors && clinic.doctors.length > 0 ? `
Doktorlar (${clinic.doctors.length})
${clinic.doctors.slice(0, 6).map(doctor => `
${doctor.full_name}
${doctor.specialty}
β
${doctor.rating}
`).join('')}
` : ''}
${clinic.photos && clinic.photos.length > 0 ? `
Fotogalereya
${clinic.photos.map(photo => `

${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 `
`;
}
// ==================== 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);
}
}