710 lines
42 KiB
HTML
710 lines
42 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru" class="h-full bg-slate-100">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SCUD Orion AI Assistant</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
|
<style>
|
|
/* Запрещаем браузеру насильно удерживать скролл внизу при появлении ответа */
|
|
* {
|
|
overflow-anchor: none !important;
|
|
}
|
|
|
|
#chat-messages-container, main {
|
|
overflow-anchor: none !important;
|
|
}
|
|
|
|
/* ⭐️ Воздух снизу для возможности поднятия вопроса на самый верх */
|
|
#chat-messages-container {
|
|
padding-bottom: clamp(400px, 85vh, 900px) !important;
|
|
}
|
|
|
|
/* Принудительное увеличение шрифта сообщений чата */
|
|
#chat-messages-container .message-content,
|
|
#chat-messages-container .text-xs,
|
|
#chat-messages-container .text-sm {
|
|
font-size: 14.5px !important;
|
|
line-height: 1.6 !important;
|
|
}
|
|
#chat-messages-container pre,
|
|
#chat-messages-container code {
|
|
font-size: 13.5px !important;
|
|
}
|
|
/* Смещение для точной прокрутки под фиксированный заголовок */
|
|
.user-chat-bubble {
|
|
scroll-margin-top: 24px !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="h-full flex flex-col font-sans antialiased text-slate-800 bg-slate-100 selection:bg-indigo-500 selection:text-white">
|
|
|
|
<div id="app-container" class="flex-1 flex overflow-hidden w-full h-full">
|
|
|
|
<!-- ЛЕВАЯ КОЛОНКА (ДИНАМИЧЕСКИЙ САЙДБАР 5-ХАБОВ) -->
|
|
<aside class="w-80 md:w-96 bg-white border-r border-slate-200 flex flex-col shrink-0 h-full shadow-sm z-10 select-none">
|
|
|
|
<!-- ДИНАМИЧЕСКИЙ ТАБ-БАР ХАБОВ И ПОДВКЛАДОК -->
|
|
<div id="sidebar-dynamic-header" class="shrink-0 bg-slate-50 border-b border-slate-200"></div>
|
|
|
|
<!-- ДИНАМИЧЕСКИЙ КОНТЕНТНЫЙ СЛОТ -->
|
|
<div id="sidebar-dynamic-content" class="flex-1 overflow-y-auto p-2 flex flex-col gap-2">
|
|
<div id="tasks-list-container" class="flex-1 flex flex-col gap-2">
|
|
<div class="text-center py-10 text-xs text-slate-400">
|
|
<i class="fa-solid fa-spinner fa-spin mr-1"></i> Загрузка...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ПОДВАЛ САЙДБАРА: ПРОФИЛЬ, НАСТРОЙКИ, АДМИНКА И ВЫХОД -->
|
|
<div class="p-3 border-t border-slate-200 bg-slate-50 flex items-center justify-between shrink-0">
|
|
<div class="flex items-center gap-2.5 min-w-0">
|
|
<div class="w-8 h-8 rounded-full bg-indigo-600 text-white flex items-center justify-center font-bold text-xs shadow-sm shrink-0">
|
|
<i class="fa-solid fa-user"></i>
|
|
</div>
|
|
<div class="min-w-0 flex flex-col">
|
|
<span id="user-display-name" class="text-xs font-bold text-slate-800 truncate">Пользователь</span>
|
|
<span id="user-display-role" class="text-[10px] text-slate-400">Оператор</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-1">
|
|
<button id="admin-panel-btn" type="button" onclick="openAdminModal()" class="hidden p-1.5 text-slate-400 hover:text-indigo-600 hover:bg-slate-200 rounded-lg transition" title="Управление пользователями">
|
|
<i class="fa-solid fa-users-gear text-sm"></i>
|
|
</button>
|
|
|
|
<button type="button" onclick="openProfileModal()" class="p-1.5 text-slate-400 hover:text-slate-700 hover:bg-slate-200 rounded-lg transition" title="Сменить пароль">
|
|
<i class="fa-solid fa-gear text-sm"></i>
|
|
</button>
|
|
|
|
<button type="button" onclick="AuthManager.logout()" class="p-1.5 text-slate-400 hover:text-rose-600 hover:bg-rose-50 rounded-lg transition" title="Выйти из системы">
|
|
<i class="fa-solid fa-arrow-right-from-bracket text-sm"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ПРАВАЯ ОБЛАСТЬ (ЧАТ И ИИ-АССИСТЕНТ) -->
|
|
<main class="flex-1 flex flex-col h-full min-w-0 bg-slate-50 relative">
|
|
<header class="h-14 bg-white border-b border-slate-200 px-4 flex items-center justify-between shrink-0 shadow-sm z-10">
|
|
<div class="flex items-center gap-2.5">
|
|
<div class="w-7 h-7 rounded-lg bg-indigo-600 text-white flex items-center justify-center shadow-sm">
|
|
<i class="fa-solid fa-robot text-xs"></i>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<h1 class="text-sm font-bold text-slate-800">SCUD Orion AI Assistant</h1>
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200">Online</span>
|
|
</div>
|
|
<p class="text-[10px] text-slate-400">Система интеллектуального аудита и контроля СКУД / 1С</p>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- ЛЕНТА ЧАТА -->
|
|
<div id="chat-messages-container" class="flex-1 overflow-y-auto p-4 md:p-6 flex flex-col gap-4">
|
|
<div class="flex gap-3 max-w-4xl mx-auto w-full">
|
|
<div class="w-7 h-7 rounded-lg bg-indigo-600 text-white flex items-center justify-center shrink-0 shadow-sm mt-0.5">
|
|
<i class="fa-solid fa-robot text-xs"></i>
|
|
</div>
|
|
<div class="flex-1 bg-white border border-slate-200 rounded-2xl rounded-tl-none p-4 shadow-sm">
|
|
<div class="text-[10px] font-bold text-indigo-600 uppercase tracking-wider mb-1">ИИ-ассистент SCUD Orion AI</div>
|
|
<div class="text-xs text-slate-700 leading-relaxed">
|
|
Привет! Вы можете задавать вопросы ассистенту, управлять системным промптом, сверять кадровые нестыковки СКУД и 1С или формировать срезы и отчеты.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- БЕЙДЖ ПРИКРЕПЛЕННОГО ФАЙЛА -->
|
|
<div id="file-attachment-preview" class="hidden max-w-4xl mx-auto w-full px-4 pt-2">
|
|
<div class="inline-flex items-center gap-2 px-3 py-1 bg-indigo-50 border border-indigo-200 rounded-xl text-xs text-indigo-700 shadow-sm">
|
|
<i class="fa-solid fa-file text-indigo-600"></i>
|
|
<span id="file-attachment-name" class="font-medium truncate max-w-xs"></span>
|
|
<button type="button" onclick="clearAttachedFile()" class="text-indigo-400 hover:text-rose-600 ml-1">
|
|
<i class="fa-solid fa-xmark"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Контейнер строки ввода сообщения -->
|
|
<div class="px-4 py-2 bg-white border-t border-slate-200">
|
|
<div id="chat-input-box" class="max-w-4xl mx-auto w-full flex items-center gap-2 bg-slate-50 border border-slate-300 rounded-xl px-2.5 py-1 transition focus-within:border-indigo-500 focus-within:bg-white focus-within:ring-1 focus-within:ring-indigo-100">
|
|
<!-- Скрепка файлов -->
|
|
<button type="button" onclick="document.getElementById('file-upload-input').click()" class="text-slate-400 hover:text-indigo-600 p-1 transition shrink-0">
|
|
<i class="fa-solid fa-paperclip text-xs"></i>
|
|
</button>
|
|
<input type="file" id="file-upload-input" class="hidden" />
|
|
|
|
<!-- Поле ввода -->
|
|
<textarea id="user-input" rows="1" placeholder="Команда, вопрос (Enter - отправить, Shift+Enter - перенос строки)..."
|
|
class="flex-1 bg-transparent border-0 focus:outline-none text-xs text-slate-800 resize-none py-0 leading-5" style="height: 24px; line-height: 24px;"></textarea>
|
|
|
|
<!-- Кнопка отправки -->
|
|
<button type="button" onclick="window.sendMessage()" class="w-6 h-6 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white flex items-center justify-center shrink-0 shadow-sm transition">
|
|
<i class="fa-solid fa-paper-plane text-[10px]"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- МОДАЛЬНОЕ ОКНО: УДАЛЕННЫЙ СОТРУДНИК -->
|
|
<div id="remote-worker-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden">
|
|
<div class="bg-white rounded-2xl shadow-2xl border border-slate-200 max-w-md w-full p-6 flex flex-col gap-4">
|
|
<div class="flex items-center justify-between">
|
|
<h3 id="remote-modal-title" class="text-sm font-bold text-slate-800 flex items-center gap-2">
|
|
<i class="fa-solid fa-house-laptop text-emerald-600"></i>
|
|
<span>Параметры удаленной работы</span>
|
|
</h3>
|
|
<button type="button" onclick="closeRemoteWorkerModal()" class="text-slate-400 hover:text-slate-600">
|
|
<i class="fa-solid fa-xmark"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="remote-worker-form" onsubmit="handleRemoteWorkerSubmit(event)" class="flex flex-col gap-3">
|
|
<input type="hidden" id="rw-mode" value="ADD" />
|
|
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">ФИО сотрудника:</label>
|
|
<input type="text" id="rw-fio" required placeholder="Например: Иванов Иван Иванович"
|
|
class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-emerald-500 bg-slate-50" />
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Подразделение:</label>
|
|
<input type="text" id="rw-dept" placeholder="Все"
|
|
class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-emerald-500 bg-slate-50" />
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Дата начала:</label>
|
|
<input type="date" id="rw-date-from"
|
|
class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-emerald-500 bg-slate-50 text-slate-700" />
|
|
<span class="text-[10px] text-slate-400 mt-0.5 block">Пусто = с сегодняшнего дня</span>
|
|
</div>
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Дата окончания:</label>
|
|
<input type="date" id="rw-date-to"
|
|
class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-emerald-500 bg-slate-50 text-slate-700" />
|
|
<span class="text-[10px] text-slate-400 mt-0.5 block">Пусто = бессрочно</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="rw-error" class="text-[11px] font-semibold text-rose-600 hidden"></div>
|
|
|
|
<div class="flex items-center justify-end gap-2 mt-2 pt-2 border-t border-slate-100">
|
|
<button type="button" onclick="closeRemoteWorkerModal()"
|
|
class="px-3.5 py-1.5 text-xs text-slate-600 rounded-lg hover:bg-slate-100 font-medium transition">
|
|
Отмена
|
|
</button>
|
|
<button type="submit" id="rw-submit-btn"
|
|
class="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold shadow transition">
|
|
Сохранить
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- МОДАЛЬНОЕ ОКНО: МЕСТНАЯ КОМАНДИРОВКА И ИНОЕ С АВТОКОМПЛИТОМ -->
|
|
<div id="manual-absence-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden">
|
|
<div class="bg-white rounded-2xl shadow-2xl border border-slate-200 max-w-md w-full p-6 flex flex-col gap-4">
|
|
<div class="flex items-center justify-between">
|
|
<h3 id="manual-absence-modal-title" class="text-sm font-bold text-slate-800 flex items-center gap-2">
|
|
<i class="fa-solid fa-location-dot text-indigo-600"></i>
|
|
<span>Добавление в реестр</span>
|
|
</h3>
|
|
<button type="button" onclick="closeManualAbsenceModal()" class="text-slate-400 hover:text-slate-600">
|
|
<i class="fa-solid fa-xmark"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3">
|
|
<!-- Поле ФИО с автодополнением по 1С -->
|
|
<div class="relative">
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">ФИО сотрудника (автоподбор из 1С):</label>
|
|
<input type="text" id="manual-absence-fio-input" autocomplete="off" placeholder="Начните вводить фамилию..."
|
|
class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500 bg-slate-50" />
|
|
<div id="manual-absence-suggestions" class="hidden absolute left-0 right-0 top-full mt-1 bg-white border border-slate-300 rounded-lg shadow-xl z-30 max-h-48 overflow-y-auto"></div>
|
|
</div>
|
|
|
|
<input type="hidden" id="manual-absence-dept" />
|
|
<input type="hidden" id="manual-absence-pos" />
|
|
|
|
<!-- Выпадающий список причин (только для "Иное") -->
|
|
<div id="manual-absence-reason-block" class="hidden">
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Причина отсутствия:</label>
|
|
<select id="manual-absence-reason-select" class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500 bg-slate-50"></select>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Начало:</label>
|
|
<input type="date" id="manual-absence-start-date"
|
|
class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500 bg-slate-50 text-slate-700" />
|
|
<span class="text-[10px] text-slate-400 mt-0.5 block">Пусто = сегодня</span>
|
|
</div>
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Окончание:</label>
|
|
<input type="date" id="manual-absence-end-date"
|
|
class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500 bg-slate-50 text-slate-700" />
|
|
<span class="text-[10px] text-slate-400 mt-0.5 block">По умолчанию: сегодня</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-end gap-2 mt-2 pt-2 border-t border-slate-100">
|
|
<button type="button" onclick="closeManualAbsenceModal()"
|
|
class="px-3.5 py-1.5 text-xs text-slate-600 rounded-lg hover:bg-slate-100 font-medium transition">
|
|
Отмена
|
|
</button>
|
|
<button type="button" onclick="submitManualAbsence()"
|
|
class="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-bold shadow transition">
|
|
Сохранить
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- МОДАЛЬНОЕ ОКНО: ДОБАВЛЕНИЕ В РЕЕСТРЫ ИСКЛЮЧЕНИЙ И ТУРНИКЕТОВ -->
|
|
<div id="exception-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden">
|
|
<div class="bg-white rounded-2xl shadow-2xl border border-slate-200 max-w-md w-full p-6 flex flex-col gap-4">
|
|
<div class="flex items-center justify-between">
|
|
<h3 id="exception-modal-title" class="text-sm font-bold text-slate-800 flex items-center gap-2">
|
|
<i class="fa-solid fa-user-shield text-indigo-600"></i>
|
|
<span id="exception-modal-header-text">Добавление в реестр</span>
|
|
</h3>
|
|
<button type="button" onclick="closeExceptionModal()" class="text-slate-400 hover:text-slate-600">
|
|
<i class="fa-solid fa-xmark"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="exception-modal-form" onsubmit="submitExceptionModalForm(event)" class="flex flex-col gap-3">
|
|
<input type="hidden" id="exception-category-input" value="" />
|
|
|
|
<!-- Поле ввода значения с автокомплитом -->
|
|
<div class="relative">
|
|
<label id="exception-value-label" class="block text-[11px] font-bold text-slate-600 mb-1">ФИО сотрудника:</label>
|
|
<input type="text" id="exception-value-input" autocomplete="off" required
|
|
placeholder="Начните вводить фамилию..."
|
|
class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500 bg-slate-50 text-slate-800" />
|
|
<!-- Выпадающие подсказки из 1С -->
|
|
<div id="exception-suggestions" class="hidden absolute left-0 right-0 top-full mt-1 bg-white border border-slate-300 rounded-lg shadow-xl z-30 max-h-48 overflow-y-auto"></div>
|
|
</div>
|
|
|
|
<!-- Опциональный комментарий -->
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Примечание / основание (опционально):</label>
|
|
<input type="text" id="exception-comment-input" placeholder="Например: служебная записка, водитель, лаборатория"
|
|
class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500 bg-slate-50 text-slate-800" />
|
|
</div>
|
|
|
|
<div id="exception-error-msg" class="text-[11px] font-semibold text-rose-600 hidden"></div>
|
|
|
|
<div class="flex items-center justify-end gap-2 mt-2 pt-2 border-t border-slate-100">
|
|
<button type="button" onclick="closeExceptionModal()"
|
|
class="px-3.5 py-1.5 text-xs text-slate-600 rounded-lg hover:bg-slate-100 font-medium transition">
|
|
Отмена
|
|
</button>
|
|
<button type="submit" id="exception-submit-btn"
|
|
class="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-bold shadow transition flex items-center gap-1.5">
|
|
<i class="fa-solid fa-check text-xs"></i>
|
|
<span>Добавить</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- МОДАЛЬНОЕ ОКНО АВТОРИЗАЦИИ -->
|
|
<div id="auth-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden">
|
|
<div class="bg-white rounded-2xl shadow-2xl border border-slate-200 max-w-sm w-full p-6 flex flex-col gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-xl bg-indigo-600 text-white flex items-center justify-center font-bold text-lg shadow">
|
|
<i class="fa-solid fa-shield-halved"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-sm font-bold text-slate-800">Авторизация в системе</h2>
|
|
<p class="text-[11px] text-slate-400">SCUD Orion AI Security Access</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="auth-form" onsubmit="handleLoginSubmit(event)" class="flex flex-col gap-3">
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Имя пользователя:</label>
|
|
<input type="text" id="auth-username" required class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500 bg-slate-50" placeholder="Логин" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Пароль:</label>
|
|
<input type="password" id="auth-password" required class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500 bg-slate-50" placeholder="••••••••" />
|
|
</div>
|
|
<div id="auth-error" class="text-[11px] font-semibold text-rose-600 hidden"></div>
|
|
<button type="submit" class="w-full py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-lg text-xs shadow transition mt-1">
|
|
Войти в систему
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- МОДАЛЬНОЕ ОКНО СМЕНЫ ПАРОЛЯ -->
|
|
<div id="profile-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden">
|
|
<div class="bg-white rounded-2xl shadow-2xl border border-slate-200 max-w-sm w-full p-6 flex flex-col gap-4">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-bold text-slate-800 flex items-center gap-1.5">
|
|
<i class="fa-solid fa-key text-indigo-600"></i> Смена пароля
|
|
</h3>
|
|
<button onclick="closeProfileModal()" class="text-slate-400 hover:text-slate-600"><i class="fa-solid fa-xmark"></i></button>
|
|
</div>
|
|
<form onsubmit="handleChangePassword(event)" class="flex flex-col gap-3">
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Текущий пароль:</label>
|
|
<input type="password" id="old-pass" required class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-[11px] font-bold text-slate-600 mb-1">Новый пароль (мин. 4 симв.):</label>
|
|
<input type="password" id="new-pass" required class="w-full text-xs px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500" />
|
|
</div>
|
|
<div id="pass-error" class="text-[11px] font-semibold text-rose-600 hidden"></div>
|
|
<div class="flex items-center justify-end gap-2 mt-2">
|
|
<button type="button" onclick="closeProfileModal()" class="px-3 py-1.5 text-xs text-slate-600 rounded-lg hover:bg-slate-100 font-medium">Отмена</button>
|
|
<button type="submit" class="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-bold shadow transition">Сохранить</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- МОДАЛЬНОЕ ОКНО ПАНЕЛИ АДМИНИСТРАТОРА -->
|
|
<div id="admin-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden">
|
|
<div class="bg-white rounded-2xl shadow-2xl border border-slate-200 max-w-lg w-full p-6 flex flex-col gap-4 max-h-[85vh]">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-bold text-slate-800 flex items-center gap-1.5">
|
|
<i class="fa-solid fa-users-gear text-indigo-600"></i> Управление учетными записями
|
|
</h3>
|
|
<button onclick="closeAdminModal()" class="text-slate-400 hover:text-slate-600"><i class="fa-solid fa-xmark"></i></button>
|
|
</div>
|
|
|
|
<form onsubmit="handleCreateUser(event)" class="p-3 bg-slate-50 border border-slate-200 rounded-xl flex flex-col gap-2">
|
|
<span class="text-xs font-bold text-slate-700">Создать нового пользователя:</span>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<input type="text" id="new-user-username" placeholder="Логин" required class="text-xs px-2.5 py-1.5 border border-slate-300 rounded-lg bg-white" />
|
|
<input type="password" id="new-user-password" placeholder="Пароль" required class="text-xs px-2.5 py-1.5 border border-slate-300 rounded-lg bg-white" />
|
|
</div>
|
|
<input type="text" id="new-user-fullname" placeholder="ФИО" class="text-xs px-2.5 py-1.5 border border-slate-300 rounded-lg bg-white" />
|
|
<div class="flex items-center justify-between">
|
|
<label class="flex items-center gap-1.5 text-xs text-slate-600 cursor-pointer">
|
|
<input type="checkbox" id="new-user-admin" class="rounded border-slate-300 text-indigo-600" />
|
|
<span>Права администратора</span>
|
|
</label>
|
|
<button type="submit" class="px-3 py-1 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-bold shadow">
|
|
Создать
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="flex-1 overflow-y-auto flex flex-col gap-1.5" id="admin-users-list">
|
|
<div class="text-center py-4 text-xs text-slate-400">Загрузка пользователей...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ПОДКЛЮЧЕНИЕ СКРИПТОВ -->
|
|
<script src="/static/js/auth.js?v=2.5.7"></script>
|
|
<script src="/static/js/tasks.js?v=2.5.7"></script>
|
|
<script src="/static/js/manual_absences.js?v=2.5.7"></script>
|
|
<script src="/static/js/sidebar.js?v=2.5.7"></script>
|
|
<script src="/static/js/chat/task_widget.js?v=2.5.7"></script>
|
|
<script src="/static/js/chat/core.js?v=2.5.7"></script>
|
|
<script src="/static/js/app.js?v=2.5.7"></script>
|
|
|
|
<script>
|
|
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");
|
|
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();
|
|
errEl.innerText = err.detail || "Неверный логин или пароль";
|
|
errEl.classList.remove("hidden");
|
|
}
|
|
} catch (err) {
|
|
errEl.innerText = "Ошибка соединения с сервером";
|
|
errEl.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
function openProfileModal() {
|
|
document.getElementById("profile-modal").classList.remove("hidden");
|
|
}
|
|
function closeProfileModal() {
|
|
document.getElementById("profile-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");
|
|
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();
|
|
errEl.innerText = err.detail || "Ошибка изменения пароля";
|
|
errEl.classList.remove("hidden");
|
|
}
|
|
} catch (e) {
|
|
errEl.innerText = "Ошибка сети";
|
|
errEl.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
function openAdminModal() {
|
|
document.getElementById("admin-modal").classList.remove("hidden");
|
|
loadAdminUsers();
|
|
}
|
|
function closeAdminModal() {
|
|
document.getElementById("admin-modal").classList.add("hidden");
|
|
}
|
|
async function loadAdminUsers() {
|
|
const listEl = document.getElementById("admin-users-list");
|
|
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("Ошибка сети");
|
|
}
|
|
}
|
|
|
|
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");
|
|
|
|
errEl.classList.add("hidden");
|
|
modeInput.value = mode;
|
|
|
|
if (mode === 'EDIT') {
|
|
titleEl.innerHTML = `<i class="fa-solid fa-pen-to-square text-emerald-600"></i><span>Изменение сроков удаленки</span>`;
|
|
fioInput.value = fio;
|
|
fioInput.readOnly = true;
|
|
fioInput.classList.add("bg-slate-100", "text-slate-500", "cursor-not-allowed");
|
|
deptInput.value = dept || "Все";
|
|
deptInput.readOnly = true;
|
|
deptInput.classList.add("bg-slate-100", "text-slate-500", "cursor-not-allowed");
|
|
fromInput.value = dmyToYmd(dateFrom);
|
|
toInput.value = dmyToYmd(dateTo);
|
|
} else {
|
|
titleEl.innerHTML = `<i class="fa-solid fa-house-laptop text-emerald-600"></i><span>Добавление удаленщика</span>`;
|
|
fioInput.value = "";
|
|
fioInput.readOnly = false;
|
|
fioInput.classList.remove("bg-slate-100", "text-slate-500", "cursor-not-allowed");
|
|
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];
|
|
fromInput.value = today;
|
|
toInput.value = "";
|
|
}
|
|
|
|
modal.classList.remove("hidden");
|
|
}
|
|
|
|
function closeRemoteWorkerModal() {
|
|
document.getElementById("remote-worker-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");
|
|
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();
|
|
errEl.innerText = err.detail || "Ошибка сохранения";
|
|
errEl.classList.remove("hidden");
|
|
}
|
|
} catch (err) {
|
|
errEl.innerText = "Ошибка соединения с сервером";
|
|
errEl.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
if (window.AuthManager && AuthManager.isAuthenticated()) {
|
|
updateUIState();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<!-- Глобальная Drag-and-Drop зона на весь экран -->
|
|
<div id="global-drag-overlay"
|
|
class="fixed inset-0 bg-indigo-900/40 backdrop-blur-xs z-50 hidden flex items-center justify-center pointer-events-none transition-all duration-200">
|
|
<div class="bg-white border-2 border-dashed border-indigo-500 rounded-3xl p-10 flex flex-col items-center gap-3 shadow-2xl scale-100 transition-transform">
|
|
<div class="w-16 h-16 rounded-2xl bg-indigo-50 text-indigo-600 flex items-center justify-center text-3xl shadow-inner">
|
|
<i class="fa-solid fa-cloud-arrow-up animate-bounce"></i>
|
|
</div>
|
|
<div class="text-base font-bold text-slate-800">Перетащите файл в любую точку окна</div>
|
|
<div class="text-xs text-slate-500 font-medium">PDF-документы, сканы, отчеты или изображения</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html> |