Corp/backend/static/js/components/employeeFormHandler.js

263 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* employeeFormHandler.js
* Обработчик формы редактирования сотрудника через API
*
* @module employeeFormHandler
* @version 1.0.0
*/
/**
* Инициализация обработчика формы редактирования сотрудника
* @param {Object} options - Параметры инициализации
* @param {string} options.formId - ID формы (по умолчанию 'apiEditForm')
* @param {string[]} options.contactFields - Список полей контактов для валидации
* @returns {Object} API формы { refresh, submit, clearErrors }
*/
export function initEmployeeForm(options = {}) {
const config = {
formId: options.formId || 'apiEditForm',
contactFields: options.contactFields || ['email', 'phone_number', 'telegram', 'whatsapp', 'wechat']
};
const form = document.getElementById(config.formId);
if (!form) {
return null; // Тихий выход, если форма не найдена
}
const endpoint = form.dataset.endpoint || '';
if (!endpoint) {
console.warn('Employee form: endpoint not found');
return null;
}
const CONTACT_FIELDS = config.contactFields;
const btn = form.querySelector('[type="submit"]');
const btnLabel = btn?.querySelector('.btn-label');
const btnSpin = btn?.querySelector('.btn-spinner');
/**
* Установить состояние отправки формы
*/
function setSubmitting(on) {
if (!btn) return;
if (on) {
btn.setAttribute('disabled', '');
btnSpin?.classList.remove('d-none');
btnLabel?.classList.add('visually-hidden');
} else {
btn.removeAttribute('disabled');
btnSpin?.classList.add('d-none');
btnLabel?.classList.remove('visually-hidden');
}
}
/**
* Очистить все ошибки валидации
*/
function clearErrors() {
form.querySelectorAll('.is-invalid').forEach((el) => el.classList.remove('is-invalid'));
form.querySelectorAll('.invalid-feedback').forEach((el) => (el.textContent = ''));
}
/**
* Показать ошибку для конкретного поля
*/
function fieldError(name, message) {
const el = form.querySelector(`[name="${CSS.escape(name)}"]`);
if (!el) return;
el.classList.add('is-invalid');
const container = el.closest('[class^="col-"]') || el.parentElement;
let fb = container?.querySelector('.invalid-feedback');
if (!fb) {
fb = document.createElement('div');
fb.className = 'invalid-feedback';
el.after(fb);
}
fb.textContent = message || 'Некорректное значение';
}
/**
* Валидация наличия хотя бы одного контакта
*/
function ensureOneContact() {
const hasAny = CONTACT_FIELDS.some((n) => (form.elements[n]?.value || '').trim().length > 0);
if (hasAny) return true;
CONTACT_FIELDS.forEach((n) => fieldError(n, 'Укажите хотя бы один контакт'));
form.elements[CONTACT_FIELDS[0]]?.focus();
return false;
}
/**
* Прочитать файл в Base64
*/
function readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = () => resolve(fr.result);
fr.onerror = reject;
fr.readAsDataURL(file);
});
}
/**
* Собрать skills_ids (массив) и сравнить с начальным значением
*/
function collectSkillsPayload() {
const sel = form.querySelector('select[name="skills_ids"]');
if (!sel) return null;
const selected = Array.from(sel.selectedOptions)
.map((o) => o.value)
.filter(Boolean);
const initCsv = sel.dataset.init || '';
const initArr = initCsv
? initCsv
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: [];
// Сравнение (без учёта порядка)
const norm = (a) => a.slice().sort().join(',');
if (norm(selected) === norm(initArr)) return null;
return selected;
}
/**
* Собрать общие поля (кроме skills_ids и avatar)
*/
function buildScalarPayload() {
const payload = {};
const fields = form.querySelectorAll(
'input[name]:not([type="file"]):not([name="clear_avatar"]):not(:disabled), select[name]:not([name="skills_ids"]):not(:disabled)'
);
fields.forEach((el) => {
const name = el.name;
const raw = (el.value ?? '').toString().trim();
const init = (el.dataset.init ?? el.defaultValue ?? '').toString();
// Ничего не менялось
if (raw === init) return;
// Пустое значение: для некоторых полей нужно слать null
const shouldNull = raw === '' && ['gender', 'birth_date', 'position_id'].includes(name);
if (shouldNull) {
// Меняем непустое init -> null (очистка)
if (init !== '') payload[name] = null;
return;
}
// Типизация gender в число
if (name === 'gender' && raw !== '') {
payload[name] = Number(raw);
return;
}
// Остальные — как есть (пустые строки допустимы для текстовых полей)
payload[name] = raw;
});
return payload;
}
/**
* Обработчик отправки формы
*/
async function handleSubmit(e) {
e.preventDefault();
clearErrors();
if (!ensureOneContact()) return;
setSubmitting(true);
try {
const payload = buildScalarPayload();
// skills_ids (массив)
const skills = collectSkillsPayload();
if (skills) payload.skills_ids = skills;
// avatar (Base64) / clear
const avatarFile = document.getElementById('f_avatar')?.files?.[0] || null;
const clearAvatar = form.querySelector('input[name="clear_avatar"]')?.checked;
if (avatarFile) {
try {
const dataUrl = await readFileAsDataURL(avatarFile);
payload.avatar = dataUrl; // Base64ImageField ожидает data URL
} catch (_) {}
} else if (clearAvatar) {
payload.avatar = ''; // Очистка
}
// Если нечего отправлять — просто обновим UI
if (Object.keys(payload).length === 0) {
setSubmitting(false);
if (window.bootstrap) {
try {
new bootstrap.Collapse(document.getElementById('editFormWrap'), { toggle: false }).hide();
} catch (_) {}
}
location.reload();
return;
}
const csrftoken = form.querySelector('input[name=csrfmiddlewaretoken]')?.value || '';
const resp = await fetch(endpoint, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrftoken
},
body: JSON.stringify(payload),
credentials: 'same-origin'
});
if (!resp.ok) {
let body = null;
try {
body = await resp.json();
} catch {}
if (body && typeof body === 'object') {
let shown = false;
for (const [k, v] of Object.entries(body)) {
if (k === 'detail') continue;
const msg = Array.isArray(v) ? v.join(' ') : String(v);
fieldError(k, msg);
shown = true;
}
if (body.detail) alert(body.detail);
else if (!shown) alert('Не удалось сохранить изменения (ошибка валидации).');
} else {
const t = await resp.text();
alert('Не удалось сохранить изменения.\n' + (t || `HTTP ${resp.status}`));
}
setSubmitting(false);
return;
}
// Успех
try {
if (window.bootstrap)
new bootstrap.Collapse(document.getElementById('editFormWrap'), { toggle: false }).hide();
} catch (_) {}
location.reload();
} catch (err) {
console.error(err);
alert('Не удалось сохранить изменения. Попробуйте ещё раз.');
setSubmitting(false);
}
}
form.addEventListener('submit', handleSubmit);
// API возвращаем для внешнего использования
return {
clearErrors,
setSubmitting,
fieldError,
refresh: () => location.reload()
};
}