/
woodoo
/
web_complex_manager
Обзор
Документация
Войти
/
woodoo
/
web_complex_manager
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
static/js/app.js
3 171 строка
130 KB
woodoo
upload files
25 ноя 2025, 13:35
25 ноя 2025, 13:35
d517a88
Код
Авторство
О чём код?
// Global variables let currentComplex = null; let isEditMode = false; let currentView = 'info'; let currentCoordinates = null; let currentMaintenanceFilter = ''; let currentUser = null; // Принудительное скрытие кнопки редактирования для user function hideEditButtonForUser() { if (currentUser && currentUser.role !== 'admin') { const editBtn = document.getElementById('editModeBtn'); if (editBtn) { editBtn.style.display = 'none'; console.log('Edit button hidden for user'); } } } // ==================== АВТОРИЗАЦИЯ И УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ ==================== // Проверка авторизации при загрузке async function checkAuth() { try { const response = await fetch('/api/current-user'); if (response.ok) { currentUser = await response.json(); console.log('Auth successful, user:', currentUser); updateUIForUserRole(); return true; } else { console.log('Auth failed, redirecting to login'); window.location.href = '/login'; return false; } } catch (error) { console.error('Auth check failed:', error); window.location.href = '/login'; return false; } } function updateUIForUserRole() { if (!currentUser) return; console.log('Updating UI for user role:', currentUser.role); // Обновляем информацию о пользователе document.getElementById('currentUsername').textContent = currentUser.username; document.getElementById('userRoleBadge').textContent = currentUser.role === 'admin' ? 'Админ' : 'Пользователь'; const isAdmin = currentUser.role === 'admin'; // Управление классами для скрытия/показа const adminElements = document.querySelectorAll('.admin-only'); adminElements.forEach(el => { if (isAdmin) { el.classList.remove('hide-for-user'); el.style.display = ''; } else { el.classList.add('hide-for-user'); el.style.display = 'none'; } }); // Принудительно скрываем кнопку редактирования для user hideEditButtonForUser(); // Если не админ, принудительно устанавливаем режим просмотра if (!isAdmin) { setViewMode(); } updateModeIndicator(); } function updateModeIndicator() { const modeIndicator = document.getElementById('modeIndicator'); if (!modeIndicator) return; if (currentUser.role === 'admin') { modeIndicator.textContent = isEditMode ? ' (Режим редактирования)' : ' (Режим просмотра)'; } else { modeIndicator.textContent = ' (Только просмотр)'; } } function logout() { if (confirm('Вы уверены, что хотите выйти?')) { fetch('/logout') .then(() => { window.location.href = '/login'; }) .catch(error => { console.error('Logout error:', error); window.location.href = '/login'; }); } } // ==================== УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ ==================== function showUsersModal() { if (!currentUser || currentUser.role !== 'admin') return; loadUsers(); const modal = new bootstrap.Modal(document.getElementById('usersModal')); modal.show(); } function loadUsers() { fetch('/api/users') .then(response => response.json()) .then(users => { renderUsers(users); }) .catch(error => { console.error('Error loading users:', error); showAlert('Ошибка загрузки пользователей', 'danger'); }); } function renderUsers(users) { const container = document.getElementById('usersTableBody'); if (!users || users.length === 0) { container.innerHTML = '<tr><td colspan="6" class="text-center">Пользователи не найдены</td></tr>'; return; } container.innerHTML = users.map(user => ` <tr> <td>${user.id}</td> <td>${user.username}</td> <td> <span class="badge ${user.role === 'admin' ? 'bg-danger' : 'bg-primary'}"> ${user.role === 'admin' ? 'Администратор' : 'Пользователь'} </span> </td> <td> <span class="badge ${user.is_active ? 'bg-success' : 'bg-secondary'}"> ${user.is_active ? 'Активен' : 'Неактивен'} </span> </td> <td>${user.created_at}</td> <td> <div class="btn-group btn-group-sm"> <button class="btn btn-outline-primary" onclick="editUser(${user.id})" title="Редактировать"> <i class="fas fa-edit"></i> </button> ${user.id !== currentUser.id ? ` <button class="btn btn-outline-danger" onclick="deleteUser(${user.id})" title="Удалить"> <i class="fas fa-trash"></i> </button> ` : ''} </div> </td> </tr> `).join(''); } function showAddUserModal() { document.getElementById('addUserForm').reset(); const modal = new bootstrap.Modal(document.getElementById('addUserModal')); modal.show(); } function createUser() { const username = document.getElementById('newUsername').value; const password = document.getElementById('newPassword').value; const role = document.getElementById('newUserRole').value; if (!username || !password) { showAlert('Заполните все обязательные поля', 'warning'); return; } fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, password: password, role: role }) }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('addUserModal')); modal.hide(); showAlert('Пользователь создан', 'success'); loadUsers(); }) .catch(error => { console.error('Error creating user:', error); showAlert('Ошибка создания пользователя: ' + error.message, 'danger'); }); } function editUser(userId) { fetch('/api/users') .then(response => response.json()) .then(users => { const user = users.find(u => u.id === userId); if (!user) { throw new Error('User not found'); } document.getElementById('editUserId').value = user.id; document.getElementById('editUsername').value = user.username; document.getElementById('editUserRole').value = user.role; document.getElementById('editUserActive').checked = user.is_active; document.getElementById('editPassword').value = ''; const modal = new bootstrap.Modal(document.getElementById('editUserModal')); modal.show(); }) .catch(error => { console.error('Error loading user for edit:', error); showAlert('Ошибка загрузки данных пользователя', 'danger'); }); } function updateUser() { const userId = document.getElementById('editUserId').value; const username = document.getElementById('editUsername').value; const password = document.getElementById('editPassword').value; const role = document.getElementById('editUserRole').value; const isActive = document.getElementById('editUserActive').checked; if (!username) { showAlert('Заполните все обязательные поля', 'warning'); return; } const updateData = { username: username, role: role, is_active: isActive }; if (password) { updateData.password = password; } fetch(`/api/users/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal')); modal.hide(); showAlert('Пользователь обновлен', 'success'); loadUsers(); // Если обновили текущего пользователя, перезагружаем страницу if (userId == currentUser.id) { setTimeout(() => { window.location.reload(); }, 1000); } }) .catch(error => { console.error('Error updating user:', error); showAlert('Ошибка обновления пользователя: ' + error.message, 'danger'); }); } function deleteUser(userId) { if (!confirm('Удалить пользователя? Это действие нельзя отменить.')) { return; } fetch(`/api/users/${userId}`, { method: 'DELETE' }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Пользователь удален', 'success'); loadUsers(); }) .catch(error => { console.error('Error deleting user:', error); showAlert('Ошибка удаления пользователя: ' + error.message, 'danger'); }); } // ==================== ОСНОВНЫЕ ФУНКЦИИ (С ПРОВЕРКОЙ ПРАВ) ==================== // Utility functions for maintenance type styling function getMaintenanceTypeClass(maintenanceType) { const typeMap = { 'ТО - еженедельное': 'maintenance-type-to', 'ТО1 - ежемесячное': 'maintenance-type-to1', 'ТО2 - ежеквартальное': 'maintenance-type-to2', 'ТО3 - ежегодное': 'maintenance-type-to3', 'Метрологическая поверка': 'maintenance-type-verification', 'Внеплановое': 'maintenance-type-unscheduled' }; return typeMap[maintenanceType] || 'maintenance-type-unscheduled'; } function showMapPreview(coordinates) { console.log('showMapPreview called with coordinates:', coordinates); if (!coordinates) { console.log('No coordinates provided'); showAlert('Координаты не указаны', 'warning'); return; } currentCoordinates = coordinates; document.getElementById('coordinatesDisplay').textContent = coordinates; document.getElementById('mapPreviewTitle').textContent = `Карта - ${currentComplex ? currentComplex.number : 'Комплекс'}`; console.log('Opening map modal...'); const modal = new bootstrap.Modal(document.getElementById('mapPreviewModal')); modal.show(); console.log('Map modal should be visible now'); } function openYandexMaps() { if (!currentCoordinates) return; // Преобразуем координаты для Яндекс.Карт const coords = currentCoordinates.split(',').map(coord => coord.trim()).join(','); const url = `https://yandex.ru/maps/?text=${encodeURIComponent(coords)}`; window.open(url, '_blank'); } function openGoogleMaps() { if (!currentCoordinates) return; // Преобразуем координаты для Google Maps const coords = currentCoordinates.split(',').map(coord => coord.trim()).join(','); const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(coords)}`; window.open(url, '_blank'); } // Utility functions function getCheckboxState(value) { // Функция для преобразования значений БД в checkbox состояния if (value === 'Да' || value === 1 || value === true) { return 'checked'; } if (value === 'Нет' || value === 0 || value === false) { return ''; } return ''; } function formatPDDValue(value) { // Функция для преобразования значений БД в отображаемый формат if (value === 'Да' || value === 1 || value === true) return 'ДА'; if (value === 'Нет' || value === 0 || value === false) return 'НЕТ'; return 'НЕТ'; } // Initialize application document.addEventListener('DOMContentLoaded', function() { console.log('Initializing Complex Manager...'); initializeApp(); }); async function initializeApp() { // Сначала проверяем авторизацию const isAuthenticated = await checkAuth(); if (!isAuthenticated) return; console.log('Initializing app for user:', currentUser); // Сразу обновляем UI по роли updateUIForUserRole(); updateDateTime(); setInterval(updateDateTime, 60000); // Load complexes list loadComplexes(); // Set up event listeners setupEventListeners(); // Для admin устанавливаем режим редактирования if (currentUser.role === 'admin') { setEditMode(); } else { setViewMode(); } // Дополнительная проверка через секунду setTimeout(hideEditButtonForUser, 1000); console.log('Application initialized'); } function setupEventListeners() { // Archive filter document.querySelectorAll('input[name="archiveFilter"]').forEach(radio => { radio.addEventListener('change', loadComplexes); }); // Search input document.getElementById('searchComplexes').addEventListener('input', debounce(loadComplexes, 300)); // Tab changes document.getElementById('complexTabs').addEventListener('shown.bs.tab', function(e) { const target = e.target.getAttribute('data-bs-target'); currentView = target.replace('#', ''); if (currentComplex) { loadTabContent(currentView); } }); } function updateDateTime() { const now = new Date(); const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }; document.getElementById('current-datetime').textContent = now.toLocaleDateString('ru-RU', options); } // Mode management function setViewMode() { isEditMode = false; document.getElementById('viewModeBtn').classList.add('btn-primary'); document.getElementById('viewModeBtn').classList.remove('btn-outline-primary'); document.getElementById('editModeBtn').classList.remove('btn-primary'); document.getElementById('editModeBtn').classList.add('btn-outline-primary'); // Скрываем кнопки для режима редактирования document.getElementById('editModeActions').style.display = 'none'; // Скрываем кнопки действий для комплекса document.getElementById('complexActions').style.display = 'none'; // Скрываем кнопку "Добавить комплекс" в списке document.getElementById('addComplexBtn').style.display = 'none'; // Обновляем индикатор режима updateModeIndicator(); if (currentComplex) { renderComplexInfo(); } } function setEditMode() { // Двойная проверка на случай прямого вызова if (!currentUser || currentUser.role !== 'admin') { console.log('Blocking edit mode for non-admin user:', currentUser); showAlert('Только администраторы могут редактировать данные', 'warning'); setViewMode(); return; } console.log('Entering edit mode for admin'); isEditMode = true; document.getElementById('editModeBtn').classList.add('btn-primary'); document.getElementById('editModeBtn').classList.remove('btn-outline-primary'); document.getElementById('viewModeBtn').classList.remove('btn-primary'); document.getElementById('viewModeBtn').classList.add('btn-outline-primary'); // Показываем кнопки для режима редактирования document.getElementById('editModeActions').style.display = 'inline'; // Показываем кнопку "Добавить комплекс" в списке document.getElementById('addComplexBtn').style.display = 'inline-block'; // Показываем кнопки действий для комплекса если выбран комплекс if (currentComplex) { document.getElementById('complexActions').style.display = 'block'; } // Обновляем индикатор режима updateModeIndicator(); if (currentComplex) { renderComplexInfo(); } } // Complex management function loadComplexes() { const filter = document.querySelector('input[name="archiveFilter"]:checked').value; const search = document.getElementById('searchComplexes').value; let url = `/api/complexes?filter=${filter}`; if (search) { url += `&search=${encodeURIComponent(search)}`; } fetch(url) .then(response => response.json()) .then(complexes => { renderComplexesList(complexes); }) .catch(error => { console.error('Error loading complexes:', error); showAlert('Ошибка загрузки списка комплексов', 'danger'); }); } function renderComplexesList(complexes) { const container = document.getElementById('complexesList'); if (!complexes || complexes.length === 0) { container.innerHTML = ` <div class="text-center text-muted py-4"> <i class="fas fa-inbox fa-2x mb-3"></i> <p>Комплексы не найдены</p> </div> `; return; } container.innerHTML = complexes.map(complex => ` <div class="complex-item ${complex.id === currentComplex?.id ? 'active' : ''} ${complex.is_archived ? 'archived' : ''}" onclick="selectComplex(${complex.id})"> <div class="complex-number">${complex.number || 'Без номера'}</div> <div class="complex-location">${complex.location || 'Адрес не указан'}</div> ${complex.is_archived ? '<small class="text-warning"><i class="fas fa-archive"></i> Архивный</small>' : ''} </div> `).join(''); } function selectComplex(complexId) { console.log('Selecting complex:', complexId); // Показываем индикатор загрузки document.getElementById('noComplexSelected').style.display = 'none'; document.getElementById('complexDetails').style.display = 'block'; document.getElementById('detailsTitle').textContent = 'Загрузка...'; fetch(`/api/complexes/${complexId}`) .then(response => { console.log('Response status:', response.status); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(complex => { console.log('Complex data received:', complex); if (complex.error) { throw new Error(complex.error); } currentComplex = complex; document.getElementById('detailsTitle').textContent = `Комплекс ${complex.number || complex.id}`; // Show/hide archive badge const archiveBadge = document.getElementById('archiveBadge'); if (complex.is_archived) { archiveBadge.style.display = 'inline'; } else { archiveBadge.style.display = 'none'; } // Update active item in list document.querySelectorAll('.complex-item').forEach(item => { item.classList.remove('active'); }); const selectedItem = document.querySelector(`.complex-item[onclick="selectComplex(${complexId})"]`); if (selectedItem) { selectedItem.classList.add('active'); } // Показываем кнопки действий в режиме редактирования if (isEditMode) { document.getElementById('complexActions').style.display = 'block'; } // Load initial tab content renderComplexInfo(); loadTabContent(currentView); }) .catch(error => { console.error('Error loading complex details:', error); document.getElementById('detailsTitle').textContent = 'Ошибка загрузки'; showAlert('Ошибка загрузки данных комплекса: ' + error.message, 'danger'); // Показываем сообщение об ошибке document.getElementById('noComplexSelected').style.display = 'block'; document.getElementById('noComplexSelected').innerHTML = ` <div class="text-center text-danger py-4"> <i class="fas fa-exclamation-triangle fa-2x mb-3"></i> <p>Ошибка загрузки данных комплекса</p> <small>${error.message}</small> </div> `; document.getElementById('complexDetails').style.display = 'none'; // Скрываем кнопки действий document.getElementById('complexActions').style.display = 'none'; }); } function renderComplexInfo() { const viewModeDiv = document.getElementById('infoViewMode'); const editModeForm = document.getElementById('infoEditMode'); if (isEditMode) { viewModeDiv.style.display = 'none'; editModeForm.style.display = 'block'; renderEditForm(); } else { viewModeDiv.style.display = 'block'; editModeForm.style.display = 'none'; renderViewMode(); } } function renderViewMode() { const complex = currentComplex; const container = document.getElementById('infoViewMode'); // Определяем какое значение использовать для разрешенной скорости - ДО использования const allowedSpeed = complex.allowed_speed || complex.pdd_speed || ''; container.innerHTML = ` <div class="row"> <div class="col-md-6"> <div class="field-group"> <legend>Основная информация</legend> <div class="row mb-2"> <div class="col-4"><strong>Номер:</strong></div> <div class="col-8">${complex.number || 'Не указан'}</div> </div> <div class="row mb-2"> <div class="col-4"><strong>Название:</strong></div> <div class="col-8">${complex.complex_name || 'Не указано'}</div> </div> <div class="row mb-2"> <div class="col-4"><strong>Полное название:</strong></div> <div class="col-8">${complex.full_name || 'Не указано'}</div> </div> <div class="row mb-2"> <div class="col-4"><strong>Адрес:</strong></div> <div class="col-8">${complex.location || 'Не указан'}</div> </div> <div class="row mb-2"> <div class="col-4"><strong>Район:</strong></div> <div class="col-8">${complex.district || 'Не указан'}</div> </div> <div class="row mb-2"> <div class="col-4"><strong>Тип:</strong></div> <div class="col-8">${complex.complex_type || 'Не указан'}</div> </div> </div> <div class="field-group"> <legend>Сетевые настройки</legend> <div class="row mb-2"> <div class="col-4"><strong>IP адрес:</strong></div> <div class="col-8">${complex.ip_address || 'Не указан'}</div> </div> <div class="row mb-2"> <div class="col-4"><strong>SIM карта:</strong></div> <div class="col-8">${complex.sim_card_number || 'Не указан'}</div> </div> <div class="row mb-2"> <div class="col-4"><strong>Номер сигнализации:</strong></div> <div class="col-8">${complex.alarm_number || 'Не указан'}</div> </div> </div> </div> <div class="col-md-6"> <div class="field-group"> <legend>Даты</legend> <div class="row mb-2"> <div class="col-5"><strong>Ввод в РИФ:</strong></div> <div class="col-7">${complex.commission_date || 'Не указана'}</div> </div> <div class="row mb-2"> <div class="col-5"><strong>Дата поверки:</strong></div> <div class="col-7">${complex.verification_date || 'Не указана'}</div> </div> <div class="row mb-2"> <div class="col-5"><strong>Поверка действительна до:</strong></div> <div class="col-7 ${getVerificationStatusClass(complex.verification_valid_until)}"> ${complex.verification_valid_until || 'Не указана'} ${getVerificationStatusBadge(complex.verification_valid_until)} </div> </div> <div class="row mb-2"> <div class="col-5"><strong>№ поверки:</strong></div> <div class="col-7">${complex.verification_number || 'Не указан'}</div> </div> </div> <div class="field-group"> <legend>Конфигурация</legend> <div class="row mb-2"> <div class="col-6"><strong>Датчики нарушений:</strong></div> <div class="col-6">${complex.violation_sensors || 'Не указаны'}</div> </div> <div class="row mb-2"> <div class="col-6"><strong>Обзорные датчики:</strong></div> <div class="col-6">${complex.overview_sensors || 'Не указаны'}</div> </div> <div class="row mb-2"> <div class="col-6"><strong>Поток:</strong></div> <div class="col-6">${formatPDDValue(complex.flow)}</div> </div> <div class="row mb-2"> <div class="col-6"><strong>Разрешенная скорость:</strong></div> <div class="col-6">${allowedSpeed ? allowedSpeed + ' км/ч' : 'Не указана'}</div> </div> </div> </div> </div> <div class="field-group mt-3"> <legend>Нарушения ПДД</legend> <div class="row"> <div class="col-md-4 mb-2"> <div class="d-flex align-items-center"> <span class="badge ${formatPDDValue(complex.pdd_stop) === 'ДА' ? 'bg-success' : 'bg-secondary'} me-2"> ${formatPDDValue(complex.pdd_stop)} </span> <span>Заезд за стоп-линию</span> </div> </div> <div class="col-md-4 mb-2"> <div class="d-flex align-items-center"> <span class="badge ${formatPDDValue(complex.pdd_traffic_light) === 'ДА' ? 'bg-success' : 'bg-secondary'} me-2"> ${formatPDDValue(complex.pdd_traffic_light)} </span> <span>Проезд на красный</span> </div> </div> <div class="col-md-4 mb-2"> <div class="d-flex align-items-center"> <span class="badge ${formatPDDValue(complex.pdd_rails) === 'ДА' ? 'bg-success' : 'bg-secondary'} me-2"> ${formatPDDValue(complex.pdd_rails)} </span> <span>Выезд на трамвайные пути</span> </div> </div> <div class="col-md-4 mb-2"> <div class="d-flex align-items-center"> <span class="badge ${formatPDDValue(complex.row_type) === 'ДА' ? 'bg-success' : 'bg-secondary'} me-2"> ${formatPDDValue(complex.row_type)} </span> <span>Рядность</span> </div> </div> <div class="col-md-4 mb-2"> <div class="d-flex align-items-center"> <span class="badge ${formatPDDValue(complex.pdd_parking) === 'ДА' ? 'bg-success' : 'bg-secondary'} me-2"> ${formatPDDValue(complex.pdd_parking)} </span> <span>Парковка</span> </div> </div> <div class="col-md-4 mb-2"> <div class="d-flex align-items-center"> <span class="badge ${formatPDDValue(complex.pdd_pedestrian) === 'ДА' ? 'bg-success' : 'bg-secondary'} me-2"> ${formatPDDValue(complex.pdd_pedestrian)} </span> <span>Пешеходный переход</span> </div> </div> <div class="col-md-4 mb-2"> <div class="d-flex align-items-center"> <span class="badge ${formatPDDValue(complex.pdd_oncoming) === 'ДА' ? 'bg-success' : 'bg-secondary'} me-2"> ${formatPDDValue(complex.pdd_oncoming)} </span> <span>Встречная полоса</span> </div> </div> <div class="col-md-4 mb-2"> <div class="d-flex align-items-center"> <span class="badge ${formatPDDValue(complex.pdd_belt) === 'ДА' ? 'bg-success' : 'bg-secondary'} me-2"> ${formatPDDValue(complex.pdd_belt)} </span> <span>Ремень безопасности</span> </div> </div> <div class="col-md-4 mb-2"> <div class="d-flex align-items-center"> <span class="badge ${formatPDDValue(complex.pdd_light) === 'ДА' ? 'bg-success' : 'bg-secondary'} me-2"> ${formatPDDValue(complex.pdd_light)} </span> <span>Световые приборы</span> </div> </div> </div> </div> <div class="field-group mt-3"> <legend>Координаты</legend> <div class="row align-items-center"> <div class="col-9"> <div class="row mb-2"> <div class="col-12"> <small class="text-muted">${complex.coordinates || 'Не указаны'}</small> </div> </div> </div> <div class="col-3"> ${complex.coordinates ? ` <button class="btn btn-outline-primary btn-sm" onclick="showMapPreview('${complex.coordinates}')"> <i class="fas fa-map-marked-alt"></i> Просмотр на карте </button> ` : ''} </div> </div> </div> `; } function renderEditForm() { const complex = currentComplex; const container = document.getElementById('infoEditMode'); // Определяем какое значение использовать для разрешенной скорости - ДО использования const allowedSpeed = complex.allowed_speed || complex.pdd_speed || ''; container.innerHTML = ` <div class="row"> <div class="col-md-6"> <div class="field-group"> <legend>Основная информация</legend> <div class="mb-3"> <label class="form-label">Номер комплекса</label> <input type="text" class="form-control" name="number" value="${complex.number || ''}"> </div> <div class="mb-3"> <label class="form-label">Название комплекса</label> <input type="text" class="form-control" name="complex_name" value="${complex.complex_name || ''}"> </div> <div class="mb-3"> <label class="form-label">Полное название (для поиска фото)</label> <input type="text" class="form-control" name="full_name" value="${complex.full_name || ''}" placeholder="Например: Арбалет Ельня (Гагаринский р-н)"> </div> <div class="mb-3"> <label class="form-label">Местоположение</label> <input type="text" class="form-control" name="location" value="${complex.location || ''}"> </div> <div class="mb-3"> <label class="form-label">Район</label> <input type="text" class="form-control" name="district" value="${complex.district || ''}"> </div> <div class="mb-3"> <label class="form-label">Тип комплекса</label> <input type="text" class="form-control" name="complex_type" value="${complex.complex_type || ''}"> </div> </div> <div class="field-group"> <legend>Установка</legend> <div class="row"> <div class="col-6"> <div class="mb-3"> <label class="form-label">Месяц установки</label> <input type="text" class="form-control" name="install_month" value="${complex.install_month || ''}"> </div> </div> <div class="col-6"> <div class="mb-3"> <label class="form-label">Год установки</label> <input type="text" class="form-control" name="install_year" value="${complex.install_year || ''}"> </div> </div> </div> <div class="mb-3"> <label class="form-label">Дата ввода в РИФ</label> <input type="text" class="form-control date-input" name="commission_date" value="${complex.commission_date || ''}" placeholder="дд.мм.гггг"> </div> </div> </div> <div class="col-md-6"> <div class="field-group"> <legend>Поверка</legend> <div class="mb-3"> <label class="form-label">Номер поверки</label> <input type="text" class="form-control" name="verification_number" value="${complex.verification_number || ''}"> </div> <div class="mb-3"> <label class="form-label">Дата поверки</label> <input type="text" class="form-control date-input" name="verification_date" value="${complex.verification_date || ''}" placeholder="дд.мм.гггг"> </div> <div class="mb-3"> <label class="form-label">Поверка действительна до</label> <input type="text" class="form-control date-input" name="verification_valid_until" value="${complex.verification_valid_until || ''}" placeholder="дд.мм.гггг"> </div> </div> <div class="field-group"> <legend>Сетевые настройки</legend> <div class="mb-3"> <label class="form-label">IP адрес</label> <input type="text" class="form-control" name="ip_address" value="${complex.ip_address || ''}"> </div> <div class="mb-3"> <label class="form-label">Номер SIM карты</label> <input type="text" class="form-control" name="sim_card_number" value="${complex.sim_card_number || ''}"> </div> <div class="mb-3"> <label class="form-label">Номер сигнализации</label> <input type="text" class="form-control" name="alarm_number" value="${complex.alarm_number || ''}"> </div> </div> <div class="field-group"> <legend>Конфигурация</legend> <div class="row"> <div class="col-6"> <div class="mb-3"> <label class="form-label">Датчики нарушений</label> <input type="text" class="form-control" name="violation_sensors" value="${complex.violation_sensors || ''}" placeholder="Например: 2 камеры"> </div> </div> <div class="col-6"> <div class="mb-3"> <label class="form-label">Обзорные датчики</label> <input type="text" class="form-control" name="overview_sensors" value="${complex.overview_sensors || ''}" placeholder="Например: 1 камера"> </div> </div> </div> <div class="mb-3"> <label class="form-label">Поток</label> <div class="form-check"> <input class="form-check-input" type="checkbox" name="flow" ${getCheckboxState(complex.flow)}> <label class="form-check-label">Активен</label> </div> </div> <div class="mb-3"> <label class="form-label">Разрешенная скорость (км/ч)</label> <input type="number" class="form-control" name="allowed_speed" value="${allowedSpeed}"> </div> </div> </div> </div> <div class="field-group mt-3"> <legend>Нарушения ПДД</legend> <div class="row"> <div class="col-md-4 mb-2"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="pdd_stop" ${getCheckboxState(complex.pdd_stop)}> <label class="form-check-label">Заезд за стоп-линию</label> </div> </div> <div class="col-md-4 mb-2"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="pdd_traffic_light" ${getCheckboxState(complex.pdd_traffic_light)}> <label class="form-check-label">Проезд на красный</label> </div> </div> <div class="col-md-4 mb-2"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="pdd_rails" ${getCheckboxState(complex.pdd_rails)}> <label class="form-check-label">Выезд на трамвайные пути</label> </div> </div> <div class="col-md-4 mb-2"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="row_type" ${getCheckboxState(complex.row_type)}> <label class="form-check-label">Рядность</label> </div> </div> <div class="col-md-4 mb-2"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="pdd_parking" ${getCheckboxState(complex.pdd_parking)}> <label class="form-check-label">Парковка</label> </div> </div> <div class="col-md-4 mb-2"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="pdd_pedestrian" ${getCheckboxState(complex.pdd_pedestrian)}> <label class="form-check-label">Пешеходный переход</label> </div> </div> <div class="col-md-4 mb-2"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="pdd_oncoming" ${getCheckboxState(complex.pdd_oncoming)}> <label class="form-check-label">Встречная полоса</label> </div> </div> <div class="col-md-4 mb-2"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="pdd_belt" ${getCheckboxState(complex.pdd_belt)}> <label class="form-check-label">Ремень безопасности</label> </div> </div> <div class="col-md-4 mb-2"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="pdd_light" ${getCheckboxState(complex.pdd_light)}> <label class="form-check-label">Световые приборы</label> </div> </div> </div> </div> <div class="field-group mt-3"> <legend>Координаты</legend> <div class="mb-3"> <label class="form-label">Координаты (широта, долгота)</label> <input type="text" class="form-control" name="coordinates" value="${complex.coordinates || ''}" placeholder="55.123456, 37.123456"> </div> </div> `; } // Tab content loading function loadTabContent(tabName) { console.log('Loading tab:', tabName); currentView = tabName; switch(tabName) { case 'info': renderComplexInfo(); break; case 'maintenance': loadMaintenance(); break; case 'images': loadImages(); break; case 'calibration-images': loadCalibrationImages(); break; case 'documents': loadDocuments(); break; case 'maintenance-documents': loadMaintenanceDocuments(); break; case 'maintenance-photos': // НОВАЯ ВКЛАДКА loadMaintenancePhotos(); break; } } // Maintenance functions function loadMaintenance() { if (!currentComplex) return; let url = `/api/complexes/${currentComplex.id}/maintenance`; if (currentMaintenanceFilter) { url += `?type=${encodeURIComponent(currentMaintenanceFilter)}`; } fetch(url) .then(response => response.json()) .then(maintenance => { renderMaintenance(maintenance); }) .catch(error => { console.error('Error loading maintenance:', error); showAlert('Ошибка загрузки данных обслуживания', 'danger'); }); } function renderMaintenance(maintenance) { const container = document.getElementById('maintenanceContent'); const isEdit = isEditMode; container.innerHTML = ` <div class="d-flex justify-content-between align-items-center mb-3"> <h6>История обслуживания</h6> ${isEdit ? ` <div class="btn-group"> <button class="btn btn-primary btn-sm" onclick="showAddMaintenanceModal()"> <i class="fas fa-plus"></i> Добавить обслуживание </button> <button class="btn btn-success btn-sm" onclick="exportMaintenance()"> <i class="fas fa-file-excel"></i> Экспорт в Excel </button> </div> ` : ` <button class="btn btn-success btn-sm" onclick="exportMaintenance()"> <i class="fas fa-file-excel"></i> Экспорт в Excel </button> `} </div> <div class="row mb-3"> <div class="col-md-6"> <label class="form-label">Фильтр по типу обслуживания:</label> <select class="form-select" id="maintenanceTypeFilter" onchange="filterMaintenanceByType(this.value)"> <option value="">Все типы</option> <option value="ТО - еженедельное">ТО - еженедельное</option> <option value="ТО1 - ежемесячное">ТО1 - ежемесячное</option> <option value="ТО2 - ежеквартальное">ТО2 - ежеквартальное</option> <option value="ТО3 - ежегодное">ТО3 - ежегодное</option> <option value="Метрологическая поверка">Метрологическая поверка</option> <option value="Внеплановое">Внеплановое</option> </select> </div> <div class="col-md-6 d-flex align-items-end"> ${currentMaintenanceFilter ? ` <div class="alert alert-info py-2 mb-0 w-100"> <small>Фильтр активен: <strong>${currentMaintenanceFilter}</strong></small> <button class="btn btn-sm btn-outline-danger ms-2" onclick="clearMaintenanceFilter()"> <i class="fas fa-times"></i> Сбросить </button> </div> ` : ''} </div> </div> ${maintenance && maintenance.length > 0 ? ` <div class="table-responsive"> <table class="table table-striped table-sm maintenance-table"> <thead> <tr> <th>Дата</th> <th>Тип</th> <th>Описание</th> ${isEdit ? '<th>Действия</th>' : ''} </tr> </thead> <tbody> ${maintenance.map(item => ` <tr> <td>${item.maintenance_date}</td> <td><span class="${getMaintenanceTypeClass(item.maintenance_type)}">${item.maintenance_type}</span></td> <td>${item.description || ''}</td> ${isEdit ? ` <td> <div class="btn-group btn-group-sm"> <button class="btn btn-outline-primary" onclick="editMaintenance(${item.id})" title="Редактировать"> <i class="fas fa-edit"></i> </button> <button class="btn btn-outline-danger" onclick="deleteMaintenance(${item.id})" title="Удалить"> <i class="fas fa-trash"></i> </button> </div> </td> ` : ''} </tr> `).join('')} </tbody> </table> </div> ` : ` <div class="text-center text-muted py-4"> <i class="fas fa-tools fa-2x mb-3"></i> <p>Записи об обслуживании отсутствуют</p> </div> `} `; // Устанавливаем текущее значение фильтра if (currentMaintenanceFilter) { document.getElementById('maintenanceTypeFilter').value = currentMaintenanceFilter; } } function filterMaintenanceByType(type) { currentMaintenanceFilter = type; loadMaintenance(); } function clearMaintenanceFilter() { currentMaintenanceFilter = ''; loadMaintenance(); } function exportMaintenance() { if (!currentComplex) return; let url = `/api/complexes/${currentComplex.id}/maintenance/export`; if (currentMaintenanceFilter) { url += `?type=${encodeURIComponent(currentMaintenanceFilter)}`; } // Создаем временную ссылку для скачивания const link = document.createElement('a'); link.href = url; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function exportAllMaintenance() { let url = '/api/maintenance/export-all'; if (currentMaintenanceFilter) { url += `?type=${encodeURIComponent(currentMaintenanceFilter)}`; } // Создаем временную ссылку для скачивания const link = document.createElement('a'); link.href = url; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function showAddMaintenanceModal() { if (!currentComplex) return; document.getElementById('maintenanceComplexId').value = currentComplex.id; document.getElementById('maintenanceDate').value = ''; document.getElementById('maintenanceType').value = ''; document.getElementById('maintenanceDescription').value = ''; const modal = new bootstrap.Modal(document.getElementById('addMaintenanceModal')); modal.show(); } function saveMaintenance() { const complexId = document.getElementById('maintenanceComplexId').value; const date = document.getElementById('maintenanceDate').value; const type = document.getElementById('maintenanceType').value; const description = document.getElementById('maintenanceDescription').value; if (!date || !type) { showAlert('Заполните обязательные поля', 'warning'); return; } const maintenanceData = { complex_id: parseInt(complexId), maintenance_date: date, maintenance_type: type, description: description }; fetch('/api/maintenance', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(maintenanceData) }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('addMaintenanceModal')); modal.hide(); showAlert('Обслуживание добавлено', 'success'); loadMaintenance(); }) .catch(error => { console.error('Error saving maintenance:', error); showAlert('Ошибка сохранения обслуживания: ' + error.message, 'danger'); }); } function editMaintenance(maintenanceId) { fetch(`/api/maintenance/${maintenanceId}`) .then(response => response.json()) .then(maintenance => { if (maintenance.error) { throw new Error(maintenance.error); } document.getElementById('editMaintenanceId').value = maintenance.id; document.getElementById('editMaintenanceComplexId').value = maintenance.complex_id; document.getElementById('editMaintenanceDate').value = maintenance.maintenance_date; document.getElementById('editMaintenanceType').value = maintenance.maintenance_type; document.getElementById('editMaintenanceDescription').value = maintenance.description || ''; const modal = new bootstrap.Modal(document.getElementById('editMaintenanceModal')); modal.show(); }) .catch(error => { console.error('Error loading maintenance for edit:', error); showAlert('Ошибка загрузки данных обслуживания', 'danger'); }); } function updateMaintenance() { const maintenanceId = document.getElementById('editMaintenanceId').value; const date = document.getElementById('editMaintenanceDate').value; const type = document.getElementById('editMaintenanceType').value; const description = document.getElementById('editMaintenanceDescription').value; if (!date || !type) { showAlert('Заполните обязательные поля', 'warning'); return; } const maintenanceData = { maintenance_date: date, maintenance_type: type, description: description }; fetch(`/api/maintenance/${maintenanceId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(maintenanceData) }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('editMaintenanceModal')); modal.hide(); showAlert('Обслуживание обновлено', 'success'); loadMaintenance(); }) .catch(error => { console.error('Error updating maintenance:', error); showAlert('Ошибка обновления обслуживания: ' + error.message, 'danger'); }); } function deleteMaintenance(maintenanceId) { if (!confirm('Удалить запись об обслуживании?')) return; fetch(`/api/maintenance/${maintenanceId}`, { method: 'DELETE' }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Запись об обслуживании удалена', 'success'); loadMaintenance(); }) .catch(error => { console.error('Error deleting maintenance:', error); showAlert('Ошибка удаления записи', 'danger'); }); } // Bulk maintenance functions function showBulkMaintenanceModal() { document.getElementById('bulkMaintenanceFile').value = ''; document.getElementById('bulkMaintenanceResults').style.display = 'none'; document.getElementById('bulkResultsContent').innerHTML = ''; const modal = new bootstrap.Modal(document.getElementById('bulkMaintenanceModal')); modal.show(); } function uploadBulkMaintenance() { const fileInput = document.getElementById('bulkMaintenanceFile'); const file = fileInput.files[0]; if (!file) { showAlert('Выберите Excel файл', 'warning'); return; } const formData = new FormData(); formData.append('file', file); // Показываем индикатор загрузки const resultsDiv = document.getElementById('bulkResultsContent'); resultsDiv.innerHTML = ` <div class="text-center"> <div class="loading-spinner"></div> <p class="mt-2">Обработка файла...</p> </div> `; document.getElementById('bulkMaintenanceResults').style.display = 'block'; fetch('/api/maintenance/bulk', { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } displayBulkResults(result.results); }) .catch(error => { console.error('Error uploading bulk maintenance:', error); resultsDiv.innerHTML = ` <div class="alert alert-danger"> <i class="fas fa-exclamation-triangle"></i> Ошибка: ${error.message} </div> `; }); } function displayBulkResults(results) { const container = document.getElementById('bulkResultsContent'); let html = ` <div class="alert alert-${results.failed === 0 ? 'success' : 'warning'}"> <strong>Обработано записей: ${results.total_processed}</strong><br> Успешно: <span class="text-success">${results.successful}</span> | Ошибки: <span class="text-danger">${results.failed}</span> </div> `; if (results.details && results.details.length > 0) { html += '<div class="small">'; results.details.forEach(detail => { const badgeClass = detail.status === 'success' ? 'bg-success' : detail.status === 'error' ? 'bg-danger' : 'bg-secondary'; const icon = detail.status === 'success' ? 'fa-check' : detail.status === 'error' ? 'fa-times' : 'fa-minus'; html += ` <div class="d-flex align-items-center mb-1 p-1 border-bottom"> <span class="badge ${badgeClass} me-2"> <i class="fas ${icon}"></i> </span> <div class="flex-grow-1"> <strong>Строка ${detail.row}:</strong> ${detail.complex_number || ''} <br><small class="text-muted">${detail.message}</small> </div> </div> `; }); html += '</div>'; } container.innerHTML = html; // Прокручиваем к результатам container.scrollTop = 0; } // Image functions function loadImages() { if (!currentComplex) return; fetch(`/api/complexes/${currentComplex.id}/images`) .then(response => response.json()) .then(images => { renderImages(images); }) .catch(error => { console.error('Error loading images:', error); showAlert('Ошибка загрузки изображений', 'danger'); }); } function renderImages(images) { const container = document.getElementById('imagesContent'); const isEdit = isEditMode; container.innerHTML = ` <div class="d-flex justify-content-between align-items-center mb-3"> <h6>Галерея изображений</h6> ${isEdit ? ` <button class="btn btn-primary btn-sm" onclick="showUploadImageModal()"> <i class="fas fa-upload"></i> Загрузить изображение </button> ` : ''} </div> ${images && images.length > 0 ? ` <div class="image-gallery"> ${images.map(image => ` <div class="image-item"> <div class="image-container"> <img src="/api/images/${image.id}" alt="${image.image_name}" onclick="previewImage(${image.id}, '${image.image_name}')"> </div> ${isEdit ? ` <div class="image-actions"> <button class="btn btn-danger btn-sm" onclick="deleteImage(${image.id}, event)" title="Удалить"> <i class="fas fa-trash"></i> </button> </div> ` : ''} <!-- Убрали блок image-info чтобы скрыть названия --> </div> `).join('')} </div> ` : ` <div class="text-center text-muted py-4"> <i class="fas fa-images fa-2x mb-3"></i> <p>Изображения отсутствуют</p> </div> `} `; } function showUploadImageModal() { if (!currentComplex) return; document.getElementById('uploadComplexId').value = currentComplex.id; document.getElementById('uploadImageForm').reset(); const modal = new bootstrap.Modal(document.getElementById('uploadImageModal')); modal.show(); } function uploadImage() { const form = document.getElementById('uploadImageForm'); const formData = new FormData(form); const complexId = document.getElementById('uploadComplexId').value; fetch(`/api/complexes/${complexId}/images`, { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('uploadImageModal')); modal.hide(); showAlert('Изображение загружено', 'success'); loadImages(); }) .catch(error => { console.error('Error uploading image:', error); showAlert('Ошибка загрузки изображения: ' + error.message, 'danger'); }); } function previewImage(imageId, imageName) { document.getElementById('imagePreviewTitle').textContent = imageName; document.getElementById('previewImage').src = `/api/images/${imageId}`; const modal = new bootstrap.Modal(document.getElementById('imagePreviewModal')); modal.show(); } function deleteImage(imageId, event) { if (event) event.stopPropagation(); if (!confirm('Удалить изображение?')) return; fetch(`/api/images/${imageId}`, { method: 'DELETE' }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Изображение удалено', 'success'); loadImages(); }) .catch(error => { console.error('Error deleting image:', error); showAlert('Ошибка удаления изображения', 'danger'); }); } // Document functions function loadDocuments() { if (!currentComplex) return; fetch(`/api/complexes/${currentComplex.id}/documents`) .then(response => response.json()) .then(documents => { renderDocuments(documents); }) .catch(error => { console.error('Error loading documents:', error); showAlert('Ошибка загрузки документов', 'danger'); }); } function renderDocuments(documents) { const container = document.getElementById('documentsContent'); const isEdit = isEditMode; container.innerHTML = ` <div class="d-flex justify-content-between align-items-center mb-3"> <h6>Документы</h6> ${isEdit ? ` <button class="btn btn-primary btn-sm" onclick="showUploadDocumentModal()"> <i class="fas fa-upload"></i> Загрузить документ </button> ` : ''} </div> ${documents && documents.length > 0 ? ` <div class="documents-list"> ${documents.map(doc => ` <div class="document-item"> <div class="d-flex justify-content-between align-items-center"> <div class="d-flex align-items-center"> <div class="document-icon me-3"> <i class="fas fa-file-pdf text-danger"></i> </div> <div> <div class="fw-bold">${doc.document_name}</div> <small class="text-muted"> ${doc.document_type_ru || doc.document_type} • ${doc.size_formatted} • ${doc.upload_date} </small> </div> </div> <div class="btn-group"> <a href="/api/documents/${doc.id}" class="btn btn-primary btn-sm" download> <i class="fas fa-download"></i> </a> ${isEdit ? ` <button class="btn btn-danger btn-sm" onclick="deleteDocument(${doc.id})"> <i class="fas fa-trash"></i> </button> ` : ''} </div> </div> </div> `).join('')} </div> ` : ` <div class="text-center text-muted py-4"> <i class="fas fa-file fa-2x mb-3"></i> <p>Документы отсутствуют</p> </div> `} `; } function showUploadDocumentModal() { if (!currentComplex) return; document.getElementById('documentComplexId').value = currentComplex.id; document.getElementById('uploadDocumentForm').reset(); const modal = new bootstrap.Modal(document.getElementById('uploadDocumentModal')); modal.show(); } function uploadDocument() { const form = document.getElementById('uploadDocumentForm'); const formData = new FormData(form); const complexId = document.getElementById('documentComplexId').value; fetch(`/api/complexes/${complexId}/documents`, { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('uploadDocumentModal')); modal.hide(); showAlert('Документ загружен', 'success'); loadDocuments(); }) .catch(error => { console.error('Error uploading document:', error); showAlert('Ошибка загрузки документа: ' + error.message, 'danger'); }); } function deleteDocument(documentId) { if (!confirm('Удалить документ?')) return; fetch(`/api/documents/${documentId}`, { method: 'DELETE' }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Документ удален', 'success'); loadDocuments(); }) .catch(error => { console.error('Error deleting document:', error); showAlert('Ошибка удаления документа', 'danger'); }); } // Calibration Images functions function loadCalibrationImages() { if (!currentComplex) return; fetch(`/api/complexes/${currentComplex.id}/calibration-images`) .then(response => response.json()) .then(images => { renderCalibrationImages(images); }) .catch(error => { console.error('Error loading calibration images:', error); showAlert('Ошибка загрузки изображений калибровки', 'danger'); }); } function renderCalibrationImages(images) { const container = document.getElementById('calibrationImagesContent'); const isEdit = isEditMode; container.innerHTML = ` <div class="d-flex justify-content-between align-items-center mb-3"> <h6>Изображения калибровки</h6> ${isEdit ? ` <button class="btn btn-primary btn-sm" onclick="showUploadCalibrationImageModal()"> <i class="fas fa-upload"></i> Загрузить изображение </button> ` : ''} </div> ${images && images.length > 0 ? ` <div class="image-gallery"> ${images.map(image => ` <div class="image-item"> <img src="/api/calibration-images/${image.id}" alt="${image.image_name}" onclick="previewCalibrationImage(${image.id}, '${image.image_name}')"> ${isEdit ? ` <div class="image-actions"> <button class="btn btn-danger btn-sm" onclick="deleteCalibrationImage(${image.id}, event)" title="Удалить"> <i class="fas fa-trash"></i> </button> </div> ` : ''} <div class="image-info"> <div class="text-truncate" title="${image.image_name}">${image.image_name}</div> <small class="text-muted">${image.upload_date}</small> </div> </div> `).join('')} </div> ` : ` <div class="text-center text-muted py-4"> <i class="fas fa-ruler-combined fa-2x mb-3"></i> <p>Изображения калибровки отсутствуют</p> </div> `} `; } function showUploadCalibrationImageModal() { if (!currentComplex) return; document.getElementById('uploadCalibrationComplexId').value = currentComplex.id; document.getElementById('uploadCalibrationImageForm').reset(); const modal = new bootstrap.Modal(document.getElementById('uploadCalibrationImageModal')); modal.show(); } function uploadCalibrationImage() { const form = document.getElementById('uploadCalibrationImageForm'); const formData = new FormData(form); const complexId = document.getElementById('uploadCalibrationComplexId').value; fetch(`/api/complexes/${complexId}/calibration-images`, { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('uploadCalibrationImageModal')); modal.hide(); showAlert('Изображение калибровки загружено', 'success'); loadCalibrationImages(); }) .catch(error => { console.error('Error uploading calibration image:', error); showAlert('Ошибка загрузки изображения калибровки: ' + error.message, 'danger'); }); } function previewCalibrationImage(imageId, imageName) { document.getElementById('calibrationImagePreviewTitle').textContent = imageName; document.getElementById('previewCalibrationImage').src = `/api/calibration-images/${imageId}`; const modal = new bootstrap.Modal(document.getElementById('calibrationImagePreviewModal')); modal.show(); } function deleteCalibrationImage(imageId, event) { if (event) event.stopPropagation(); if (!confirm('Удалить изображение калибровки?')) return; fetch(`/api/calibration-images/${imageId}`, { method: 'DELETE' }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Изображение калибровки удалено', 'success'); loadCalibrationImages(); }) .catch(error => { console.error('Error deleting calibration image:', error); showAlert('Ошибка удаления изображения калибровки', 'danger'); }); } // Maintenance Documents functions function loadMaintenanceDocuments() { if (!currentComplex) return; fetch(`/api/complexes/${currentComplex.id}/maintenance-documents`) .then(response => response.json()) .then(documents => { renderMaintenanceDocuments(documents); }) .catch(error => { console.error('Error loading maintenance documents:', error); showAlert('Ошибка загрузки документов обслуживания', 'danger'); }); } function renderMaintenanceDocuments(documents) { const container = document.getElementById('maintenanceDocumentsContent'); const isEdit = isEditMode; container.innerHTML = ` <div class="d-flex justify-content-between align-items-center mb-3"> <h6>Документы обслуживания</h6> ${isEdit ? ` <button class="btn btn-primary btn-sm" onclick="showUploadMaintenanceDocumentModal()"> <i class="fas fa-upload"></i> Загрузить документ </button> ` : ''} </div> ${documents && documents.length > 0 ? ` <div class="documents-list"> ${documents.map(doc => ` <div class="document-item"> <div class="d-flex justify-content-between align-items-center"> <div class="d-flex align-items-center"> <div class="document-icon me-3"> <i class="fas fa-file-pdf text-danger"></i> </div> <div> <div class="fw-bold">${doc.document_name}</div> <small class="text-muted"> ${doc.document_type_ru || doc.document_type} • ${doc.size_formatted} • ${doc.upload_date} </small> </div> </div> <div class="btn-group"> <a href="/api/maintenance-documents/${doc.id}" class="btn btn-primary btn-sm" download> <i class="fas fa-download"></i> </a> ${isEdit ? ` <button class="btn btn-danger btn-sm" onclick="deleteMaintenanceDocument(${doc.id})"> <i class="fas fa-trash"></i> </button> ` : ''} </div> </div> </div> `).join('')} </div> ` : ` <div class="text-center text-muted py-4"> <i class="fas fa-file-contract fa-2x mb-3"></i> <p>Документы обслуживания отсутствуют</p> </div> `} `; } function showUploadMaintenanceDocumentModal() { if (!currentComplex) return; document.getElementById('maintenanceDocumentComplexId').value = currentComplex.id; document.getElementById('uploadMaintenanceDocumentForm').reset(); const modal = new bootstrap.Modal(document.getElementById('uploadMaintenanceDocumentModal')); modal.show(); } function uploadMaintenanceDocument() { const form = document.getElementById('uploadMaintenanceDocumentForm'); const formData = new FormData(form); const complexId = document.getElementById('maintenanceDocumentComplexId').value; fetch(`/api/complexes/${complexId}/maintenance-documents`, { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('uploadMaintenanceDocumentModal')); modal.hide(); showAlert('Документ обслуживания загружен', 'success'); loadMaintenanceDocuments(); }) .catch(error => { console.error('Error uploading maintenance document:', error); showAlert('Ошибка загрузки документа обслуживания: ' + error.message, 'danger'); }); } function deleteMaintenanceDocument(documentId) { if (!confirm('Удалить документ обслуживания?')) return; fetch(`/api/maintenance-documents/${documentId}`, { method: 'DELETE' }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Документ обслуживания удален', 'success'); loadMaintenanceDocuments(); }) .catch(error => { console.error('Error deleting maintenance document:', error); showAlert('Ошибка удаления документа обслуживания', 'danger'); }); } // Complex CRUD operations function showAddComplexModal() { document.getElementById('newComplexForm').reset(); const modal = new bootstrap.Modal(document.getElementById('addComplexModal')); modal.show(); } function createNewComplex() { const form = document.getElementById('newComplexForm'); const formData = new FormData(form); // Создаем объект данных из формы const data = {}; formData.forEach((value, key) => { // Пропускаем пустые значения для числовых полей if (value === '' && (key === 'install_year' || key === 'violation_sensors' || key === 'overview_sensors' || key === 'allowed_speed')) { // Для числовых полей оставляем пустым - сервер преобразует в NULL return; } // Для чекбоксов преобразуем 'on' в true, отсутствие - false if (value === 'on') { data[key] = true; } else if (value === '') { // Для строковых полей оставляем пустую строку data[key] = ''; } else { data[key] = value; } }); console.log('Sending complex data:', data); // Validate required fields if (!data.number || !data.location) { showAlert('Заполните обязательные поля (номер и адрес)', 'warning'); return; } fetch('/api/complexes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => { if (!response.ok) { return response.json().then(err => { throw new Error(err.error || `HTTP error! status: ${response.status}`); }); } return response.json(); }) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('addComplexModal')); modal.hide(); showAlert('Комплекс создан успешно!', 'success'); loadComplexes(); // Автоматически выбираем новый комплекс if (result.id) { setTimeout(() => { selectComplex(result.id); setEditMode(); }, 1000); } }) .catch(error => { console.error('Error creating complex:', error); showAlert('Ошибка создания комплекса: ' + error.message, 'danger'); }); } function saveComplex() { if (!currentComplex) return; const form = document.getElementById('infoEditMode'); const formData = new FormData(form); // Создаем объект данных из формы const data = {}; formData.forEach((value, key) => { // Обработка чекбоксов if (value === 'on') { data[key] = true; } else if (value === '') { // Для пустых строковых полей оставляем пустую строку data[key] = ''; } else { data[key] = value; } }); // Добавляем ID комплекса data.id = currentComplex.id; console.log('Saving complex data:', data); fetch('/api/complexes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => { if (!response.ok) { return response.json().then(err => { throw new Error(err.error || `HTTP error! status: ${response.status}`); }); } return response.json(); }) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Комплекс сохранен', 'success'); selectComplex(currentComplex.id); // Reload data }) .catch(error => { console.error('Error saving complex:', error); showAlert('Ошибка сохранения комплекса: ' + error.message, 'danger'); }); } function archiveCurrentComplex() { if (!currentComplex) return; const action = currentComplex.is_archived ? 'восстановить из архива' : 'архивировать'; if (!confirm(`Вы уверены, что хотите ${action} этот комплекс?`)) return; fetch(`/api/complexes/${currentComplex.id}/archive`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ archive: !currentComplex.is_archived }) }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert(result.message, 'success'); loadComplexes(); selectComplex(currentComplex.id); }) .catch(error => { console.error('Error archiving complex:', error); showAlert('Ошибка архивирования комплекса', 'danger'); }); } function deleteCurrentComplex() { if (!currentComplex) return; if (!confirm('Вы уверены, что хотите удалить этот комплекс? Это действие нельзя отменить.')) { return; } fetch(`/api/complexes/${currentComplex.id}`, { method: 'DELETE' }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Комплекс удален', 'success'); currentComplex = null; document.getElementById('complexDetails').style.display = 'none'; document.getElementById('noComplexSelected').style.display = 'block'; document.getElementById('complexActions').style.display = 'none'; loadComplexes(); }) .catch(error => { console.error('Error deleting complex:', error); showAlert('Ошибка удаления комплекса', 'danger'); }); } // Expiring verifications function showExpiringVerifications() { fetch('/api/expiring-verifications') .then(response => response.json()) .then(complexes => { renderExpiringVerifications(complexes); }) .catch(error => { console.error('Error loading expiring verifications:', error); showAlert('Ошибка загрузки данных', 'danger'); }); const modal = new bootstrap.Modal(document.getElementById('expiringVerificationsModal')); modal.show(); } function renderExpiringVerifications(complexes) { const container = document.getElementById('expiringVerificationsContent'); if (!complexes || complexes.length === 0) { container.innerHTML = ` <div class="text-center text-success py-4"> <i class="fas fa-check-circle fa-2x mb-3"></i> <p>Нет комплексов с истекающей поверкой</p> </div> `; return; } container.innerHTML = ` <div class="alert alert-warning"> <i class="fas fa-exclamation-triangle"></i> Найдено ${complexes.length} комплексов с истекающей поверкой </div> <div class="table-responsive"> <table class="table table-striped"> <thead> <tr> <th>Номер</th> <th>Название</th> <th>Адрес</th> <th>Поверка действительна до</th> <th>Осталось дней</th> <th>Действия</th> </tr> </thead> <tbody> ${complexes.map(complex => { const daysLeft = complex.days_left; let rowClass = ''; if (daysLeft <= 7) rowClass = 'expiring-urgent'; else if (daysLeft <= 30) rowClass = 'expiring-soon'; else rowClass = 'expiring-item'; return ` <tr class="${rowClass}"> <td>${complex.number || 'Без номера'}</td> <td>${complex.complex_name || 'Не указано'}</td> <td>${complex.location || 'Не указан'}</td> <td>${complex.verification_valid_until}</td> <td> <span class="badge ${getDaysLeftBadgeClass(daysLeft)}"> ${daysLeft} дн. </span> </td> <td> <button class="btn btn-primary btn-sm" onclick="selectComplexFromExpiring(${complex.id})"> <i class="fas fa-external-link-alt"></i> Перейти </button> </td> </tr> `; }).join('')} </tbody> </table> </div> `; } function selectComplexFromExpiring(complexId) { const modal = bootstrap.Modal.getInstance(document.getElementById('expiringVerificationsModal')); modal.hide(); selectComplex(complexId); } // Utility functions function getVerificationStatusClass(validUntil) { if (!validUntil) return ''; const validDate = parseDateString(validUntil); if (!validDate) return ''; const today = new Date(); const diffTime = validDate - today; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays <= 0) return 'text-danger'; if (diffDays <= 30) return 'text-warning'; return ''; } function getVerificationStatusBadge(validUntil) { if (!validUntil) return ''; const validDate = parseDateString(validUntil); if (!validDate) return ''; const today = new Date(); const diffTime = validDate - today; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays <= 0) { return '<span class="badge bg-danger ms-1">ПРОСРОЧЕНО</span>'; } else if (diffDays <= 7) { return '<span class="badge bg-danger ms-1">СКОРО ИСТЕКАЕТ</span>'; } else if (diffDays <= 30) { return '<span class="badge bg-warning ms-1">ВНИМАНИЕ</span>'; } return ''; } function getDaysLeftBadgeClass(daysLeft) { if (daysLeft <= 0) return 'bg-danger'; if (daysLeft <= 7) return 'bg-danger'; if (daysLeft <= 30) return 'bg-warning'; return 'bg-info'; } function parseDateString(dateStr) { if (!dateStr) return null; const parts = dateStr.split('.'); if (parts.length !== 3) return null; return new Date(parts[2], parts[1] - 1, parts[0]); } function showAlert(message, type) { const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type} alert-dismissible fade show`; alertDiv.innerHTML = ` ${message} <button type="button" class="btn-close" data-bs-dismiss="alert"></button> `; // Remove existing alerts document.querySelectorAll('.alert').forEach(alert => alert.remove()); // Add new alert document.querySelector('.container-fluid').insertBefore(alertDiv, document.querySelector('.container-fluid').firstChild); // Auto remove after 5 seconds setTimeout(() => { if (alertDiv.parentElement) { alertDiv.remove(); } }, 5000); } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Периодическая проверка и скрытие кнопки редактирования для user setInterval(function() { hideEditButtonForUser(); }, 2000); // Maintenance Documents functions with date grouping and multiple upload let currentDocumentDate = null; function loadMaintenanceDocuments() { if (!currentComplex) return; fetch(`/api/complexes/${currentComplex.id}/maintenance-documents-with-dates`) .then(response => response.json()) .then(documentsByDate => { renderMaintenanceDocuments(documentsByDate); }) .catch(error => { console.error('Error loading maintenance documents:', error); showAlert('Ошибка загрузки документов обслуживания', 'danger'); }); } function renderMaintenanceDocuments(documentsByDate) { const container = document.getElementById('maintenanceDocumentsContent'); const isEdit = isEditMode; container.innerHTML = ` <div class="d-flex justify-content-between align-items-center mb-3"> <h6>Документы обслуживания</h6> ${isEdit ? ` <button class="btn btn-primary btn-sm" onclick="showUploadMaintenanceDocumentModal()"> <i class="fas fa-upload"></i> Загрузить документы </button> ` : ''} </div> ${documentsByDate && documentsByDate.length > 0 ? ` <div class="documents-by-date"> ${documentsByDate.map(dateGroup => ` <div class="date-group mb-3"> <div class="date-header bg-light p-3 rounded" onclick="toggleDateGroup('${dateGroup.date_key}')" style="cursor: pointer;"> <h6 class="mb-0 d-flex align-items-center"> <i class="fas fa-calendar-alt text-primary me-2"></i> <span class="flex-grow-1"> Дата работ: ${dateGroup.date_display} <span class="badge bg-secondary ms-2">${dateGroup.documents.length} док.</span> </span> <i class="fas fa-chevron-right text-muted" id="chevron-${dateGroup.date_key}"></i> </h6> </div> <div class="documents-list mt-2" id="date-group-${dateGroup.date_key}" style="display: none;"> ${dateGroup.documents.map(doc => ` <div class="document-item"> <div class="d-flex justify-content-between align-items-center"> <div class="d-flex align-items-center"> <div class="document-icon me-3"> <i class="${getDocumentIcon(doc.document_name)} text-danger"></i> </div> <div> <div class="fw-bold">${doc.document_name}</div> <small class="text-muted"> ${doc.document_type_ru || doc.document_type} • ${doc.size_formatted} • ${doc.upload_date} </small> </div> </div> <div class="btn-group"> <a href="/api/maintenance-documents/${doc.id}" class="btn btn-primary btn-sm" download> <i class="fas fa-download"></i> </a> ${isEdit ? ` <button class="btn btn-danger btn-sm" onclick="deleteMaintenanceDocument(${doc.id})"> <i class="fas fa-trash"></i> </button> ` : ''} </div> </div> </div> `).join('')} </div> </div> `).join('')} </div> ` : ` <div class="text-center text-muted py-4"> <i class="fas fa-file-contract fa-2x mb-3"></i> <p>Документы обслуживания отсутствуют</p> </div> `} `; } function getDocumentIcon(filename) { const ext = filename.split('.').pop().toLowerCase(); if (['pdf'].includes(ext)) return 'fas fa-file-pdf'; if (['doc', 'docx'].includes(ext)) return 'fas fa-file-word'; if (['xls', 'xlsx'].includes(ext)) return 'fas fa-file-excel'; if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(ext)) return 'fas fa-file-image'; return 'fas fa-file'; } function toggleDateGroup(dateKey) { const group = document.getElementById(`date-group-${dateKey}`); const chevron = document.getElementById(`chevron-${dateKey}`); if (group.style.display === 'none') { // Разворачиваем group.style.display = 'block'; chevron.className = 'fas fa-chevron-down text-muted'; } else { // Сворачиваем group.style.display = 'none'; chevron.className = 'fas fa-chevron-right text-muted'; } } function showUploadMaintenanceDocumentModal() { if (!currentComplex) return; document.getElementById('maintenanceDocumentComplexId').value = currentComplex.id; document.getElementById('uploadMaintenanceDocumentForm').reset(); document.getElementById('workDate').value = ''; document.getElementById('fileList').innerHTML = ''; const modal = new bootstrap.Modal(document.getElementById('uploadMaintenanceDocumentModal')); modal.show(); } function handleFileSelect(event) { const files = event.target.files; const fileList = document.getElementById('fileList'); fileList.innerHTML = ''; if (files.length > 0) { fileList.innerHTML = `<h6>Выбранные файлы (${files.length}):</h6>`; for (let i = 0; i < files.length; i++) { const file = files[i]; const fileItem = document.createElement('div'); fileItem.className = 'file-item d-flex justify-content-between align-items-center p-2 border-bottom'; fileItem.innerHTML = ` <div> <i class="${getDocumentIcon(file.name)} me-2"></i> <span>${file.name}</span> </div> <small class="text-muted">${formatFileSize(file.size)}</small> `; fileList.appendChild(fileItem); } } } function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function uploadMaintenanceDocument() { const form = document.getElementById('uploadMaintenanceDocumentForm'); const formData = new FormData(form); const complexId = document.getElementById('maintenanceDocumentComplexId').value; const files = document.getElementById('maintenanceDocuments').files; if (files.length === 0) { showAlert('Выберите файлы для загрузки', 'warning'); return; } // Добавляем все файлы в FormData for (let i = 0; i < files.length; i++) { formData.append('documents[]', files[i]); } // Показываем индикатор загрузки const uploadBtn = document.querySelector('#uploadMaintenanceDocumentModal .btn-primary'); const originalText = uploadBtn.innerHTML; uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Загрузка...'; uploadBtn.disabled = true; fetch(`/api/complexes/${complexId}/maintenance-documents`, { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { const modal = bootstrap.Modal.getInstance(document.getElementById('uploadMaintenanceDocumentModal')); modal.hide(); if (result.error) { throw new Error(result.error); } showAlert(result.message, 'success'); // Показываем детали загрузки если есть if (result.uploaded_files && result.uploaded_files.length > 0) { let details = `Успешно загружены: ${result.uploaded_files.join(', ')}`; if (result.failed_files && result.failed_files.length > 0) { details += `<br>Ошибки: ${result.failed_files.map(f => f.name).join(', ')}`; } setTimeout(() => { showAlert(details, 'info'); }, 1000); } loadMaintenanceDocuments(); }) .catch(error => { console.error('Error uploading maintenance documents:', error); showAlert('Ошибка загрузки документов: ' + error.message, 'danger'); }) .finally(() => { // Восстанавливаем кнопку uploadBtn.innerHTML = originalText; uploadBtn.disabled = false; }); } function deleteMaintenanceDocument(documentId) { if (!confirm('Удалить документ обслуживания?')) return; fetch(`/api/maintenance-documents/${documentId}`, { method: 'DELETE' }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Документ обслуживания удален', 'success'); loadMaintenanceDocuments(); }) .catch(error => { console.error('Error deleting maintenance document:', error); showAlert('Ошибка удаления документа обслуживания', 'danger'); }); } // Функция загрузки фото обслуживания function loadMaintenancePhotos() { if (!currentComplex) return; console.log('Loading maintenance photos for complex:', currentComplex.id); fetch(`/api/complexes/${currentComplex.id}/maintenance-photos`) .then(response => { console.log('Maintenance photos response status:', response.status); return response.json(); }) .then(photos => { console.log('Maintenance photos received:', photos); renderMaintenancePhotos(photos); }) .catch(error => { console.error('Error loading maintenance photos:', error); showAlert('Ошибка загрузки фото обслуживания', 'danger'); }); } function renderMaintenancePhotos(photos) { const container = document.getElementById('maintenancePhotosContent'); const isEdit = isEditMode; console.log('Rendering maintenance photos:', photos); // Группируем фото по датам const photosByDate = groupPhotosByDate(photos); // Правильно сортируем даты - самая свежая сверху const dateKeys = Object.keys(photosByDate).sort((a, b) => { // Преобразуем даты в формате "YYYY-MM-DD" для корректной сортировки const dateA = new Date(a.split('.').reverse().join('-')); const dateB = new Date(b.split('.').reverse().join('-')); return dateB - dateA; // Сортировка по убыванию (свежие сверху) }); console.log('Sorted date keys:', dateKeys); container.innerHTML = ` <div class="d-flex justify-content-between align-items-center mb-3"> <h6>Фото обслуживания</h6> ${isEdit ? ` <div class="btn-group"> <button class="btn btn-primary btn-sm" onclick="showUploadMaintenancePhotoModal()"> <i class="fas fa-upload"></i> Загрузить фото </button> <button class="btn btn-success btn-sm" onclick="showBulkUploadMaintenancePhotosModal()"> <i class="fas fa-folder-open"></i> Из сетевой папки </button> </div> ` : ''} </div> ${photos && photos.length > 0 ? ` <div class="photos-by-date"> ${dateKeys.map(dateKey => { const dateGroup = photosByDate[dateKey]; return ` <div class="date-group"> <div class="date-header bg-light p-3 rounded" onclick="toggleMaintenanceDateGroup('${dateKey}')" style="cursor: pointer;"> <h6 class="mb-0 d-flex align-items-center"> <i class="fas fa-calendar-day text-primary me-2"></i> <span class="flex-grow-1"> Дата работ: ${dateGroup.dateDisplay} <span class="badge bg-secondary ms-2">${dateGroup.photos.length} фото</span> </span> <i class="fas fa-chevron-right text-muted" id="chevron-${dateKey}"></i> </h6> </div> <div class="image-gallery mt-2" id="date-group-${dateKey}" style="display: none !important;"> ${dateGroup.photos.map(photo => ` <div class="image-item"> <div class="image-container"> <img src="/api/maintenance-photos/${photo.id}" alt="${photo.photo_name}" onclick="previewMaintenancePhoto(${photo.id}, '${photo.photo_name}')"> </div> ${isEdit ? ` <div class="image-actions"> <button class="btn btn-danger btn-sm" onclick="deleteMaintenancePhoto(${photo.id}, event)" title="Удалить"> <i class="fas fa-trash"></i> </button> </div> ` : ''} </div> `).join('')} </div> </div> `; }).join('')} </div> ` : ` <div class="empty-state text-center text-muted py-4"> <i class="fas fa-camera fa-2x mb-3"></i> <p>Фото обслуживания отсутствуют</p> ${isEdit ? ` <p class="small">Используйте кнопки выше для добавления фото</p> ` : ''} </div> `} `; // Принудительно применяем стили после рендеринга setTimeout(() => { forceCollapseAllGalleries(); applyGalleryStyles(); }, 50); } // СИЛЬНАЯ функция для принудительного свёртывания всех галерей function forceCollapseAllGalleries() { console.log('=== FORCE COLLAPSING ALL GALLERIES ==='); const galleries = document.querySelectorAll('#maintenancePhotosContent .image-gallery'); console.log('Found galleries to collapse:', galleries.length); galleries.forEach((gallery, index) => { console.log(`Collapsing gallery ${index + 1}:`, gallery.id); // Множественные способы скрытия gallery.style.display = 'none'; gallery.style.visibility = 'hidden'; gallery.style.opacity = '0'; gallery.style.height = '0'; gallery.style.overflow = 'hidden'; gallery.setAttribute('data-collapsed', 'true'); // Добавляем важный стиль gallery.style.setProperty('display', 'none', 'important'); }); // Принудительно устанавливаем правильные иконки шевронов const chevrons = document.querySelectorAll('#maintenancePhotosContent .date-header i.fas'); chevrons.forEach((chevron, index) => { console.log(`Setting chevron ${index + 1} to right:`, chevron.id); chevron.className = 'fas fa-chevron-right text-muted'; chevron.style.transform = 'none'; }); console.log('=== ALL GALLERIES SHOULD BE COLLAPSED NOW ==='); } // Функция для принудительного применения стилей галереи function applyGalleryStyles() { const galleries = document.querySelectorAll('#maintenancePhotosContent .image-gallery'); console.log('Applying gallery styles to:', galleries.length, 'galleries'); galleries.forEach((gallery, index) => { // Устанавливаем grid стили, но оставляем display: none gallery.style.gridTemplateColumns = 'repeat(auto-fill, minmax(150px, 1fr))'; gallery.style.gap = '1rem'; gallery.style.padding = '1rem'; gallery.style.background = 'rgba(255, 255, 255, 0.5)'; gallery.style.borderRadius = '12px'; gallery.style.marginTop = '1rem'; gallery.style.width = '100%'; // Применяем стили к элементам галереи const items = gallery.querySelectorAll('.image-item'); items.forEach(item => { item.style.position = 'relative'; item.style.border = 'none'; item.style.borderRadius = '12px'; item.style.overflow = 'hidden'; item.style.background = 'white'; item.style.boxShadow = '0 3px 10px rgba(0, 0, 0, 0.1)'; item.style.transition = 'all 0.3s ease'; item.style.display = 'flex'; item.style.flexDirection = 'column'; item.style.height = 'fit-content'; item.style.minHeight = '120px'; }); }); } // Функция для гарантированного свёртывания всех галерей function ensureGalleriesCollapsed() { const galleries = document.querySelectorAll('#maintenancePhotosContent .image-gallery'); console.log('Ensuring galleries are collapsed. Count:', galleries.length); galleries.forEach(gallery => { gallery.style.display = 'none'; }); const chevrons = document.querySelectorAll('#maintenancePhotosContent .date-header i.fas'); chevrons.forEach(chevron => { // Устанавливаем иконку "вправо" для свёрнутого состояния if (chevron.classList.contains('fa-chevron-down')) { chevron.className = 'fas fa-chevron-right text-muted'; } }); console.log('All galleries should be collapsed now'); } // Функция группировки фото по датам function groupPhotosByDate(photos) { const grouped = {}; if (!photos || !Array.isArray(photos)) { return grouped; } photos.forEach(photo => { // Используем work_date если есть, иначе upload_date const date = photo.work_date || photo.upload_date; if (!date) return; // Извлекаем только дату (без времени) const dateKey = date.split(' ')[0]; // Берем только часть до пробела // Создаем объект Date для корректного форматирования let dateObj; try { // Пробуем разные форматы дат if (dateKey.includes('.')) { // Формат DD.MM.YYYY const [day, month, year] = dateKey.split('.'); dateObj = new Date(year, month - 1, day); } else if (dateKey.includes('-')) { // Формат YYYY-MM-DD dateObj = new Date(dateKey); } else { console.warn('Unknown date format:', dateKey); return; } // Проверяем валидность даты if (isNaN(dateObj.getTime())) { console.warn('Invalid date:', dateKey); return; } const dateDisplay = dateObj.toLocaleDateString('ru-RU'); if (!grouped[dateKey]) { grouped[dateKey] = { dateDisplay: dateDisplay, dateObject: dateObj, // Сохраняем объект даты для сортировки photos: [] }; } grouped[dateKey].photos.push(photo); } catch (error) { console.error('Error processing date:', dateKey, error); } }); return grouped; } // Улучшенная функция переключения с отладкой function toggleMaintenanceDateGroup(dateKey) { const group = document.getElementById(`date-group-${dateKey}`); const chevron = document.getElementById(`chevron-${dateKey}`); console.log('=== TOGGLE GROUP:', dateKey, '==='); console.log('Current display:', group.style.display); console.log('Current computed display:', window.getComputedStyle(group).display); console.log('Current class:', chevron.className); const isCurrentlyHidden = group.style.display === 'none' || window.getComputedStyle(group).display === 'none' || group.getAttribute('data-collapsed') === 'true'; if (isCurrentlyHidden) { // Разворачиваем console.log('EXPANDING group'); group.style.display = 'grid'; group.style.visibility = 'visible'; group.style.opacity = '1'; group.style.height = 'auto'; group.style.overflow = 'visible'; group.setAttribute('data-collapsed', 'false'); chevron.className = 'fas fa-chevron-down text-muted'; } else { // Сворачиваем console.log('COLLAPSING group'); group.style.display = 'none'; group.style.visibility = 'hidden'; group.style.opacity = '0'; group.style.height = '0'; group.style.overflow = 'hidden'; group.setAttribute('data-collapsed', 'true'); chevron.className = 'fas fa-chevron-right text-muted'; } console.log('New display:', group.style.display); console.log('New class:', chevron.className); console.log('=== TOGGLE COMPLETE ==='); } // Добавим функцию для отладки состояния галерей function debugGalleries() { console.log('=== DEBUG GALLERIES ==='); const galleries = document.querySelectorAll('#maintenancePhotosContent .image-gallery'); galleries.forEach((gallery, index) => { const computedStyle = window.getComputedStyle(gallery); console.log(`Gallery ${index + 1} (${gallery.id}):`); console.log(' - style.display:', gallery.style.display); console.log(' - computed.display:', computedStyle.display); console.log(' - data-collapsed:', gallery.getAttribute('data-collapsed')); console.log(' - visibility:', computedStyle.visibility); console.log(' - opacity:', computedStyle.opacity); }); const chevrons = document.querySelectorAll('#maintenancePhotosContent .date-header i.fas'); chevrons.forEach((chevron, index) => { console.log(`Chevron ${index + 1} (${chevron.id}):`, chevron.className); }); console.log('=== DEBUG COMPLETE ==='); } // Вызовем отладку после загрузки (можно убрать позже) setTimeout(debugGalleries, 1000); // Стандартная загрузка фото function showUploadMaintenancePhotoModal() { if (!currentComplex) return; document.getElementById('maintenancePhotoComplexId').value = currentComplex.id; document.getElementById('uploadMaintenancePhotoForm').reset(); const modal = new bootstrap.Modal(document.getElementById('uploadMaintenancePhotoModal')); modal.show(); } function uploadMaintenancePhoto() { const form = document.getElementById('uploadMaintenancePhotoForm'); const formData = new FormData(form); const complexId = document.getElementById('maintenancePhotoComplexId').value; fetch(`/api/complexes/${complexId}/maintenance-photos`, { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } const modal = bootstrap.Modal.getInstance(document.getElementById('uploadMaintenancePhotoModal')); modal.hide(); showAlert('Фото обслуживания загружено', 'success'); loadMaintenancePhotos(); }) .catch(error => { console.error('Error uploading maintenance photo:', error); showAlert('Ошибка загрузки фото: ' + error.message, 'danger'); }); } // Глобальная загрузка фото function showBulkUploadMaintenancePhotosModal() { // Устанавливаем сегодняшнюю дату по умолчанию const today = new Date().toISOString().split('T')[0]; document.getElementById('maintenancePhotosDate').value = today; document.getElementById('bulkUploadResults').style.display = 'none'; document.getElementById('bulkUploadResultsContent').innerHTML = ''; const modal = new bootstrap.Modal(document.getElementById('bulkUploadMaintenancePhotosModal')); modal.show(); } function uploadMaintenancePhotos() { const selectedDate = document.getElementById('maintenancePhotosDate').value; if (!selectedDate) { showAlert('Выберите дату', 'warning'); return; } const resultsDiv = document.getElementById('bulkUploadResultsContent'); resultsDiv.innerHTML = ` <div class="text-center"> <div class="loading-spinner"></div> <p class="mt-2">Поиск и загрузка фото...</p> </div> `; document.getElementById('bulkUploadResults').style.display = 'block'; fetch('/api/maintenance-photos/bulk-upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ selected_date: selectedDate }) }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } displayBulkUploadResults(result); }) .catch(error => { console.error('Error uploading maintenance photos:', error); resultsDiv.innerHTML = ` <div class="alert alert-danger"> <i class="fas fa-exclamation-triangle"></i> Ошибка: ${error.message} </div> `; }); } function displayBulkUploadResults(result) { const container = document.getElementById('bulkUploadResultsContent'); let html = ` <div class="alert alert-${result.results.failed === 0 ? 'success' : 'warning'}"> <strong>Обработано папок: ${result.results.total_processed}</strong><br> Успешно: <span class="text-success">${result.results.successful}</span> | Ошибки: <span class="text-danger">${result.results.failed}</span> </div> <div class="mb-3"> <strong>Путь поиска:</strong><br> <code class="small">${result.target_folder}</code> </div> `; if (result.results.details && result.results.details.length > 0) { html += '<div class="small">'; result.results.details.forEach(detail => { const badgeClass = detail.status === 'success' ? 'bg-success' : detail.status === 'error' ? 'bg-danger' : 'bg-warning'; const icon = detail.status === 'success' ? 'fa-check' : detail.status === 'error' ? 'fa-times' : 'fa-exclamation-triangle'; html += ` <div class="d-flex align-items-start mb-2 p-2 border-bottom"> <span class="badge ${badgeClass} me-2 mt-1"> <i class="fas ${icon}"></i> </span> <div class="flex-grow-1"> <strong>${detail.folder}</strong> ${detail.complex_number ? `<br><small>Комплекс: ${detail.complex_number}</small>` : ''} <br><small class="text-muted">${detail.message}</small> ${detail.photo ? `<br><small class="text-muted">Файл: ${detail.photo}</small>` : ''} </div> </div> `; }); html += '</div>'; } container.innerHTML = html; // Прокручиваем к результатам container.scrollTop = 0; // Обновляем список фото если есть успешные загрузки if (result.results.successful > 0 && currentComplex) { setTimeout(() => { loadMaintenancePhotos(); }, 1000); } } // Функция предпросмотра фото обслуживания function previewMaintenancePhoto(photoId, photoName) { console.log('Previewing maintenance photo:', photoId, photoName); // Устанавливаем название фото в заголовок модального окна document.getElementById('maintenancePhotoPreviewTitle').textContent = photoName; // Показываем индикатор загрузки const previewImg = document.getElementById('previewMaintenancePhoto'); previewImg.src = ''; previewImg.alt = 'Загрузка...'; // Загружаем фото const img = new Image(); img.onload = function() { previewImg.src = `/api/maintenance-photos/${photoId}`; previewImg.alt = photoName; }; img.onerror = function() { console.error('Error loading maintenance photo:', photoId); previewImg.alt = 'Ошибка загрузки изображения'; showAlert('Ошибка загрузки фото', 'danger'); }; img.src = `/api/maintenance-photos/${photoId}`; const modal = new bootstrap.Modal(document.getElementById('maintenancePhotoPreviewModal')); modal.show(); } // Функция удаления фото обслуживания function deleteMaintenancePhoto(photoId, event) { if (event) event.stopPropagation(); if (!confirm('Удалить фото обслуживания?')) return; fetch(`/api/maintenance-photos/${photoId}`, { method: 'DELETE' }) .then(response => response.json()) .then(result => { if (result.error) { throw new Error(result.error); } showAlert('Фото обслуживания удалено', 'success'); loadMaintenancePhotos(); }) .catch(error => { console.error('Error deleting maintenance photo:', error); showAlert('Ошибка удаления фото обслуживания', 'danger'); }); } // Export for global access window.selectComplex = selectComplex; window.setViewMode = setViewMode; window.setEditMode = setEditMode; window.showAddComplexModal = showAddComplexModal; window.createNewComplex = createNewComplex; window.saveComplex = saveComplex; window.archiveCurrentComplex = archiveCurrentComplex; window.deleteCurrentComplex = deleteCurrentComplex; window.showExpiringVerifications = showExpiringVerifications; window.selectComplexFromExpiring = selectComplexFromExpiring; window.showAddMaintenanceModal = showAddMaintenanceModal; window.saveMaintenance = saveMaintenance; window.editMaintenance = editMaintenance; window.updateMaintenance = updateMaintenance; window.deleteMaintenance = deleteMaintenance; window.showBulkMaintenanceModal = showBulkMaintenanceModal; window.uploadBulkMaintenance = uploadBulkMaintenance; window.showUploadImageModal = showUploadImageModal; window.uploadImage = uploadImage; window.previewImage = previewImage; window.deleteImage = deleteImage; window.showUploadDocumentModal = showUploadDocumentModal; window.uploadDocument = uploadDocument; window.deleteDocument = deleteDocument; window.showMapPreview = showMapPreview; window.openYandexMaps = openYandexMaps; window.openGoogleMaps = openGoogleMaps; window.loadComplexes = loadComplexes; window.getCheckboxState = getCheckboxState; window.formatPDDValue = formatPDDValue; window.filterMaintenanceByType = filterMaintenanceByType; window.clearMaintenanceFilter = clearMaintenanceFilter; window.exportMaintenance = exportMaintenance; window.exportAllMaintenance = exportAllMaintenance; window.loadCalibrationImages = loadCalibrationImages; window.showUploadCalibrationImageModal = showUploadCalibrationImageModal; window.uploadCalibrationImage = uploadCalibrationImage; window.previewCalibrationImage = previewCalibrationImage; window.deleteCalibrationImage = deleteCalibrationImage; window.loadMaintenanceDocuments = loadMaintenanceDocuments; window.showUploadMaintenanceDocumentModal = showUploadMaintenanceDocumentModal; window.uploadMaintenanceDocument = uploadMaintenanceDocument; window.deleteMaintenanceDocument = deleteMaintenanceDocument; window.logout = logout; window.showUsersModal = showUsersModal; window.createUser = createUser; window.editUser = editUser; window.updateUser = updateUser; window.deleteUser = deleteUser; window.loadMaintenancePhotos = loadMaintenancePhotos; window.showBulkUploadMaintenancePhotosModal = showBulkUploadMaintenancePhotosModal; window.uploadMaintenancePhotos = uploadMaintenancePhotos; window.previewMaintenancePhoto = previewMaintenancePhoto; window.deleteMaintenancePhoto = deleteMaintenancePhoto; window.loadMaintenancePhotos = loadMaintenancePhotos; window.showBulkUploadMaintenancePhotosModal = showBulkUploadMaintenancePhotosModal; window.uploadMaintenancePhotos = uploadMaintenancePhotos; window.previewMaintenancePhoto = previewMaintenancePhoto; window.deleteMaintenancePhoto = deleteMaintenancePhoto; window.toggleMaintenanceDateGroup = toggleMaintenanceDateGroup;