feat(turnstile): двухконтурный учет СКУД, реестры исключений с автокомплитом 1С и калибровка таймзон

This commit is contained in:
2026-09-10 15:36:32 +03:00
parent 6c9b131cf2
commit 74332aa38a
31 changed files with 4509 additions and 10769 deletions
+8 -13
View File
@@ -25,27 +25,22 @@ function saveCommandToHistory(commandText) {
chatHistoryIndex = -1;
}
// ⭐️ ЕДИНАЯ ФУНКЦИЯ СКРОЛЛА: плавное выравнивание вопроса к верхней границе
// ⭐️ ЕДИНАЯ ФУНКЦИЯ СКРОЛЛА: выравнивание вопроса к верхней границе
function scrollToUserMessageTop() {
const container = document.getElementById("chat-messages-container");
if (!container) return;
const userBubbles = container.querySelectorAll(".user-chat-bubble");
const targetEl = userBubbles[userBubbles.length - 1] || container.lastElementChild;
const targetEl = userBubbles[userBubbles.length - 1];
if (!targetEl) return;
requestAnimationFrame(() => {
setTimeout(() => {
const containerTop = container.getBoundingClientRect().top;
const targetTop = targetEl.getBoundingClientRect().top;
const targetScroll = container.scrollTop + (targetTop - containerTop) - 16;
const targetScroll = targetEl.offsetTop - container.offsetTop - 12;
container.scrollTo({
top: Math.max(0, targetScroll),
behavior: 'smooth'
});
}, 50);
container.scrollTo({
top: Math.max(0, targetScroll),
behavior: 'smooth'
});
});
}
@@ -99,7 +94,7 @@ function appendUserMessage(text, filename = null) {
}
const msgHtml = `
<div id="${msgId}" class="user-chat-bubble flex gap-3 max-w-4xl mx-auto w-full justify-end pt-3 scroll-mt-4">
<div id="${msgId}" class="user-chat-bubble relative flex gap-3 max-w-4xl mx-auto w-full justify-end pt-3 scroll-mt-4">
<div class="flex-1 max-w-2xl bg-indigo-600 text-white rounded-2xl rounded-tr-none p-4 shadow-sm">
${fileBadge}
<div class="text-sm leading-relaxed whitespace-pre-wrap">${escapeHtml(text)}</div>
@@ -0,0 +1,231 @@
/**
* ===============================================================================
* FILE: modules/web_api/static/js/manual_absences.js
* ROLE: Модальные окна "Мест. командир.", "Иное", живой автокомплит ФИО из 1С:ЗУП
* и мгновенная синхронизация с боковой панелью SidebarManager.
* ===============================================================================
*/
let activeAbsenceType = 'LOCAL_TRIP'; // 'LOCAL_TRIP' или 'OTHER'
let reasonsCache = [];
async function loadAbsenceReasons() {
try {
const res = await fetch('/api/v1/manual-absences/reasons');
if (res.ok) {
const data = await res.json();
reasonsCache = data.reasons || [];
}
} catch (e) {
console.error('Ошибка загрузки причин:', e);
}
}
function openManualAbsenceModal(type) {
activeAbsenceType = type;
const isTrip = type === 'LOCAL_TRIP';
const titleEl = document.getElementById('manual-absence-modal-title');
const reasonBlock = document.getElementById('manual-absence-reason-block');
const reasonSelect = document.getElementById('manual-absence-reason-select');
if (titleEl) {
titleEl.innerHTML = isTrip
? '<i class="fa-solid fa-location-dot text-indigo-600 mr-2"></i>Местная командировка'
: '<i class="fa-solid fa-clipboard-list text-purple-600 mr-2"></i>Иные причины отсутствия';
}
if (reasonBlock && reasonSelect) {
if (isTrip) {
reasonBlock.classList.add('hidden');
} else {
reasonBlock.classList.remove('hidden');
reasonSelect.innerHTML = reasonsCache.map(r => `<option value="${r}">${r}</option>`).join('');
}
}
// Сброс полей ввода
const fioInput = document.getElementById('manual-absence-fio-input');
const deptInput = document.getElementById('manual-absence-dept');
const posInput = document.getElementById('manual-absence-pos');
const startDateInput = document.getElementById('manual-absence-start-date');
const endDateInput = document.getElementById('manual-absence-end-date');
const suggestionsBox = document.getElementById('manual-absence-suggestions');
if (fioInput) fioInput.value = '';
if (deptInput) deptInput.value = '';
if (posInput) posInput.value = '';
if (startDateInput) startDateInput.value = '';
if (suggestionsBox) {
suggestionsBox.classList.add('hidden');
suggestionsBox.innerHTML = '';
}
// Окончание по умолчанию — сегодняшний день
if (endDateInput) {
const today = new Date().toISOString().split('T')[0];
endDateInput.value = today;
}
loadManualAbsencesTable();
const modal = document.getElementById('manual-absence-modal');
if (modal) modal.classList.remove('hidden');
}
function closeManualAbsenceModal() {
const modal = document.getElementById('manual-absence-modal');
if (modal) modal.classList.add('hidden');
}
// Живой автокомплит ФИО из базы zup_staff
let searchTimeout = null;
function setupStaffAutocomplete(inputEl, suggestionsBoxId) {
const box = document.getElementById(suggestionsBoxId);
if (!inputEl || !box) return;
inputEl.addEventListener('input', function() {
const val = this.value.trim();
clearTimeout(searchTimeout);
if (val.length < 2) {
box.classList.add('hidden');
box.innerHTML = '';
return;
}
searchTimeout = setTimeout(async () => {
try {
const res = await fetch(`/api/v1/manual-absences/staff-autocomplete?q=${encodeURIComponent(val)}`);
if (!res.ok) return;
const items = await res.json();
if (items.length === 0) {
box.classList.add('hidden');
return;
}
box.innerHTML = items.map(it => `
<div class="p-2 hover:bg-indigo-50 cursor-pointer border-b border-slate-100 flex flex-col text-xs"
onclick="selectStaffSuggestion('${escapeHtml(it.fio)}', '${escapeHtml(it.department)}', '${escapeHtml(it.position)}')">
<span class="font-bold text-slate-800">${escapeHtml(it.fio)}</span>
<span class="text-[10px] text-slate-500">${escapeHtml(it.department)} · ${escapeHtml(it.position)}</span>
</div>
`).join('');
box.classList.remove('hidden');
} catch (e) {
console.error(e);
}
}, 200);
});
}
function selectStaffSuggestion(fio, dept, pos) {
const fioInput = document.getElementById('manual-absence-fio-input');
const deptInput = document.getElementById('manual-absence-dept');
const posInput = document.getElementById('manual-absence-pos');
const box = document.getElementById('manual-absence-suggestions');
if (fioInput) fioInput.value = fio;
if (deptInput) deptInput.value = dept;
if (posInput) posInput.value = pos;
if (box) box.classList.add('hidden');
}
async function submitManualAbsence() {
const fioInput = document.getElementById('manual-absence-fio-input');
const fio = fioInput ? fioInput.value.trim() : '';
if (!fio) {
alert('Укажите ФИО сотрудника');
return;
}
const deptVal = document.getElementById('manual-absence-dept')?.value.trim() || '';
const posVal = document.getElementById('manual-absence-pos')?.value.trim() || '';
const startDateVal = document.getElementById('manual-absence-start-date')?.value || null;
const endDateVal = document.getElementById('manual-absence-end-date')?.value || null;
const reasonSelect = document.getElementById('manual-absence-reason-select');
const payload = {
absence_type: activeAbsenceType,
fio: fio,
department: deptVal,
position: posVal,
date_start: startDateVal,
date_end: endDateVal,
reason: activeAbsenceType === 'LOCAL_TRIP' ? 'Местная командировка' : (reasonSelect ? reasonSelect.value : 'Иное')
};
try {
const res = await fetch('/api/v1/manual-absences/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (res.ok) {
if (fioInput) fioInput.value = '';
loadManualAbsencesTable();
// Обновляем список карточек в боковой панели и закрываем окно
if (window.SidebarManager && typeof SidebarManager.renderContent === 'function') {
SidebarManager.renderContent();
}
closeManualAbsenceModal();
} else {
alert('Ошибка добавления записи');
}
} catch (e) {
alert('Сетевая ошибка при добавлении');
}
}
async function loadManualAbsencesTable() {
const tableContainer = document.getElementById('manual-absences-table-body');
if (!tableContainer) return;
try {
const res = await fetch(`/api/v1/manual-absences/?type=${activeAbsenceType}`);
if (!res.ok) return;
const data = await res.json();
const items = data.items || [];
if (items.length === 0) {
tableContainer.innerHTML = '<tr><td colspan="5" class="text-center p-4 text-xs text-slate-400">Нет активных записей</td></tr>';
return;
}
tableContainer.innerHTML = items.map(it => `
<tr class="border-b border-slate-100 text-xs hover:bg-slate-50">
<td class="p-2 font-bold text-slate-800">${escapeHtml(it.fio)}</td>
<td class="p-2 text-slate-500">${escapeHtml(it.department || '—')}</td>
<td class="p-2 text-slate-600">${escapeHtml(it.reason)}</td>
<td class="p-2 text-center text-slate-500 font-mono text-[11px]">${it.date_start || '—'} / ${it.date_end || '—'}</td>
<td class="p-2 text-center">
<button onclick="deleteManualAbsenceRecord(${it.id})" class="text-slate-400 hover:text-rose-600 transition p-1" title="Удалить">
<i class="fa-solid fa-trash-can"></i>
</button>
</td>
</tr>
`).join('');
} catch (e) {
console.error(e);
}
}
async function deleteManualAbsenceRecord(id) {
if (!confirm('Удалить эту запись?')) return;
try {
const res = await fetch(`/api/v1/manual-absences/${id}`, { method: 'DELETE' });
if (res.ok) {
loadManualAbsencesTable();
if (window.SidebarManager && typeof SidebarManager.renderContent === 'function') {
SidebarManager.renderContent();
}
}
} catch (e) {
alert('Ошибка при удалении');
}
}
document.addEventListener('DOMContentLoaded', () => {
loadAbsenceReasons();
setupStaffAutocomplete(
document.getElementById('manual-absence-fio-input'),
'manual-absence-suggestions'
);
});
+235 -34
View File
@@ -1,12 +1,11 @@
/**
* ===============================================================================
* FILE: modules/web_api/static/js/sidebar.js
* ROLE: Контроллер левого сайдбара с 5-хабовой навигацией, реестрами,
* подробным описанием управления контекстом и интеграцией чата.
* ROLE: Контроллер левого сайдбара с 5-хабовой навигацией, реестрами (2x2),
* модальным окном добавления исключений с автокомплитом из 1С:ЗУП.
* ===============================================================================
*/
// Глобальная функция безопасного экранирования HTML
window.escapeHtml = function(str) {
if (str === null || str === undefined) return '';
return String(str)
@@ -31,10 +30,13 @@ window.SidebarManager = {
{ id: 'CONTEXT', label: 'Контекст', icon: 'fa-comments' }
],
// Сетка реестров 2x2
subTabs: {
'REGISTRIES': [
{ id: 'REMOTE', label: 'Удаленщики', icon: 'fa-house-laptop' },
{ id: 'EXCEPTIONS', label: 'Исключения', icon: 'fa-user-shield' }
{ id: 'EXCEPTIONS', label: 'Исключения', icon: 'fa-user-shield' },
{ id: 'LOCAL_TRIP', label: 'Мест. командир.', icon: 'fa-location-dot' },
{ id: 'OTHER', label: 'Иное', icon: 'fa-clipboard-list' }
]
},
@@ -79,17 +81,17 @@ window.SidebarManager = {
</div>
`;
// 2. Подвкладки (только для хабов, где они требуются, например Реестры)
// 2. Подвкладки реестров (Сетка 2x2)
let subTabsHtml = '';
if (this.subTabs[this.currentHub]) {
const currentActiveSub = this.currentSubTab[this.currentHub] || this.subTabs[this.currentHub][0].id;
subTabsHtml = `
<div class="flex items-center gap-1.5 p-1.5 bg-slate-100/90 border-b border-slate-200">
<div class="grid grid-cols-2 gap-1.5 p-1.5 bg-slate-100/90 border-b border-slate-200">
${this.subTabs[this.currentHub].map(st => {
const isSubActive = currentActiveSub === st.id;
return `
<button onclick="SidebarManager.setSubTab('${st.id}')"
class="flex-1 py-1 px-2 rounded-md text-[11px] font-semibold flex items-center justify-center gap-1.5 transition ${
class="py-1 px-2 rounded-md text-[11px] font-semibold flex items-center justify-center gap-1.5 transition ${
isSubActive
? 'bg-white text-indigo-700 shadow-sm'
: 'text-slate-600 hover:text-slate-900 hover:bg-white/50'
@@ -196,14 +198,16 @@ window.SidebarManager = {
},
// =========================================================================
// ХАБ 3: РЕЕСТРЫ (УДАЛЕНЩИКИ + ИСКЛЮЧЕНИЯ)
// ХАБ 3: РЕЕСТРЫ (УДАЛЕНЩИКИ + ИСКЛЮЧЕНИЯ + МЕСТ. КОМАНДИР. + ИНОЕ)
// =========================================================================
renderRegistriesView(container) {
const subTab = this.currentSubTab['REGISTRIES'] || 'REMOTE';
if (subTab === 'REMOTE') {
this.renderRemoteWorkersView(container);
} else {
} else if (subTab === 'EXCEPTIONS') {
this.renderExceptionsView(container);
} else {
this.renderManualAbsencesView(container, subTab);
}
},
@@ -289,22 +293,32 @@ window.SidebarManager = {
{ key: 'include_fio', title: 'Белый список (ФИО)' },
{ key: 'fio', title: 'Исключенные сотрудники (ФИО)' },
{ key: 'departments', title: 'Исключенные отделы' },
{ key: 'positions', title: 'Исключенные должности' }
{ key: 'positions', title: 'Исключенные должности' },
{ key: 'turnstile_fio', title: 'Пр. турникет (ФИО)', badge: 'Оба турникета' },
{ key: 'turnstile_departments', title: 'Пр. турникет (Отделы)', badge: 'Оба турникета' }
];
const html = categories.map(cat => {
const items = data[cat.key] || [];
const isTurnstile = cat.key.startsWith('turnstile_');
const badgeHtml = cat.badge
? `<span class="px-1.5 py-0.2 rounded text-[9px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200">${cat.badge}</span>`
: '';
return `
<div class="bg-white border border-slate-200 rounded-xl p-3 flex flex-col gap-2 shadow-sm">
<div class="bg-white border ${isTurnstile ? 'border-emerald-200/80 bg-emerald-50/10' : 'border-slate-200'} rounded-xl p-3 flex flex-col gap-2 shadow-sm">
<div class="flex items-center justify-between">
<span class="font-bold text-xs text-slate-700">${cat.title} (${items.length})</span>
<button onclick="SidebarManager.addExceptionPrompt('${cat.key}')" class="text-indigo-600 hover:text-indigo-800 text-xs font-bold">
<div class="flex items-center gap-1.5">
<span class="font-bold text-xs text-slate-700">${cat.title} (${items.length})</span>
${badgeHtml}
</div>
<button onclick="openExceptionModal('${cat.key}', '${cat.title}')" class="text-indigo-600 hover:text-indigo-800 text-xs font-bold">
+ Добавить
</button>
</div>
<div class="flex flex-wrap gap-1">
${items.map(it => `
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] bg-slate-100 text-slate-700 border border-slate-200">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] ${isTurnstile ? 'bg-emerald-50 text-emerald-800 border border-emerald-200' : 'bg-slate-100 text-slate-700 border border-slate-200'}">
${escapeHtml(it)}
<button onclick="SidebarManager.deleteExceptionItem('${cat.key}', '${escapeHtml(it)}')" class="hover:text-rose-600 ml-0.5">×</button>
</span>
@@ -320,24 +334,8 @@ window.SidebarManager = {
}
},
async addExceptionPrompt(category) {
const val = prompt(`Введите значение для категории [${category}]:`);
if (!val || !val.trim()) return;
try {
const res = await fetch("/api/v1/exceptions/", {
method: "POST",
headers: AuthManager.getAuthHeaders(),
body: JSON.stringify({ category: category, value: val.trim() })
});
if (res.ok) this.renderContent();
else alert("Ошибка добавления");
} catch (e) {
alert("Ошибка сети");
}
},
async deleteExceptionItem(category, value) {
if (!confirm(`Удалить "${value}" из ${category}?`)) return;
if (!confirm(`Удалить "${value}" из реестра?`)) return;
try {
const res = await fetch(`/api/v1/exceptions/?category=${encodeURIComponent(category)}&value=${encodeURIComponent(value)}`, {
method: "DELETE",
@@ -350,6 +348,73 @@ window.SidebarManager = {
}
},
async renderManualAbsencesView(container, absenceType) {
const typeLabel = absenceType === 'LOCAL_TRIP' ? 'местных командировок' : 'иных отсутствий';
container.innerHTML = `<div class="text-center py-8 text-xs text-slate-400"><i class="fa-solid fa-spinner fa-spin mr-1"></i> Загрузка ${typeLabel}...</div>`;
try {
const res = await fetch(`/api/v1/manual-absences/?type=${absenceType}`);
const data = res.ok ? await res.json() : { items: [] };
const items = data.items || [];
const listHtml = items.map(it => {
const dFrom = it.date_start ? it.date_start : 'сегодня';
const dTo = it.date_end ? it.date_end : 'сегодня';
const periodLabel = (dFrom === dTo) ? `на ${dFrom}` : `${dFrom} — ${dTo}`;
const badgeText = absenceType === 'LOCAL_TRIP' ? 'Местная командировка' : escapeHtml(it.reason);
return `
<div class="flex items-center justify-between p-2.5 bg-white border border-slate-200 rounded-xl text-xs gap-2 shadow-sm hover:border-indigo-300 transition">
<div class="min-w-0 flex-1">
<div class="font-bold text-slate-800 truncate">${escapeHtml(it.fio)}</div>
<div class="text-[10px] text-slate-400 truncate">${escapeHtml(it.department || 'Все')} · ${badgeText}</div>
<div class="mt-0.5 inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-indigo-50 text-indigo-700 border border-indigo-200">
<i class="fa-regular fa-calendar-days text-[8px]"></i>
<span>${escapeHtml(periodLabel)}</span>
</div>
</div>
<div class="flex items-center gap-0.5 shrink-0">
<button onclick="SidebarManager.deleteManualAbsenceRecord(${it.id})"
class="text-slate-400 hover:text-rose-600 p-1.5 rounded-lg hover:bg-rose-50 transition"
title="Удалить">
<i class="fa-solid fa-trash-can text-xs"></i>
</button>
</div>
</div>
`;
}).join('');
container.innerHTML = `
<div class="p-2 flex flex-col gap-2.5">
<div class="flex items-center justify-between px-1">
<span class="text-xs font-bold text-slate-700">В реестре: ${items.length} чел.</span>
<button onclick="openManualAbsenceModal('${absenceType}')" class="px-2 py-1 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-[11px] font-bold shadow-sm flex items-center gap-1 transition">
<i class="fa-solid fa-plus text-[10px]"></i> Добавить
</button>
</div>
<div class="flex flex-col gap-1.5 max-h-[70vh] overflow-y-auto">
${items.length > 0 ? listHtml : '<div class="text-center py-8 text-xs text-slate-400">Список пуст</div>'}
</div>
</div>
`;
} catch (e) {
container.innerHTML = `<div class="p-4 text-xs text-rose-500 text-center">Ошибка загрузки реестра</div>`;
}
},
async deleteManualAbsenceRecord(id) {
if (!confirm("Удалить эту запись из реестра?")) return;
try {
const res = await fetch(`/api/v1/manual-absences/${id}`, { method: "DELETE" });
if (res.ok) {
this.renderContent();
} else {
alert("Ошибка удаления");
}
} catch (e) {
alert("Ошибка сети");
}
},
// =========================================================================
// ХАБ 4: СИСТЕМНЫЙ ПРОМПТ И БАЗА ЗНАНИЙ
// =========================================================================
@@ -381,7 +446,7 @@ window.SidebarManager = {
},
// =========================================================================
// ХАБ 5: УПРАВЛЕНИЕ КОНТЕКСТОМ СЕССИИ (С ПОДРОБНЫМ ОПИСАНИЕМ)
// ХАБ 5: УПРАВЛЕНИЕ КОНТЕКСТОМ СЕССИИ
// =========================================================================
renderContextView(container) {
container.innerHTML = `
@@ -432,7 +497,6 @@ window.SidebarManager = {
return;
}
// Очищаем локальное окно чата до стартового приветствия
const chatContainer = document.getElementById("chat-messages-container");
if (chatContainer) {
chatContainer.innerHTML = `
@@ -465,8 +529,145 @@ window.SidebarManager = {
}
};
// ============================================================================
// МОДАЛЬНОЕ ОКНО ИСКЛЮЧЕНИЙ И АВТОКОМПЛИТ 1С
// ============================================================================
window.openExceptionModal = function(category, title = "") {
const modal = document.getElementById("exception-modal");
const headerText = document.getElementById("exception-modal-header-text");
const catInput = document.getElementById("exception-category-input");
const valInput = document.getElementById("exception-value-input");
const labelEl = document.getElementById("exception-value-label");
const commentInput = document.getElementById("exception-comment-input");
const errEl = document.getElementById("exception-error-msg");
const suggestionsBox = document.getElementById("exception-suggestions");
if (!modal) return;
if (errEl) errEl.classList.add("hidden");
if (suggestionsBox) {
suggestionsBox.classList.add("hidden");
suggestionsBox.innerHTML = "";
}
if (catInput) catInput.value = category;
if (commentInput) commentInput.value = "";
if (valInput) valInput.value = "";
if (headerText) headerText.innerText = title || "Добавление в реестр";
if (labelEl && valInput) {
if (category.includes("fio")) {
labelEl.innerText = "ФИО сотрудника (автоподбор из 1С):";
valInput.placeholder = "Начните вводить фамилию...";
} else if (category.includes("department")) {
labelEl.innerText = "Подразделение:";
valInput.placeholder = "Например: ЭТО, ЛЦ, ОВК";
} else {
labelEl.innerText = "Должность:";
valInput.placeholder = "Например: Уборщик, Слесарь";
}
}
modal.classList.remove("hidden");
if (valInput) valInput.focus();
};
window.closeExceptionModal = function() {
const modal = document.getElementById("exception-modal");
if (modal) modal.classList.add("hidden");
};
window.submitExceptionModalForm = async function(e) {
e.preventDefault();
const category = document.getElementById("exception-category-input").value;
const value = document.getElementById("exception-value-input").value.trim();
const comment = document.getElementById("exception-comment-input")?.value.trim() || "";
const errEl = document.getElementById("exception-error-msg");
if (!value) return;
try {
const res = await fetch("/api/v1/exceptions/", {
method: "POST",
headers: AuthManager.getAuthHeaders(),
body: JSON.stringify({ category: category, value: value, comment: comment })
});
if (res.ok) {
closeExceptionModal();
if (window.SidebarManager) SidebarManager.renderContent();
} else {
const err = await res.json().catch(() => ({}));
if (errEl) {
errEl.innerText = err.detail || "Ошибка сохранения";
errEl.classList.remove("hidden");
}
}
} catch (err) {
if (errEl) {
errEl.innerText = "Ошибка соединения с сервером";
errEl.classList.remove("hidden");
}
}
};
let excSearchTimeout = null;
document.addEventListener("DOMContentLoaded", () => {
if (window.AuthManager && AuthManager.isAuthenticated()) {
SidebarManager.init();
}
});
const inputEl = document.getElementById("exception-value-input");
const box = document.getElementById("exception-suggestions");
if (inputEl && box) {
inputEl.addEventListener("input", function() {
const category = document.getElementById("exception-category-input")?.value || "";
if (!category.includes("fio")) {
box.classList.add("hidden");
return;
}
const val = this.value.trim();
clearTimeout(excSearchTimeout);
if (val.length < 2) {
box.classList.add("hidden");
box.innerHTML = "";
return;
}
excSearchTimeout = setTimeout(async () => {
try {
const res = await fetch(`/api/v1/manual-absences/staff-autocomplete?q=${encodeURIComponent(val)}`);
if (!res.ok) return;
const items = await res.json();
if (items.length === 0) {
box.classList.add("hidden");
return;
}
box.innerHTML = items.map(it => `
<div class="p-2 hover:bg-indigo-50 cursor-pointer border-b border-slate-100 flex flex-col text-xs"
onclick="selectExceptionStaff('${escapeHtml(it.fio)}')">
<span class="font-bold text-slate-800">${escapeHtml(it.fio)}</span>
<span class="text-[10px] text-slate-500">${escapeHtml(it.department)} · ${escapeHtml(it.position)}</span>
</div>
`).join("");
box.classList.remove("hidden");
} catch (e) {
console.error(e);
}
}, 200);
});
}
});
window.selectExceptionStaff = function(fio) {
const inputEl = document.getElementById("exception-value-input");
const box = document.getElementById("exception-suggestions");
if (inputEl) inputEl.value = fio;
if (box) {
box.classList.add("hidden");
box.innerHTML = "";
}
};