refactor(web): модуляризация модальных окон index.html и синхронизация диагностических слепков
This commit is contained in:
@@ -17,12 +17,12 @@
|
|||||||
overflow-anchor: none !important;
|
overflow-anchor: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ⭐️ Воздух снизу для возможности поднятия вопроса на самый верх */
|
/* Воздух снизу для возможности поднятия вопроса на самый верх */
|
||||||
#chat-messages-container {
|
#chat-messages-container {
|
||||||
padding-bottom: clamp(400px, 85vh, 900px) !important;
|
padding-bottom: clamp(400px, 85vh, 900px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Принудительное увеличение шрифта сообщений чата */
|
/* Шрифт сообщений чата */
|
||||||
#chat-messages-container .message-content,
|
#chat-messages-container .message-content,
|
||||||
#chat-messages-container .text-xs,
|
#chat-messages-container .text-xs,
|
||||||
#chat-messages-container .text-sm {
|
#chat-messages-container .text-sm {
|
||||||
@@ -33,17 +33,16 @@
|
|||||||
#chat-messages-container code {
|
#chat-messages-container code {
|
||||||
font-size: 13.5px !important;
|
font-size: 13.5px !important;
|
||||||
}
|
}
|
||||||
/* Смещение для точной прокрутки под фиксированный заголовок */
|
|
||||||
.user-chat-bubble {
|
.user-chat-bubble {
|
||||||
scroll-margin-top: 24px !important;
|
scroll-margin-top: 24px !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full flex flex-col font-sans antialiased text-slate-800 bg-slate-100 selection:bg-indigo-500 selection:text-white">
|
<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">
|
<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">
|
<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">
|
||||||
|
|
||||||
<!-- ДИНАМИЧЕСКИЙ ТАБ-БАР ХАБОВ И ПОДВКЛАДОК -->
|
<!-- ДИНАМИЧЕСКИЙ ТАБ-БАР ХАБОВ И ПОДВКЛАДОК -->
|
||||||
@@ -129,20 +128,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Контейнер строки ввода сообщения -->
|
<!-- СТРОКА ВВОДА -->
|
||||||
<div class="px-4 py-2 bg-white border-t border-slate-200">
|
<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">
|
<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" title="Прикрепить файл">
|
||||||
<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>
|
<i class="fa-solid fa-paperclip text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
<input type="file" id="file-upload-input" class="hidden" />
|
<input type="file" id="file-upload-input" class="hidden" />
|
||||||
|
|
||||||
<!-- Поле ввода -->
|
|
||||||
<textarea id="user-input" rows="1" placeholder="Команда, вопрос (Enter - отправить, Shift+Enter - перенос строки)..."
|
<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>
|
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">
|
<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>
|
<i class="fa-solid fa-paper-plane text-[10px]"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -151,551 +147,10 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</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 id="modals-container"></div>
|
||||||
<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">
|
<!-- ГЛОБАЛЬНАЯ DRAG-AND-DROP ЗОНА -->
|
||||||
<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"
|
<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">
|
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="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">
|
||||||
@@ -706,5 +161,14 @@
|
|||||||
<div class="text-xs text-slate-500 font-medium">PDF-документы, сканы, отчеты или изображения</div>
|
<div class="text-xs text-slate-500 font-medium">PDF-документы, сканы, отчеты или изображения</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- СКРИПТЫ -->
|
||||||
|
<script src="/static/js/auth.js?v=2.5.8"></script>
|
||||||
|
<script src="/static/js/tasks.js?v=2.5.8"></script>
|
||||||
|
<script src="/static/js/manual_absences.js?v=2.5.8"></script>
|
||||||
|
<script src="/static/js/sidebar.js?v=2.5.8"></script>
|
||||||
|
<script src="/static/js/chat/task_widget.js?v=2.5.8"></script>
|
||||||
|
<script src="/static/js/chat/core.js?v=2.5.8"></script>
|
||||||
|
<script src="/static/js/app.js?v=2.5.8"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* ===============================================================================
|
||||||
|
* FILE: modules/web_api/static/js/app.js
|
||||||
|
* ROLE: Главная точка входа UI: динамическая загрузка модальных окон,
|
||||||
|
* маршрутизация авторизации, управление профилем и глобальный стейт.
|
||||||
|
* ===============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
const AUTH_TOKEN_KEY = "scud_api_auth_token";
|
const AUTH_TOKEN_KEY = "scud_api_auth_token";
|
||||||
const SESSION_ID = "web_session_main";
|
const SESSION_ID = "web_session_main";
|
||||||
const STORAGE_KEY = "scud_chat_input_history";
|
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 inputHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
||||||
let historyIndex = -1;
|
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 = `<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) {
|
if (userInputEl) {
|
||||||
userInputEl.addEventListener("input", function() {
|
userInputEl.addEventListener("input", function() {
|
||||||
this.style.height = "24px";
|
this.style.height = "24px";
|
||||||
@@ -21,15 +380,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Проверка сессии пользователя
|
||||||
if (IS_GUEST) {
|
if (IS_GUEST) {
|
||||||
hideAuthModal();
|
hideAuthModal();
|
||||||
updateUIState();
|
updateUIState();
|
||||||
} else if (API_TOKEN) {
|
} else if (AuthManager && AuthManager.isAuthenticated()) {
|
||||||
hideAuthModal();
|
hideAuthModal();
|
||||||
updateUIState();
|
updateUIState();
|
||||||
if (typeof loadTasks === "function") {
|
if (typeof loadTasks === "function") {
|
||||||
loadTasks();
|
loadTasks();
|
||||||
}
|
}
|
||||||
|
if (window.SidebarManager) {
|
||||||
|
SidebarManager.init();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showAuthModal();
|
showAuthModal();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<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" />
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<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">
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<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>
|
||||||
@@ -42,12 +42,20 @@ TARGET_FILES = [
|
|||||||
"services/knowledge_base.py",
|
"services/knowledge_base.py",
|
||||||
"services/knowledge/service.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/pipeline.py",
|
||||||
"services/scud_etl/merger.py",
|
"services/scud_etl/merger.py",
|
||||||
"services/scud_etl/svodka_generator.py",
|
"services/scud_etl/svodka_generator.py",
|
||||||
"services/scud_etl/otchet_generator.py",
|
"services/scud_etl/otchet_generator.py",
|
||||||
"services/scud_etl/anomaly_detector.py",
|
"services/scud_etl/anomaly_detector.py",
|
||||||
|
"services/scud_etl/sql_queries.py",
|
||||||
"services/snapshots/service.py",
|
"services/snapshots/service.py",
|
||||||
"services/tasks/repository.py",
|
"services/tasks/repository.py",
|
||||||
"services/tasks/service.py"
|
"services/tasks/service.py"
|
||||||
|
|||||||
Reference in New Issue
Block a user