You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
7.0 KiB
169 lines
7.0 KiB
{% extends "base.html" %} |
|
{% block title %}Назначения — Тестирование персонала{% endblock %} |
|
|
|
{% block content %} |
|
<div class="space-y-4 sm:space-y-5 pb-10"> |
|
|
|
<div class="flex items-center justify-between gap-3"> |
|
<h1 class="text-xl font-semibold text-ink-900">Назначения</h1> |
|
<a href="{{ url_for('main.index') }}" class="link-back text-sm">← Главная</a> |
|
</div> |
|
|
|
{# Выбор теста #} |
|
<div class="cabinet-brick"> |
|
<h2 class="font-semibold text-sm text-ink-700 mb-3">Тест</h2> |
|
<select id="assign-test-select" class="form-input"> |
|
<option value="">— Выберите тест —</option> |
|
{% for t in tests %} |
|
<option value="{{ t.id }}">{{ t.title }}</option> |
|
{% endfor %} |
|
</select> |
|
</div> |
|
|
|
{# Панель назначения (скрыта пока не выбран тест) #} |
|
<div id="assign-panel" class="cabinet-brick hidden"> |
|
<h2 class="font-semibold text-sm text-ink-700 mb-3">Кому выдать</h2> |
|
<p class="test-detail-hint">Список с учётом поиска и фильтров; можно отметить всех на экране.</p> |
|
<div class="assign-toolbar mt-3"> |
|
<input id="assign-search" class="form-input assign-toolbar__search" type="text" placeholder="Поиск: ФИО, логин" /> |
|
<select id="assign-dept" class="form-input"><option value="__all__">Все отделы</option></select> |
|
<select id="assign-clinic" class="form-input"> |
|
<option value="all">Все сотрудники</option> |
|
<option value="with">Уже есть в системе</option> |
|
<option value="without">Ещё не в системе</option> |
|
</select> |
|
<button id="assign-select-all" class="btn btn-ghost btn--sm" type="button">Выбрать всех</button> |
|
</div> |
|
<div id="assign-list" class="assign-list mt-3"></div> |
|
<div class="inline-actions mt-3"> |
|
<button id="assign-submit" class="btn btn-primary" type="button">Назначить выбранным</button> |
|
<span id="assign-status" class="muted"></span> |
|
</div> |
|
</div> |
|
|
|
</div> |
|
{% endblock %} |
|
|
|
{% block scripts %} |
|
<script> |
|
(function () { |
|
const $ = (s) => document.querySelector(s); |
|
const testSelect = $('#assign-test-select'); |
|
const assignPanel = $('#assign-panel'); |
|
const assignSearchEl = $('#assign-search'); |
|
const assignDeptEl = $('#assign-dept'); |
|
const assignClinicEl = $('#assign-clinic'); |
|
const assignListEl = $('#assign-list'); |
|
const assignSelectAllBtn = $('#assign-select-all'); |
|
const assignSubmitBtn = $('#assign-submit'); |
|
const assignStatusEl = $('#assign-status'); |
|
|
|
let currentTestId = null; |
|
let assignPeople = []; |
|
let assignSelected = new Set(); |
|
|
|
function escHtml(s) { |
|
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); |
|
} |
|
|
|
function renderAssignList() { |
|
assignListEl.innerHTML = ''; |
|
assignPeople.forEach((p) => { |
|
const row = document.createElement('label'); |
|
row.className = `assign-row${assignSelected.has(String(p.staffId)) ? ' assign-row--selected' : ''}`; |
|
row.innerHTML = ` |
|
<input type="checkbox" ${assignSelected.has(String(p.staffId)) ? 'checked' : ''} /> |
|
<span class="assign-row__text"> |
|
<span class="assign-row__fio">${escHtml(p.fio || '—')}</span> |
|
${p.webLogin ? `<span class="assign-row__login">${escHtml(p.webLogin)}</span>` : ''} |
|
<span class="assign-row__meta">${escHtml(p.department || '—')}</span> |
|
</span>`; |
|
const cb = row.querySelector('input'); |
|
cb.addEventListener('change', () => { |
|
const k = String(p.staffId); |
|
if (cb.checked) assignSelected.add(k); else assignSelected.delete(k); |
|
row.classList.toggle('assign-row--selected', cb.checked); |
|
}); |
|
assignListEl.appendChild(row); |
|
}); |
|
if (!assignPeople.length) assignListEl.innerHTML = '<p class="muted" style="padding:.75rem;">Никого не найдено.</p>'; |
|
} |
|
|
|
async function loadDirectory() { |
|
if (!currentTestId) return; |
|
assignStatusEl.textContent = 'Загружаем…'; |
|
try { |
|
const params = new URLSearchParams(); |
|
if (assignSearchEl.value.trim()) params.set('q', assignSearchEl.value.trim()); |
|
if (assignDeptEl.value && assignDeptEl.value !== '__all__') params.set('department', assignDeptEl.value); |
|
params.set('clinic', assignClinicEl.value || 'all'); |
|
const r = await fetch(`/api/auth/dev/assignment-directory?${params.toString()}`); |
|
const data = await r.json(); |
|
if (!r.ok) throw new Error(data.error || 'Не удалось загрузить сотрудников'); |
|
assignPeople = data.people || []; |
|
const depts = data.departments || []; |
|
if (assignDeptEl.options.length <= 1) { |
|
depts.forEach((d) => { |
|
const o = document.createElement('option'); |
|
o.value = d; o.textContent = d; |
|
assignDeptEl.appendChild(o); |
|
}); |
|
} |
|
assignSelected = new Set(); |
|
renderAssignList(); |
|
assignStatusEl.textContent = ''; |
|
} catch (e) { |
|
assignStatusEl.textContent = e.message || 'Ошибка загрузки'; |
|
} |
|
} |
|
|
|
testSelect.addEventListener('change', () => { |
|
currentTestId = testSelect.value || null; |
|
if (currentTestId) { |
|
assignPanel.classList.remove('hidden'); |
|
// Сброс фильтров |
|
assignDeptEl.innerHTML = '<option value="__all__">Все отделы</option>'; |
|
assignSearchEl.value = ''; |
|
loadDirectory(); |
|
} else { |
|
assignPanel.classList.add('hidden'); |
|
} |
|
}); |
|
|
|
let t = null; |
|
assignSearchEl.addEventListener('input', () => { clearTimeout(t); t = setTimeout(loadDirectory, 350); }); |
|
assignDeptEl.addEventListener('change', loadDirectory); |
|
assignClinicEl.addEventListener('change', loadDirectory); |
|
|
|
assignSelectAllBtn.addEventListener('click', () => { |
|
assignPeople.forEach((p) => assignSelected.add(String(p.staffId))); |
|
renderAssignList(); |
|
}); |
|
|
|
assignSubmitBtn.addEventListener('click', async () => { |
|
if (!currentTestId) return; |
|
const selectedRows = assignPeople.filter((p) => assignSelected.has(String(p.staffId))); |
|
if (!selectedRows.length) { assignStatusEl.textContent = 'Никто не выбран'; return; } |
|
assignStatusEl.textContent = 'Назначаем…'; |
|
try { |
|
const payload = selectedRows.map((p) => ({ |
|
staffId: p.staffId, |
|
webLogin: p.webLogin || null, |
|
fio: p.fio || null, |
|
department: p.department || null, |
|
})); |
|
const r = await fetch(`/api/tests/${currentTestId}/assign`, { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ targets: payload }), |
|
}); |
|
const data = await r.json(); |
|
if (!r.ok) throw new Error(data.error || 'Ошибка назначения'); |
|
assignStatusEl.textContent = `Назначено: ${data.count ?? selectedRows.length}`; |
|
} catch (e) { |
|
assignStatusEl.textContent = e.message || 'Ошибка назначения'; |
|
} |
|
}); |
|
})(); |
|
</script> |
|
{% endblock %}
|
|
|