// Глобальные данные let allObjects = []; let filteredObjects = []; let map = null; let markers = []; // Загрузка данных async function loadData() { try { const response = await fetch('data.json'); allObjects = await response.json(); filteredObjects = [...allObjects]; initDashboard(); initMap(); populateFilters(); renderObjects(); renderProblems(); renderAnalytics(); updateNotifications(); } catch (error) { console.error('Ошибка загрузки данных:', error); } } // Инициализация панели function initDashboard() { const total = allObjects.length; const active = allObjects.filter(o => ['План', 'Обследование', 'СМР'].includes(o.status)).length; const completed = allObjects.filter(o => ['Готов', 'Сдан'].includes(o.status)).length; const problems = allObjects.filter(o => o.has_problem).length; const overdue = allObjects.filter(o => o.is_overdue).length; const portsPlan = allObjects.reduce((sum, o) => sum + (o.ports || 0), 0); const portsFact = allObjects.filter(o => ['Готов', 'Сдан'].includes(o.status)) .reduce((sum, o) => sum + (o.ports || 0), 0); const percent = total > 0 ? ((completed / total) * 100).toFixed(1) : 0; document.getElementById('kpi-total').textContent = total; document.getElementById('kpi-active').textContent = active; document.getElementById('kpi-completed').textContent = completed; document.getElementById('kpi-problems').textContent = problems; document.getElementById('kpi-overdue').textContent = overdue; document.getElementById('kpi-ports-plan').textContent = portsPlan.toLocaleString(); document.getElementById('kpi-ports-fact').textContent = portsFact.toLocaleString(); document.getElementById('kpi-percent').textContent = percent + '%'; // График статусов const statusCounts = {}; allObjects.forEach(o => { statusCounts[o.status] = (statusCounts[o.status] || 0) + 1; }); const ctx = document.getElementById('statusChart').getContext('2d'); new Chart(ctx, { type: 'doughnut', data: { labels: Object.keys(statusCounts), datasets: [{ data: Object.values(statusCounts), backgroundColor: [ '#6b7280', // План '#f59e0b', // Обследование '#0ea5e9', // СМР '#ef4444', // Проблема '#10b981', // Готов '#10b981' // Сдан ] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } } }); } // Инициализация карты function initMap() { map = L.map('map').setView([48.0196, 66.9237], 5); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); // Маркеры объектов allObjects.forEach(obj => { const [lat, lng] = obj.coordinates; let color = '#1e40af'; if (obj.is_overdue) color = '#ef4444'; else if (obj.is_approaching) color = '#f59e0b'; else if (obj.status === 'Сдан') color = '#10b981'; else if (obj.status === 'Проблема') color = '#ef4444'; const marker = L.circleMarker([lat, lng], { radius: 8, fillColor: color, color: '#fff', weight: 2, opacity: 1, fillOpacity: 0.8 }); marker.bindPopup(` `); marker.on('click', () => openModal(obj)); markers.push(marker); marker.addTo(map); }); } // Заполнение фильтров function populateFilters() { const regions = [...new Set(allObjects.map(o => o.region).filter(Boolean))].sort(); const cities = [...new Set(allObjects.map(o => o.city).filter(Boolean))].sort(); const managers = [...new Set(allObjects.map(o => o.manager).filter(Boolean))].sort(); const regionSelect = document.getElementById('filter-region'); const citySelect = document.getElementById('filter-city'); const managerSelect = document.getElementById('filter-manager'); regions.forEach(r => { regionSelect.innerHTML += ``; }); cities.forEach(c => { citySelect.innerHTML += ``; }); managers.forEach(m => { managerSelect.innerHTML += ``; }); } // Фильтрация объектов function filterObjects() { const region = document.getElementById('filter-region').value; const city = document.getElementById('filter-city').value; const status = document.getElementById('filter-status').value; const manager = document.getElementById('filter-manager').value; const search = document.getElementById('search-input').value.toLowerCase(); filteredObjects = allObjects.filter(o => { if (region && o.region !== region) return false; if (city && o.city !== city) return false; if (status && o.status !== status) return false; if (manager && o.manager !== manager) return false; if (search && !o.address.toLowerCase().includes(search) && !o.city.toLowerCase().includes(search)) return false; return true; }); renderObjects(); } // Рендер объектов function renderObjects() { const container = document.getElementById('objects-list'); container.innerHTML = ''; filteredObjects.forEach(obj => { const card = createObjectCard(obj); container.innerHTML += card; }); } // Создание карточки объекта function createObjectCard(obj) { const overdueClass = obj.is_overdue ? 'overdue' : ''; const approachingClass = obj.is_approaching && !obj.is_overdue ? 'approaching' : ''; const statusClass = `status-${obj.status.toLowerCase().replace(/ /g, '-')}`; return `

${obj.city}

${obj.status}

${obj.address}

Тип: ${obj.object_type}
Портов: ${obj.ports}
План: ${formatDate(obj.plan_end_date)}
${obj.fact_end_date ? `
Факт: ${formatDate(obj.fact_end_date)}
` : ''}
Ответственный: ${obj.manager}
${obj.is_overdue ? `
⚠️ Просрочено на ${getDaysOverdue(obj.plan_end_date)} дн.
` : ''} ${obj.is_approaching && !obj.is_overdue ? `
⏰ Срок истекает через ${getDaysUntil(obj.plan_end_date)} дн.
` : ''}
`; } // Рендер проблемных объектов function renderProblems() { const problems = allObjects.filter(o => o.has_problem); const container = document.getElementById('problems-list'); container.innerHTML = ''; problems.forEach(obj => { container.innerHTML += createObjectCard(obj); }); } // Аналитика function renderAnalytics() { const regions = {}; allObjects.forEach(obj => { if (!regions[obj.region]) { regions[obj.region] = { plan: 0, fact: 0, portsPlan: 0, portsFact: 0 }; } regions[obj.region].plan++; if (['Готов', 'Сдан'].includes(obj.status)) { regions[obj.region].fact++; regions[obj.region].portsFact += obj.ports || 0; } regions[obj.region].portsPlan += obj.ports || 0; }); const regionNames = Object.keys(regions); const planData = regionNames.map(r => regions[r].plan); const factData = regionNames.map(r => regions[r].fact); const percentData = regionNames.map(r => regions[r].plan > 0 ? ((regions[r].fact / regions[r].plan) * 100).toFixed(1) : 0 ); const portsPlanData = regionNames.map(r => regions[r].portsPlan); const portsFactData = regionNames.map(r => regions[r].portsFact); // План vs Факт new Chart(document.getElementById('regionsChart').getContext('2d'), { type: 'bar', data: { labels: regionNames, datasets: [ { label: 'План', data: planData, backgroundColor: '#1e40af' }, { label: 'Факт', data: factData, backgroundColor: '#10b981' } ] }, options: { responsive: true, scales: { y: { beginAtZero: true } } } }); // Процент выполнения new Chart(document.getElementById('percentChart').getContext('2d'), { type: 'bar', data: { labels: regionNames, datasets: [{ label: '% выполнения', data: percentData, backgroundColor: percentData.map(p => p >= 100 ? '#10b981' : p >= 50 ? '#f59e0b' : '#ef4444') }] }, options: { responsive: true, scales: { y: { beginAtZero: true, max: 100 } } } }); // Порты new Chart(document.getElementById('portsChart').getContext('2d'), { type: 'bar', data: { labels: regionNames, datasets: [ { label: 'Портов по плану', data: portsPlanData, backgroundColor: '#6366f1' }, { label: 'Портов факт', data: portsFactData, backgroundColor: '#10b981' } ] }, options: { responsive: true, scales: { y: { beginAtZero: true } } } }); } // Уведомления function updateNotifications() { const overdue = allObjects.filter(o => o.is_overdue); const approaching = allObjects.filter(o => o.is_approaching && !o.is_overdue); const badge = document.getElementById('notif-badge'); badge.textContent = overdue.length + approaching.length; const list = document.getElementById('notif-list'); list.innerHTML = ''; overdue.forEach(obj => { list.innerHTML += `

Просрочен: ${obj.city}

${obj.address}

План: ${formatDate(obj.plan_end_date)}

`; }); approaching.forEach(obj => { list.innerHTML += `

Срок подходит: ${obj.city}

${obj.address}

План: ${formatDate(obj.plan_end_date)}

`; }); if (overdue.length === 0 && approaching.length === 0) { list.innerHTML = '

Нет уведомлений

'; } } function toggleNotifications() { const notif = document.getElementById('notifications'); notif.classList.toggle('hidden'); } // Модальное окно function openModal(obj) { document.getElementById('modal-title').textContent = `${obj.city} - ${obj.address}`; const content = document.getElementById('modal-content'); content.innerHTML = `

Тип объекта

${obj.object_type}

Статус

${obj.status}

Количество портов

${obj.ports}

Проникновение

${obj.penetration || 0}%

Регион: ${obj.region}
Адрес: ${obj.address}
Ответственный: ${obj.manager}
Дата начала: ${formatDate(obj.start_date)}
Плановая дата: ${formatDate(obj.plan_end_date)}
${obj.fact_end_date ? `
Фактическая дата: ${formatDate(obj.fact_end_date)}
` : ''}
Домов: ${obj.houses || 0}
Квартир: ${obj.apartments || 0}
${obj.problem_description ? `

⚠️ Описание проблемы

${obj.problem_description}

` : ''} ${obj.comment ? `

💬 Комментарий

${obj.comment}

` : ''} ${obj.is_overdue ? `

🚨 ОБЪЕКТ ПРОСРОЧЕН

Просрочка: ${getDaysOverdue(obj.plan_end_date)} дн.

` : ''} ${obj.is_approaching && !obj.is_overdue ? `

⏰ ВНИМАНИЕ: СРОК ПОДХОДИТ

Осталось дней: ${getDaysUntil(obj.plan_end_date)}

` : ''}
`; document.getElementById('object-modal').classList.remove('hidden'); } function closeModal() { document.getElementById('object-modal').classList.add('hidden'); } function showObject(id) { const obj = allObjects.find(o => o.id === id); if (obj) openModal(obj); } // Переключение секций function showSection(sectionId) { document.querySelectorAll('.section').forEach(s => s.classList.add('hidden')); document.getElementById(sectionId).classList.remove('hidden'); document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); event.target.classList.add('active'); if (sectionId === 'objects') { setTimeout(() => map.invalidateSize(), 300); } } // Вспомогательные функции function formatDate(dateStr) { if (!dateStr) return '—'; const date = new Date(dateStr); return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' }); } function getDaysOverdue(dateStr) { const today = new Date('2026-08-17'); const planDate = new Date(dateStr); const diff = Math.floor((today - planDate) / (1000 * 60 * 60 * 24)); return diff > 0 ? diff : 0; } function getDaysUntil(dateStr) { const today = new Date('2026-08-17'); const planDate = new Date(dateStr); const diff = Math.floor((planDate - today) / (1000 * 60 * 60 * 24)); return diff > 0 ? diff : 0; } // Закрытие модального окна по клику вне document.getElementById('object-modal').addEventListener('click', (e) => { if (e.target.id === 'object-modal') closeModal(); }); // Закрытие уведомлений по клику вне document.addEventListener('click', (e) => { const notif = document.getElementById('notifications'); const btn = e.target.closest('button[onclick*="toggleNotifications"]'); if (!notif.contains(e.target) && !btn && !notif.classList.contains('hidden')) { notif.classList.add('hidden'); } }); // Инициализация при загрузке document.addEventListener('DOMContentLoaded', loadData);