399 lines
16 KiB
JavaScript
399 lines
16 KiB
JavaScript
/**
|
|
* ===============================================================================
|
|
* 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 = `<div class="text-center py-4 text-xs text-slate-400"><i class="fa-solid fa-spinner fa-spin mr-1"></i> Загрузка...</div>`;
|
|
|
|
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 => `
|
|
<div class="flex items-center justify-between p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs">
|
|
<div>
|
|
<div class="font-bold text-slate-800">${escapeHtml(u.full_name || u.username)} <span class="text-slate-400 font-mono text-[10px]">(${escapeHtml(u.username)})</span></div>
|
|
<div class="text-[10px] ${u.is_admin ? 'text-indigo-600 font-bold' : 'text-slate-400'}">${u.is_admin ? 'Администратор' : 'Оператор'}</div>
|
|
</div>
|
|
<button onclick="deleteAdminUser(${u.id}, '${escapeHtml(u.username)}')" class="text-slate-400 hover:text-rose-600 p-1.5" title="Удалить">
|
|
<i class="fa-solid fa-trash-can"></i>
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
} else {
|
|
listEl.innerHTML = `<div class="text-center py-4 text-xs text-rose-500">Ошибка загрузки пользователей</div>`;
|
|
}
|
|
} catch (e) {
|
|
listEl.innerHTML = `<div class="text-center py-4 text-xs text-rose-500">Ошибка сети</div>`;
|
|
}
|
|
}
|
|
|
|
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 = `<i class="fa-solid fa-pen-to-square text-emerald-600"></i><span>Изменение сроков удаленки</span>`;
|
|
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 = `<i class="fa-solid fa-house-laptop text-emerald-600"></i><span>Добавление удаленщика</span>`;
|
|
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();
|
|
}
|
|
}); |