From 565cf5af37f942048424b8d7c9d8536f41eeb084 Mon Sep 17 00:00:00 2001 From: manoraga Date: Fri, 11 Sep 2026 16:02:49 +0300 Subject: [PATCH] =?UTF-8?q?refactor(web):=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=20index.html=20=D0=B8=20=D1=81=D0=B8=D0=BD=D1=85=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=B8?= =?UTF-8?q?=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82=D0=B8=D1=87=D0=B5=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D1=85=20=D1=81=D0=BB=D0=B5=D0=BF=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/web_api/static/index.html | 574 +----------------- modules/web_api/static/js/app.js | 369 ++++++++++- .../web_api/static/modals/admin_modal.html | 32 + modules/web_api/static/modals/auth_modal.html | 28 + .../static/modals/exception_modal.html | 45 ++ .../static/modals/manual_absence_modal.html | 56 ++ .../web_api/static/modals/profile_modal.html | 25 + .../static/modals/remote_worker_modal.html | 57 ++ scripts/diagnostics/make_etl_snapshot.py | 10 +- 9 files changed, 637 insertions(+), 559 deletions(-) create mode 100644 modules/web_api/static/modals/admin_modal.html create mode 100644 modules/web_api/static/modals/auth_modal.html create mode 100644 modules/web_api/static/modals/exception_modal.html create mode 100644 modules/web_api/static/modals/manual_absence_modal.html create mode 100644 modules/web_api/static/modals/profile_modal.html create mode 100644 modules/web_api/static/modals/remote_worker_modal.html diff --git a/modules/web_api/static/index.html b/modules/web_api/static/index.html index 705e1c6..83288fa 100644 --- a/modules/web_api/static/index.html +++ b/modules/web_api/static/index.html @@ -8,7 +8,7 @@ +
- +
- +
- - - - @@ -151,551 +147,10 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/modules/web_api/static/js/app.js b/modules/web_api/static/js/app.js index 4a727d9..9cb1b89 100644 --- a/modules/web_api/static/js/app.js +++ b/modules/web_api/static/js/app.js @@ -1,3 +1,11 @@ +/** + * =============================================================================== + * 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"; @@ -10,9 +18,360 @@ let IS_GUEST = localStorage.getItem("scud_is_guest") === "true"; let inputHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); let historyIndex = -1; -document.addEventListener("DOMContentLoaded", () => { - const userInputEl = document.getElementById("user-input"); +// ============================================================================ +// 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"; @@ -21,15 +380,19 @@ document.addEventListener("DOMContentLoaded", () => { }); } + // 4. Проверка сессии пользователя if (IS_GUEST) { hideAuthModal(); updateUIState(); - } else if (API_TOKEN) { + } else if (AuthManager && AuthManager.isAuthenticated()) { hideAuthModal(); updateUIState(); if (typeof loadTasks === "function") { loadTasks(); } + if (window.SidebarManager) { + SidebarManager.init(); + } } else { showAuthModal(); } diff --git a/modules/web_api/static/modals/admin_modal.html b/modules/web_api/static/modals/admin_modal.html new file mode 100644 index 0000000..cb161da --- /dev/null +++ b/modules/web_api/static/modals/admin_modal.html @@ -0,0 +1,32 @@ + \ No newline at end of file diff --git a/modules/web_api/static/modals/auth_modal.html b/modules/web_api/static/modals/auth_modal.html new file mode 100644 index 0000000..685a2b5 --- /dev/null +++ b/modules/web_api/static/modals/auth_modal.html @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/modules/web_api/static/modals/exception_modal.html b/modules/web_api/static/modals/exception_modal.html new file mode 100644 index 0000000..3630fd1 --- /dev/null +++ b/modules/web_api/static/modals/exception_modal.html @@ -0,0 +1,45 @@ + \ No newline at end of file diff --git a/modules/web_api/static/modals/manual_absence_modal.html b/modules/web_api/static/modals/manual_absence_modal.html new file mode 100644 index 0000000..02cb816 --- /dev/null +++ b/modules/web_api/static/modals/manual_absence_modal.html @@ -0,0 +1,56 @@ + \ No newline at end of file diff --git a/modules/web_api/static/modals/profile_modal.html b/modules/web_api/static/modals/profile_modal.html new file mode 100644 index 0000000..e27703e --- /dev/null +++ b/modules/web_api/static/modals/profile_modal.html @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/modules/web_api/static/modals/remote_worker_modal.html b/modules/web_api/static/modals/remote_worker_modal.html new file mode 100644 index 0000000..abefde2 --- /dev/null +++ b/modules/web_api/static/modals/remote_worker_modal.html @@ -0,0 +1,57 @@ + \ No newline at end of file diff --git a/scripts/diagnostics/make_etl_snapshot.py b/scripts/diagnostics/make_etl_snapshot.py index dda9f70..6ef7235 100644 --- a/scripts/diagnostics/make_etl_snapshot.py +++ b/scripts/diagnostics/make_etl_snapshot.py @@ -42,12 +42,20 @@ TARGET_FILES = [ "services/knowledge_base.py", "services/knowledge/service.py", - # Модули сборки Сводки и Отчета + # Модули генерации отчетов Excel + "services/reports/styles.py", + "services/reports/calculators.py", + "services/reports/svodka_builder.py", + "services/reports/otchet_builder.py", + "services/reports/raw_scud_builder.py", + + # Модули сборки Сводки, Отчета и запросы "services/scud_etl/pipeline.py", "services/scud_etl/merger.py", "services/scud_etl/svodka_generator.py", "services/scud_etl/otchet_generator.py", "services/scud_etl/anomaly_detector.py", + "services/scud_etl/sql_queries.py", "services/snapshots/service.py", "services/tasks/repository.py", "services/tasks/service.py"