feat(reports): stabilize on-demand generation, live presence and 1C fallback

This commit is contained in:
2026-09-25 13:00:11 +03:00
parent 2f8cc1bffd
commit 90656944c5
42 changed files with 13410 additions and 6778 deletions
@@ -3,54 +3,57 @@
<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>
<span id="manual-absence-modal-title-text">Добавление в реестр</span>
</h3>
<button type="button" onclick="closeManualAbsenceModal()" class="text-slate-400 hover:text-slate-600">
<i class="fa-solid fa-xmark"></i>
<button type="button" onclick="closeManualAbsenceModal()" class="text-slate-400 hover:text-slate-600 p-1 rounded-lg hover:bg-slate-100 transition">
<i class="fa-solid fa-xmark text-base"></i>
</button>
</div>
<div class="flex flex-col gap-3">
<form id="manual-absence-form" onsubmit="submitManualAbsence(event)" class="flex flex-col gap-3">
<input type="hidden" id="manual-absence-id" value="" />
<input type="hidden" id="manual-absence-dept" value="" />
<input type="hidden" id="manual-absence-pos" value="" />
<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" />
<input type="text" id="manual-absence-fio-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 transition" />
<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>
<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 text-slate-800"></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"
<label class="block text-[11px] font-bold text-slate-600 mb-1">Дата начала:</label>
<input type="date" id="manual-absence-start-date" 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 text-slate-700" />
<span class="text-[10px] text-slate-400 mt-0.5 block">Пусто = сегодня</span>
<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"
<label class="block text-[11px] font-bold text-slate-600 mb-1">Дата окончания:</label>
<input type="date" id="manual-absence-end-date" 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 text-slate-700" />
<span class="text-[10px] text-slate-400 mt-0.5 block">По умолчанию: сегодня</span>
<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">
<div id="manual-absence-error" class="text-[11px] font-semibold text-rose-600 hidden"></div>
<div class="flex items-center justify-end gap-2 mt-2 pt-3 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 type="submit" id="manual-absence-submit-btn"
class="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-bold shadow-xs transition">
Сохранить
</button>
</div>
</div>
</form>
</div>
</div>
@@ -0,0 +1,101 @@
<div id="presence-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl h-[88vh] flex flex-col overflow-hidden border border-slate-200">
<!-- Шапка -->
<div class="px-6 py-4 border-b border-slate-200 bg-slate-50/80 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-600 flex items-center justify-center font-bold text-xl">
🏢
</div>
<div>
<h3 class="text-lg font-bold text-slate-800">Оперативный мониторинг присутствия</h3>
<p id="presence-modal-subtitle" class="text-xs text-slate-500">Загрузка данных...</p>
</div>
</div>
<div class="flex items-center space-x-2">
<button id="btn-presence-force-refresh" class="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-semibold flex items-center space-x-1.5 transition-colors shadow-sm">
<span>🔄</span>
<span>Запросить из СКУД</span>
</button>
<button id="btn-close-presence-modal" class="text-slate-400 hover:text-slate-600 p-2 rounded-lg hover:bg-slate-100 transition-colors">
✕
</button>
</div>
</div>
<!-- Метрики (плашки) - 7 колонок для точного баланса штата 1С -->
<div class="grid grid-cols-2 sm:grid-cols-7 gap-2 p-4 bg-slate-100/60 border-b border-slate-200">
<div class="bg-white p-2.5 rounded-xl border border-slate-200/80 text-center">
<div class="text-[11px] text-slate-500 font-medium">Штат 1С</div>
<div id="metric-total" class="text-lg font-bold text-slate-800">—</div>
</div>
<div class="bg-emerald-50 p-2.5 rounded-xl border border-emerald-200/80 text-center">
<div class="text-[11px] text-emerald-700 font-medium">В здании</div>
<div id="metric-inside" class="text-lg font-bold text-emerald-700">—</div>
</div>
<div class="bg-amber-50 p-2.5 rounded-xl border border-amber-200/80 text-center">
<div class="text-[11px] text-amber-700 font-medium">Вышли</div>
<div id="metric-outside" class="text-lg font-bold text-amber-700">—</div>
</div>
<div class="bg-blue-50 p-2.5 rounded-xl border border-blue-200/80 text-center">
<div class="text-[11px] text-blue-700 font-medium">Удаленка</div>
<div id="metric-remote" class="text-lg font-bold text-blue-700">—</div>
</div>
<div class="bg-purple-50 p-2.5 rounded-xl border border-purple-200/80 text-center">
<div class="text-[11px] text-purple-700 font-medium">Отпуск/Ком.</div>
<div id="metric-absence" class="text-lg font-bold text-purple-700">—</div>
</div>
<div class="bg-rose-50 p-2.5 rounded-xl border border-rose-200/80 text-center">
<div class="text-[11px] text-rose-700 font-medium">Не пришли</div>
<div id="metric-not-entered" class="text-lg font-bold text-rose-700">—</div>
</div>
<div class="bg-slate-200/70 p-2.5 rounded-xl border border-slate-300 text-center">
<div class="text-[11px] text-slate-600 font-medium">Исключения</div>
<div id="metric-excluded" class="text-lg font-bold text-slate-700">—</div>
</div>
</div>
<!-- Панель управления и фильтрации -->
<div class="p-4 border-b border-slate-200 flex flex-wrap items-center justify-between gap-3 bg-white">
<!-- Табы статусов -->
<div class="flex items-center space-x-1 overflow-x-auto bg-slate-100 p-1 rounded-xl text-xs font-semibold">
<button class="presence-tab-btn px-3 py-1.5 rounded-lg bg-white shadow-xs text-slate-800" data-tab="ALL">Все (<span id="tab-cnt-all">0</span>)</button>
<button class="presence-tab-btn px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900" data-tab="INSIDE">В здании (<span id="tab-cnt-inside">0</span>)</button>
<button class="presence-tab-btn px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900" data-tab="OUTSIDE">Вышли (<span id="tab-cnt-outside">0</span>)</button>
<button class="presence-tab-btn px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900" data-tab="REMOTE">Удаленка (<span id="tab-cnt-remote">0</span>)</button>
<button class="presence-tab-btn px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900" data-tab="ABSENCE">Отсутствуют (<span id="tab-cnt-absence">0</span>)</button>
<button class="presence-tab-btn px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900" data-tab="NOT_ENTERED">Не пришли (<span id="tab-cnt-not-entered">0</span>)</button>
<button class="presence-tab-btn px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900" data-tab="EXCLUDED">Исключения (<span id="tab-cnt-excluded">0</span>)</button>
</div>
<!-- Поиск по ФИО и фильтр отдела -->
<div class="flex items-center space-x-2 w-full sm:w-auto">
<input type="text" id="presence-search-input" placeholder="🔍 Поиск сотрудника..." class="text-xs px-3 py-1.5 bg-slate-50 border border-slate-200 rounded-lg w-48 focus:outline-emerald-500 focus:bg-white">
<select id="presence-dept-filter" class="text-xs px-3 py-1.5 bg-slate-50 border border-slate-200 rounded-lg max-w-[180px] focus:outline-emerald-500">
<option value="ALL">Все подразделения</option>
</select>
</div>
</div>
<!-- Таблица сотрудников -->
<div class="flex-1 overflow-y-auto p-4">
<table class="w-full text-left text-xs border-collapse">
<thead class="sticky top-0 bg-slate-50 border-b border-slate-200 text-slate-500 uppercase tracking-wider">
<tr>
<th class="py-2.5 px-3 font-semibold">Сотрудник</th>
<th class="py-2.5 px-3 font-semibold">Отдел</th>
<th class="py-2.5 px-3 font-semibold">Должность</th>
<th class="py-2.5 px-3 font-semibold text-center">Статус</th>
<th class="py-2.5 px-3 font-semibold text-center">Отметка</th>
</tr>
</thead>
<tbody id="presence-table-body" class="divide-y divide-slate-100">
<tr>
<td colspan="5" class="py-8 text-center text-slate-400">Загрузка данных...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@@ -13,10 +13,11 @@
<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="Например: Иванов Иван Иванович"
<div class="relative">
<label class="block text-[11px] font-bold text-slate-600 mb-1">ФИО сотрудника (автоподбор из 1С):</label>
<input type="text" id="rw-fio" required autocomplete="off" 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 id="rw-fio-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>
@@ -0,0 +1,94 @@
<div id="snapshot-inspector-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 w-full max-w-6xl h-[90vh] flex flex-col overflow-hidden">
<!-- ШАПКА МОДАЛКИ ИНСПЕКЦИИ СРЕЗА -->
<div class="px-6 py-4 bg-slate-50 border-b border-slate-200 flex items-center justify-between shrink-0">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-xl bg-indigo-600 text-white flex items-center justify-center shadow-xs">
<i class="fa-solid fa-magnifying-glass-chart text-sm"></i>
</div>
<div>
<h3 id="inspector-modal-title" class="text-sm font-bold text-slate-800">Инспекция среза</h3>
<p id="inspector-modal-subtitle" class="text-[11px] text-slate-400">Загрузка данных...</p>
</div>
</div>
<!-- КНОПКИ ЭКСПОРТА И ЗАКРЫТИЯ -->
<div class="flex items-center gap-2">
<button onclick="downloadInspectorExport('xlsx')"
class="px-3 py-1.5 bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border border-emerald-200 rounded-lg text-xs font-semibold transition flex items-center gap-1.5 shadow-xs"
title="Скачать срез в формате Excel (.xlsx)">
<i class="fa-solid fa-file-excel text-emerald-600"></i>
<span>Excel</span>
</button>
<button onclick="downloadInspectorExport('csv')"
class="px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 border border-slate-200 rounded-lg text-xs font-semibold transition flex items-center gap-1.5 shadow-xs"
title="Скачать срез в CSV (для анализа нейросетями)">
<i class="fa-solid fa-file-csv text-slate-600"></i>
<span>CSV</span>
</button>
<button onclick="closeSnapshotInspectorModal()"
class="text-slate-400 hover:text-slate-600 p-2 rounded-lg hover:bg-slate-200 transition ml-1"
title="Закрыть">
<i class="fa-solid fa-xmark text-base"></i>
</button>
</div>
</div>
<!-- ПАНЕЛЬ ФИЛЬТРОВ И ПОИСКА -->
<div class="px-6 py-3 bg-white border-b border-slate-200 flex flex-wrap items-center gap-3 shrink-0">
<div class="flex-1 min-w-[240px] relative">
<i class="fa-solid fa-magnifying-glass absolute left-3 top-2.5 text-slate-400 text-xs"></i>
<input type="text" id="inspector-search-input" oninput="filterInspectorTable()" placeholder="Поиск по ФИО или должности..."
class="w-full text-xs pl-8 pr-3 py-2 bg-slate-50 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500" />
</div>
<div class="flex items-center gap-2">
<select id="inspector-filter-status" onchange="filterInspectorTable()" class="text-xs px-3 py-2 bg-slate-50 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500">
<option value="ALL">Все статусы</option>
<option value="PRESENT">Присутствовал</option>
<option value="ABSENT">Отсутствовал</option>
<option value="ANOMALY">Аномалии</option>
</select>
<select id="inspector-filter-dept" onchange="filterInspectorTable()" class="text-xs px-3 py-2 bg-slate-50 border border-slate-300 rounded-lg focus:outline-none focus:border-indigo-500">
<option value="ALL">Все подразделения</option>
</select>
</div>
</div>
<!-- ТАБЛИЦА ДАННЫХ (СКРОЛЛИРУЕМАЯ ОБЛАСТЬ НА ВСЮ ВЫСОТУ) -->
<div class="flex-1 overflow-y-auto p-6 bg-slate-50">
<div class="bg-white border border-slate-200 rounded-xl shadow-xs overflow-hidden">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-100 border-b border-slate-200 text-[11px] font-bold text-slate-600 uppercase tracking-wider">
<th class="p-3">#</th>
<th class="p-3">Сотрудник</th>
<th class="p-3">Подразделение</th>
<th class="p-3">Вход</th>
<th class="p-3">Первая акт.</th>
<th class="p-3">Выход</th>
<th class="p-3">В здании</th>
<th class="p-3">Статус</th>
</tr>
</thead>
<tbody id="inspector-table-body" class="divide-y divide-slate-100 text-xs text-slate-700">
<tr><td colspan="8" class="text-center py-8 text-slate-400">Выберите срез для инспекции</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ПОДВАЛ МОДАЛКИ -->
<div class="px-6 py-3 bg-white border-t border-slate-200 flex items-center justify-between shrink-0">
<span id="inspector-records-count" class="text-xs font-semibold text-slate-500">Записей не найдено</span>
<button onclick="closeSnapshotInspectorModal()" class="px-4 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-xs font-bold transition">
Закрыть
</button>
</div>
</div>
</div>