/** * =============================================================================== * FILE: modules/web_api/static/js/app.js * ROLE: Главная точка входа UI: динамическая загрузка модальных окон, * маршрутизация авторизации, управление профилем и глобальный стейт. * =============================================================================== */ const AUTH_TOKEN_KEY = "scud_api_auth_token"; const SESSION_ID = "web_session_main"; const STORAGE_KEY = "scud_chat_input_history"; let API_TOKEN = localStorage.getItem(AUTH_TOKEN_KEY) || ""; let CURRENT_USERNAME = localStorage.getItem("scud_username") || ""; let IS_ADMIN = localStorage.getItem("scud_is_admin") === "true"; let IS_GUEST = localStorage.getItem("scud_is_guest") === "true"; let inputHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); let historyIndex = -1; // ============================================================================ // 1. ДИНАМИЧЕСКАЯ ЗАГРУЗКА МОДАЛЬНЫХ ОКОН // ============================================================================ async function loadModals() { const modalFiles = [ 'remote_worker_modal.html', 'manual_absence_modal.html', 'exception_modal.html', 'auth_modal.html', 'profile_modal.html', 'admin_modal.html' ]; const container = document.getElementById('modals-container'); if (!container) return; for (const file of modalFiles) { try { const res = await fetch(`/static/modals/${file}?v=2.5.8`); if (res.ok) { const html = await res.text(); container.insertAdjacentHTML('beforeend', html); } } catch (e) { console.error(`[Modals] Ошибка загрузки ${file}:`, e); } } } // ============================================================================ // 2. УПРАВЛЕНИЕ АВТОРИЗАЦИЕЙ И ПРОФИЛЕМ В UI // ============================================================================ function showAuthModal() { const modal = document.getElementById("auth-modal"); if (modal) modal.classList.remove("hidden"); } function hideAuthModal() { const modal = document.getElementById("auth-modal"); if (modal) modal.classList.add("hidden"); } function updateUIState() { const nameEl = document.getElementById("user-display-name"); const roleEl = document.getElementById("user-display-role"); const adminBtn = document.getElementById("admin-panel-btn"); if (nameEl) nameEl.innerText = AuthManager.getFullName(); if (roleEl) roleEl.innerText = AuthManager.isAdmin() ? "Администратор" : "Оператор"; if (adminBtn) { if (AuthManager.isAdmin()) { adminBtn.classList.remove("hidden"); } else { adminBtn.classList.add("hidden"); } } } async function handleLoginSubmit(e) { e.preventDefault(); const uInput = document.getElementById("auth-username"); const pInput = document.getElementById("auth-password"); const errEl = document.getElementById("auth-error"); if (errEl) errEl.classList.add("hidden"); try { const res = await fetch("/api/v1/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: uInput.value.trim(), password: pInput.value }) }); if (res.ok) { const data = await res.json(); AuthManager.setSession(data.token, data.username, data.full_name, data.is_admin, data.user_id); hideAuthModal(); updateUIState(); if (window.SidebarManager) SidebarManager.setHub('TASKS'); } else { const err = await res.json(); if (errEl) { errEl.innerText = err.detail || "Неверный логин или пароль"; errEl.classList.remove("hidden"); } } } catch (err) { if (errEl) { errEl.innerText = "Ошибка соединения с сервером"; errEl.classList.remove("hidden"); } } } function openProfileModal() { const modal = document.getElementById("profile-modal"); if (modal) modal.classList.remove("hidden"); } function closeProfileModal() { const modal = document.getElementById("profile-modal"); if (modal) modal.classList.add("hidden"); } async function handleChangePassword(e) { e.preventDefault(); const oldP = document.getElementById("old-pass").value; const newP = document.getElementById("new-pass").value; const errEl = document.getElementById("pass-error"); if (errEl) errEl.classList.add("hidden"); try { const res = await fetch("/api/v1/auth/change-password", { method: "POST", headers: AuthManager.getAuthHeaders(), body: JSON.stringify({ old_password: oldP, new_password: newP }) }); if (res.ok) { alert("Пароль успешно изменен"); closeProfileModal(); } else { const err = await res.json(); if (errEl) { errEl.innerText = err.detail || "Ошибка изменения пароля"; errEl.classList.remove("hidden"); } } } catch (e) { if (errEl) { errEl.innerText = "Ошибка сети"; errEl.classList.remove("hidden"); } } } // ============================================================================ // 3. АДМИНИСТРИРОВАНИЕ ПОЛЬЗОВАТЕЛЕЙ // ============================================================================ function openAdminModal() { const modal = document.getElementById("admin-modal"); if (modal) { modal.classList.remove("hidden"); loadAdminUsers(); } } function closeAdminModal() { const modal = document.getElementById("admin-modal"); if (modal) modal.classList.add("hidden"); } async function loadAdminUsers() { const listEl = document.getElementById("admin-users-list"); if (!listEl) return; listEl.innerHTML = `
Загрузка...
`; try { const res = await fetch("/api/v1/admin/users", { headers: AuthManager.getAuthHeaders() }); if (res.ok) { const users = await res.json(); listEl.innerHTML = users.map(u => `
${escapeHtml(u.full_name || u.username)} (${escapeHtml(u.username)})
${u.is_admin ? 'Администратор' : 'Оператор'}
`).join(''); } else { listEl.innerHTML = `
Ошибка загрузки пользователей
`; } } catch (e) { listEl.innerHTML = `
Ошибка сети
`; } } async function handleCreateUser(e) { e.preventDefault(); const u = document.getElementById("new-user-username").value; const p = document.getElementById("new-user-password").value; const f = document.getElementById("new-user-fullname").value; const a = document.getElementById("new-user-admin").checked; try { const res = await fetch("/api/v1/admin/users", { method: "POST", headers: AuthManager.getAuthHeaders(), body: JSON.stringify({ username: u, password: p, full_name: f, is_admin: a }) }); if (res.ok) { document.getElementById("new-user-username").value = ""; document.getElementById("new-user-password").value = ""; document.getElementById("new-user-fullname").value = ""; document.getElementById("new-user-admin").checked = false; loadAdminUsers(); } else { const err = await res.json(); alert(err.detail || "Ошибка создания пользователя"); } } catch (e) { alert("Ошибка сети"); } } async function deleteAdminUser(id, username) { if (!confirm(`Удалить пользователя ${username}?`)) return; try { const res = await fetch(`/api/v1/admin/users/${id}`, { method: "DELETE", headers: AuthManager.getAuthHeaders() }); if (res.ok) { loadAdminUsers(); } else { const err = await res.json(); alert(err.detail || "Ошибка удаления"); } } catch (e) { alert("Ошибка сети"); } } // ============================================================================ // 4. ВСПОМОГАТЕЛЬНЫЕ ФОРМАТЕРЫ И МОДАЛКА УДАЛЕНЩИКОВ // ============================================================================ function dmyToYmd(str) { if (!str) return ""; const parts = str.replace(/_/g, '.').split('.'); if (parts.length === 3) return `${parts[2]}-${parts[1].padStart(2, '0')}-${parts[0].padStart(2, '0')}`; return ""; } function ymdToDmy(str) { if (!str) return ""; const parts = str.split('-'); if (parts.length === 3) return `${parts[2]}.${parts[1]}.${parts[0]}`; return ""; } function openRemoteWorkerModal(mode = 'ADD', fio = '', dept = 'Все', dateFrom = '', dateTo = '') { const modal = document.getElementById("remote-worker-modal"); const titleEl = document.getElementById("remote-modal-title"); const modeInput = document.getElementById("rw-mode"); const fioInput = document.getElementById("rw-fio"); const deptInput = document.getElementById("rw-dept"); const fromInput = document.getElementById("rw-date-from"); const toInput = document.getElementById("rw-date-to"); const errEl = document.getElementById("rw-error"); if (!modal) return; if (errEl) errEl.classList.add("hidden"); if (modeInput) modeInput.value = mode; if (mode === 'EDIT') { if (titleEl) titleEl.innerHTML = `Изменение сроков удаленки`; if (fioInput) { fioInput.value = fio; fioInput.readOnly = true; fioInput.classList.add("bg-slate-100", "text-slate-500", "cursor-not-allowed"); } if (deptInput) { deptInput.value = dept || "Все"; deptInput.readOnly = true; deptInput.classList.add("bg-slate-100", "text-slate-500", "cursor-not-allowed"); } if (fromInput) fromInput.value = dmyToYmd(dateFrom); if (toInput) toInput.value = dmyToYmd(dateTo); } else { if (titleEl) titleEl.innerHTML = `Добавление удаленщика`; if (fioInput) { fioInput.value = ""; fioInput.readOnly = false; fioInput.classList.remove("bg-slate-100", "text-slate-500", "cursor-not-allowed"); } if (deptInput) { deptInput.value = "Все"; deptInput.readOnly = false; deptInput.classList.remove("bg-slate-100", "text-slate-500", "cursor-not-allowed"); } const today = new Date().toISOString().split('T')[0]; if (fromInput) fromInput.value = today; if (toInput) toInput.value = ""; } modal.classList.remove("hidden"); } function closeRemoteWorkerModal() { const modal = document.getElementById("remote-worker-modal"); if (modal) modal.classList.add("hidden"); } async function handleRemoteWorkerSubmit(e) { e.preventDefault(); const mode = document.getElementById("rw-mode").value; const fio = document.getElementById("rw-fio").value.trim(); const dept = document.getElementById("rw-dept").value.trim() || "Все"; const fromVal = ymdToDmy(document.getElementById("rw-date-from").value); const toVal = ymdToDmy(document.getElementById("rw-date-to").value); const errEl = document.getElementById("rw-error"); if (errEl) errEl.classList.add("hidden"); const method = (mode === 'EDIT') ? "PUT" : "POST"; const payload = (mode === 'EDIT') ? { fio: fio, date_from: fromVal, date_to: toVal } : { fio: fio, department: dept, reason: "Удаленная работа", date_from: fromVal, date_to: toVal }; try { const res = await fetch("/api/v1/remote-workers", { method: method, headers: AuthManager.getAuthHeaders(), body: JSON.stringify(payload) }); if (res.ok) { closeRemoteWorkerModal(); if (window.SidebarManager) SidebarManager.renderContent(); } else { const err = await res.json(); if (errEl) { errEl.innerText = err.detail || "Ошибка сохранения"; errEl.classList.remove("hidden"); } } } catch (err) { if (errEl) { errEl.innerText = "Ошибка соединения с сервером"; errEl.classList.remove("hidden"); } } } // ============================================================================ // 5. ИНИЦИАЛИЗАЦИЯ ПРИЛОЖЕНИЯ // ============================================================================ document.addEventListener("DOMContentLoaded", async () => { // 1. Асинхронная подгрузка модальных окон await loadModals(); // 2. Инициализация автокомплита исключений после монтирования разметки const excInput = document.getElementById("exception-value-input"); const excBox = document.getElementById("exception-suggestions"); if (excInput && excBox && typeof setupStaffAutocomplete === "function") { setupStaffAutocomplete(excInput, "exception-suggestions"); } // 3. Авто-высота поля ввода команд const userInputEl = document.getElementById("user-input"); if (userInputEl) { userInputEl.addEventListener("input", function() { this.style.height = "24px"; const newHeight = Math.min(this.scrollHeight, 120); this.style.height = newHeight + "px"; }); } // 4. Проверка сессии пользователя if (IS_GUEST) { hideAuthModal(); updateUIState(); } else if (AuthManager && AuthManager.isAuthenticated()) { hideAuthModal(); updateUIState(); if (typeof loadTasks === "function") { loadTasks(); } if (window.SidebarManager) { SidebarManager.init(); } } else { showAuthModal(); } });