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
+124 -140
View File
@@ -1,8 +1,8 @@
/**
* ===============================================================================
* FILE: modules/web_api/static/js/manual_absences.js
* ROLE: Модальные окна "Мест. командир.", "Иное", живой автокомплит ФИО из 1С:ЗУП
* и мгновенная синхронизация с боковой панелью SidebarManager.
* ROLE: Модальные окна "Мест. командир.", "Иное", универсальный автокомплит ФИО
* и синхронизация с боковой панелью SidebarManager.
* ===============================================================================
*/
@@ -21,18 +21,24 @@ async function loadAbsenceReasons() {
}
}
function openManualAbsenceModal(type) {
// ⭐️ Открытие окна: режим создания (item = null) или редактирования (item = {...})
function openManualAbsenceModal(type, editItem = null) {
activeAbsenceType = type;
const isTrip = type === 'LOCAL_TRIP';
const titleEl = document.getElementById('manual-absence-modal-title');
const isTrip = (type === 'LOCAL_TRIP');
const modal = document.getElementById('manual-absence-modal');
const titleEl = document.getElementById('manual-absence-modal-title-text');
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>Иные причины отсутствия';
}
const idInput = document.getElementById('manual-absence-id');
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 errEl = document.getElementById('manual-absence-error');
if (errEl) errEl.classList.add('hidden');
if (reasonBlock && reasonSelect) {
if (isTrip) {
@@ -43,31 +49,37 @@ function openManualAbsenceModal(type) {
}
}
// Сброс полей ввода
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');
const today = new Date().toISOString().split('T')[0];
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;
if (editItem) {
// Режим РЕДАКТИРОВАНИЯ
if (titleEl) titleEl.innerText = isTrip ? 'Изменение сроков командировки' : 'Изменение сроков отсутствия';
if (idInput) idInput.value = editItem.id;
if (fioInput) {
fioInput.value = editItem.fio;
fioInput.readOnly = true;
fioInput.classList.add('bg-slate-100', 'text-slate-500', 'cursor-not-allowed');
}
if (deptInput) deptInput.value = editItem.department || '';
if (posInput) posInput.value = editItem.position || '';
if (startDateInput) startDateInput.value = editItem.date_start ? editItem.date_start.replace(/\./g, '-') : today;
if (endDateInput) endDateInput.value = editItem.date_end ? editItem.date_end.replace(/\./g, '-') : today;
if (reasonSelect && editItem.reason) reasonSelect.value = editItem.reason;
} else {
// Режим СОЗДАНИЯ
if (titleEl) titleEl.innerText = isTrip ? 'Добавить в командировки' : 'Добавить отсутствие';
if (idInput) idInput.value = '';
if (fioInput) {
fioInput.value = '';
fioInput.readOnly = false;
fioInput.classList.remove('bg-slate-100', 'text-slate-500', 'cursor-not-allowed');
}
if (deptInput) deptInput.value = '';
if (posInput) posInput.value = '';
if (startDateInput) startDateInput.value = today;
if (endDateInput) endDateInput.value = today;
}
loadManualAbsencesTable();
const modal = document.getElementById('manual-absence-modal');
if (modal) modal.classList.remove('hidden');
}
@@ -76,7 +88,65 @@ function closeManualAbsenceModal() {
if (modal) modal.classList.add('hidden');
}
// Живой автокомплит ФИО из базы zup_staff
async function submitManualAbsence(event) {
if (event) event.preventDefault();
const idVal = document.getElementById('manual-absence-id')?.value.trim();
const fio = document.getElementById('manual-absence-fio-input')?.value.trim();
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 errEl = document.getElementById('manual-absence-error');
if (!fio) {
if (errEl) { errEl.innerText = 'Укажите ФИО сотрудника'; errEl.classList.remove('hidden'); }
return;
}
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 {
let res;
if (idVal) {
// Редактирование
res = await fetch(`/api/v1/manual-absences/${idVal}`, {
method: 'PUT',
headers: { ...AuthManager.getAuthHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ id: parseInt(idVal), date_start: startDateVal, date_end: endDateVal })
});
} else {
// Создание
res = await fetch('/api/v1/manual-absences/', {
method: 'POST',
headers: { ...AuthManager.getAuthHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
if (res.ok) {
closeManualAbsenceModal();
if (typeof loadManualAbsencesView === 'function') {
loadManualAbsencesView(activeAbsenceType);
}
} else {
const data = await res.json();
if (errEl) { errEl.innerText = data.detail || 'Ошибка сохранения'; errEl.classList.remove('hidden'); }
}
} catch (e) {
if (errEl) { errEl.innerText = 'Сетевая ошибка при сохранении'; errEl.classList.remove('hidden'); }
}
}
// Универсальный автокомплит сотрудников
let searchTimeout = null;
function setupStaffAutocomplete(inputEl, suggestionsBoxId) {
const box = document.getElementById(suggestionsBoxId);
@@ -103,7 +173,7 @@ function setupStaffAutocomplete(inputEl, suggestionsBoxId) {
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)}')">
onmousedown="selectStaffSuggestion('${escapeHtml(it.fio)}', '${escapeHtml(it.department)}', '${escapeHtml(it.position)}', '${inputEl.id}', '${suggestionsBoxId}')">
<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>
@@ -114,118 +184,32 @@ function setupStaffAutocomplete(inputEl, suggestionsBoxId) {
}
}, 200);
});
document.addEventListener('click', (e) => {
if (!inputEl.contains(e.target) && !box.contains(e.target)) {
box.classList.add('hidden');
}
});
}
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');
function selectStaffSuggestion(fio, dept, pos, targetInputId, boxId) {
const targetFioEl = document.getElementById(targetInputId);
if (targetFioEl) targetFioEl.value = fio;
if (fioInput) fioInput.value = fio;
if (deptInput) deptInput.value = dept;
if (posInput) posInput.value = pos;
if (targetInputId === 'rw-fio') {
const rwDept = document.getElementById('rw-dept');
if (rwDept && dept && dept !== '—') rwDept.value = dept;
} else if (targetInputId === 'manual-absence-fio-input') {
const deptInput = document.getElementById('manual-absence-dept');
const posInput = document.getElementById('manual-absence-pos');
if (deptInput) deptInput.value = dept;
if (posInput) posInput.value = pos;
}
const box = document.getElementById(boxId);
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'
);
});