// Глобальные данные
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.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);