354 lines
12 KiB
JavaScript
354 lines
12 KiB
JavaScript
/**
|
||
* positionManager.js
|
||
* Управление должностями в форме редактирования сотрудника
|
||
* Функции: создание, редактирование, назначение должностей
|
||
*
|
||
* @module positionManager
|
||
* @version 1.0.0
|
||
*/
|
||
|
||
/**
|
||
* Инициализация менеджера должностей
|
||
*/
|
||
export function initPositionManager(options = {}) {
|
||
const form = document.getElementById('apiEditForm');
|
||
const block = document.getElementById('positionBlock');
|
||
if (!form || !block) {
|
||
return null; // Тихий выход, если элементы не найдены
|
||
}
|
||
|
||
const select = document.getElementById('f_position');
|
||
const csrftoken = form.querySelector('input[name=csrfmiddlewaretoken]')?.value || '';
|
||
const createUrl = block.dataset.createUrl || '';
|
||
const assignUrl = block.dataset.assignUrl || '';
|
||
const canAssign = block.dataset.canAssign === '1';
|
||
|
||
function busy(btn, on) {
|
||
const lbl = btn.querySelector('.lbl');
|
||
const sp = btn.querySelector('.spinner-border');
|
||
if (on) {
|
||
btn.setAttribute('disabled', '');
|
||
sp?.classList.remove('d-none');
|
||
lbl?.classList.add('visually-hidden');
|
||
} else {
|
||
btn.removeAttribute('disabled');
|
||
sp?.classList.add('d-none');
|
||
lbl?.classList.remove('visually-hidden');
|
||
}
|
||
}
|
||
|
||
function msg(el, text, ok) {
|
||
if (!el) return;
|
||
el.textContent = text || '';
|
||
el.classList.toggle('d-none', !text);
|
||
el.classList.toggle('text-success', !!ok);
|
||
el.classList.toggle('text-danger', !ok);
|
||
}
|
||
|
||
// Создание новой должности
|
||
document.getElementById('posCreateBtn')?.addEventListener('click', async () => {
|
||
const btn = document.getElementById('posCreateBtn');
|
||
const m = document.getElementById('posCreateMsg');
|
||
msg(m, '');
|
||
|
||
if (!createUrl || !assignUrl) {
|
||
msg(m, 'Не настроены URL создания/назначения.', false);
|
||
return;
|
||
}
|
||
|
||
const name = (document.getElementById('pos_name')?.value || '').trim();
|
||
const description = (document.getElementById('pos_desc')?.value || '').trim();
|
||
if (!name) {
|
||
msg(m, 'Укажите название должности.', false);
|
||
return;
|
||
}
|
||
|
||
let groupIds = [];
|
||
if (canAssign) {
|
||
const msel = document.getElementById('pos_groups');
|
||
const hid = document.getElementById('pos_groups_hidden');
|
||
const csv = document.getElementById('pos_groups_csv');
|
||
|
||
if (msel) groupIds = Array.from(msel.selectedOptions).map((o) => o.value).filter(Boolean);
|
||
else if (hid) groupIds = (hid.value || '').split(',').map((s) => s.trim()).filter(Boolean);
|
||
else if (csv) groupIds = (csv.value || '').split(',').map((s) => s.trim()).filter(Boolean);
|
||
|
||
groupIds = groupIds.map((id) => Number(id)).filter((n) => !Number.isNaN(n));
|
||
}
|
||
|
||
busy(btn, true);
|
||
try {
|
||
// 1) Создаём должность
|
||
const createPayload = { name, description };
|
||
if (canAssign && groupIds.length) createPayload.groups = groupIds;
|
||
|
||
const cResp = await fetch(createUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
'X-CSRFToken': csrftoken
|
||
},
|
||
body: JSON.stringify(createPayload)
|
||
});
|
||
const cData = await (async () => {
|
||
try {
|
||
return await cResp.json();
|
||
} catch {
|
||
return null;
|
||
}
|
||
})();
|
||
if (!cResp.ok) {
|
||
msg(m, cData?.detail || JSON.stringify(cData) || `HTTP ${cResp.status}`, false);
|
||
return;
|
||
}
|
||
const newId = cData?.id;
|
||
if (!newId) {
|
||
msg(m, 'API не вернул id новой должности.', false);
|
||
return;
|
||
}
|
||
|
||
// 2) Назначаем сотруднику
|
||
const eResp = await fetch(assignUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
'X-CSRFToken': csrftoken
|
||
},
|
||
body: JSON.stringify({ position_id: newId })
|
||
});
|
||
const eData = await (async () => {
|
||
try {
|
||
return await eResp.json();
|
||
} catch {
|
||
return null;
|
||
}
|
||
})();
|
||
if (!eResp.ok) {
|
||
msg(
|
||
m,
|
||
'Создано, но не назначено: ' + (eData?.detail || JSON.stringify(eData) || `HTTP ${eResp.status}`),
|
||
false
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 3) Обновляем селект и закрываем панель
|
||
if (select) {
|
||
let opt = Array.from(select.options).find((o) => o.value == String(newId));
|
||
if (!opt) {
|
||
opt = document.createElement('option');
|
||
opt.value = String(newId);
|
||
opt.textContent = cData?.name || 'Должность #' + newId;
|
||
select.appendChild(opt);
|
||
}
|
||
select.value = String(newId);
|
||
select.dataset.init = String(newId);
|
||
}
|
||
msg(m, 'Должность создана и назначена.', true);
|
||
try {
|
||
if (window.bootstrap)
|
||
new bootstrap.Collapse(document.getElementById('newPositionPane'), { toggle: false }).hide();
|
||
} catch (_) {}
|
||
} catch (ex) {
|
||
console.error(ex);
|
||
msg(m, 'Неожиданная ошибка при создании/назначении.', false);
|
||
} finally {
|
||
busy(btn, false);
|
||
}
|
||
});
|
||
|
||
// Редактирование существующей должности
|
||
const updateTpl = block.dataset.updateUrlTpl || '';
|
||
const detailTpl = block.dataset.detailUrlTpl || '';
|
||
|
||
function buildUrlFromTpl(tpl, id) {
|
||
if (!tpl) return '';
|
||
if (tpl.includes('/0/')) return tpl.replace('/0/', `/${id}/`);
|
||
if (tpl.endsWith('0')) return tpl.slice(0, -1) + id;
|
||
return tpl.replace('0', String(id));
|
||
}
|
||
|
||
function buildUpdateUrl(posId) {
|
||
return buildUrlFromTpl(updateTpl, posId);
|
||
}
|
||
function buildDetailUrl(posId) {
|
||
return buildUrlFromTpl(detailTpl, posId);
|
||
}
|
||
|
||
async function loadPositionDetail(posId) {
|
||
const msgBox = document.getElementById('posEditMsg');
|
||
const loadBtn = document.getElementById('posEditLoadBtn');
|
||
const nameInp = document.getElementById('pos_edit_name');
|
||
const descInp = document.getElementById('pos_edit_desc');
|
||
|
||
function showMsg(text, ok) {
|
||
msgBox.textContent = text || '';
|
||
msgBox.classList.toggle('d-none', !text);
|
||
msgBox.classList.toggle('text-success', !!ok);
|
||
msgBox.classList.toggle('text-danger', !ok);
|
||
}
|
||
|
||
if (!posId) {
|
||
showMsg('Выберите должность в выпадающем списке.', false);
|
||
return;
|
||
}
|
||
const url = buildDetailUrl(posId);
|
||
if (!url) {
|
||
showMsg('Не настроен URL detail-вьюхи должности.', false);
|
||
return;
|
||
}
|
||
|
||
showMsg('', true);
|
||
busy(loadBtn, true);
|
||
try {
|
||
const resp = await fetch(url, {
|
||
credentials: 'same-origin',
|
||
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||
});
|
||
const data = await (async () => {
|
||
try {
|
||
return await resp.json();
|
||
} catch {
|
||
return null;
|
||
}
|
||
})();
|
||
if (!resp.ok) {
|
||
showMsg(data?.detail || `Не удалось загрузить должность (HTTP ${resp.status})`, false);
|
||
return;
|
||
}
|
||
|
||
nameInp && (nameInp.value = data?.name || '');
|
||
descInp && (descInp.value = data?.description || '');
|
||
|
||
if (canAssign) {
|
||
const groupsRaw = Array.isArray(data?.groups)
|
||
? data.groups
|
||
: Array.isArray(data?.groups_ids)
|
||
? data.groups_ids
|
||
: [];
|
||
const ids = groupsRaw
|
||
.map((x) => (typeof x === 'number' ? String(x) : String(x?.id || x)))
|
||
.filter(Boolean);
|
||
const hidden = document.getElementById('pos_edit_groups_hidden');
|
||
if (hidden) {
|
||
hidden.value = ids.join(',');
|
||
// Trigger render chips if function exists
|
||
if (typeof window.renderPositionChips === 'function') {
|
||
window.renderPositionChips();
|
||
}
|
||
}
|
||
}
|
||
showMsg('Данные должности загружены.', true);
|
||
} catch (_) {
|
||
showMsg('Ошибка загрузки должности.', false);
|
||
} finally {
|
||
busy(loadBtn, false);
|
||
}
|
||
}
|
||
|
||
async function savePositionChanges() {
|
||
const msgBox = document.getElementById('posEditMsg');
|
||
const saveBtn = document.getElementById('posEditSaveBtn');
|
||
const nameInp = document.getElementById('pos_edit_name');
|
||
const descInp = document.getElementById('pos_edit_desc');
|
||
const hidden = document.getElementById('pos_edit_groups_hidden');
|
||
|
||
function showMsg(text, ok) {
|
||
msgBox.textContent = text || '';
|
||
msgBox.classList.toggle('d-none', !text);
|
||
msgBox.classList.toggle('text-success', !!ok);
|
||
msgBox.classList.toggle('text-danger', !ok);
|
||
}
|
||
|
||
const posId = Number(select.value || 0);
|
||
if (!posId) {
|
||
showMsg('Сначала выберите должность, которую хотите редактировать.', false);
|
||
return;
|
||
}
|
||
const updateUrl = buildUpdateUrl(posId);
|
||
if (!updateUrl) {
|
||
showMsg('Не настроен URL обновления должности.', false);
|
||
return;
|
||
}
|
||
|
||
const payload = {};
|
||
const nm = (nameInp?.value || '').trim();
|
||
const ds = (descInp?.value || '').trim();
|
||
if (nm) payload.name = nm;
|
||
payload.description = ds;
|
||
if (canAssign) {
|
||
const csv = (hidden?.value || '').trim();
|
||
const ids = csv ? csv.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
||
const groups = ids.map((x) => Number(x)).filter((n) => !Number.isNaN(n));
|
||
payload.groups = groups;
|
||
}
|
||
|
||
showMsg('', true);
|
||
busy(saveBtn, true);
|
||
try {
|
||
const resp = await fetch(updateUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Accept: 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
'X-CSRFToken': csrftoken
|
||
},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const data = await (async () => {
|
||
try {
|
||
return await resp.json();
|
||
} catch {
|
||
return null;
|
||
}
|
||
})();
|
||
if (!resp.ok) {
|
||
showMsg(data?.detail || `Не удалось сохранить (HTTP ${resp.status})`, false);
|
||
return;
|
||
}
|
||
|
||
if (payload.name) {
|
||
const opt = Array.from(select.options).find((o) => o.value == String(posId));
|
||
if (opt) opt.textContent = payload.name;
|
||
}
|
||
|
||
try {
|
||
if (window.bootstrap)
|
||
new bootstrap.Collapse(document.getElementById('editPositionPane'), { toggle: false }).hide();
|
||
} catch (_) {}
|
||
showMsg('Изменения сохранены.', true);
|
||
} catch (_) {
|
||
showMsg('Ошибка сохранения изменений.', false);
|
||
} finally {
|
||
busy(saveBtn, false);
|
||
}
|
||
}
|
||
|
||
document.getElementById('toggleEditPositionBtn')?.addEventListener('click', () => {
|
||
const posId = Number(select.value || 0);
|
||
if (posId) setTimeout(() => loadPositionDetail(posId), 150);
|
||
else {
|
||
const msgBox = document.getElementById('posEditMsg');
|
||
msgBox.textContent = 'Выберите должность в выпадающем списке.';
|
||
msgBox.classList.remove('d-none');
|
||
msgBox.classList.add('text-danger');
|
||
}
|
||
});
|
||
|
||
document.getElementById('posEditLoadBtn')?.addEventListener('click', () =>
|
||
loadPositionDetail(Number(select.value || 0))
|
||
);
|
||
document.getElementById('posEditSaveBtn')?.addEventListener('click', savePositionChanges);
|
||
|
||
return {
|
||
loadPosition: loadPositionDetail,
|
||
savePosition: savePositionChanges
|
||
};
|
||
}
|