AJAX submit для формы редактирования сотрудника, ошибки показываются inline

This commit is contained in:
admin i 2025-12-02 11:05:37 +03:00
parent dd902d6d52
commit e6e39b7fd0

View File

@ -1,84 +1,180 @@
/**
* employeeFormHandler.js
* Минимальный стаб, оставленный ради обратной совместимости после
* отказа от кастомного JS-сабмита. Теперь форма отправляется браузером
* напрямую в фронтовую Django-вьюху, а модуль лишь возвращает
* совместимое API без вмешательства в submit.
* Обработка отправки формы редактирования сотрудника через AJAX
* с отображением ошибок валидации inline без перезагрузки страницы
*/
export function initEmployeeForm(options = {}) {
const formId = options.formId || 'apiEditForm';
const form = document.getElementById(formId);
if (!form) {
console.warn('[EmployeeForm] Форма не найдена, инициализация пропущена');
return null;
}
// Новый обработчик: submit через fetch
form.addEventListener('submit', async function (e) {
console.info('[EmployeeForm] Инициализация AJAX обработчика формы');
// Перехватываем submit формы
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Очищаем старые ошибки
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
form.querySelectorAll('.invalid-feedback').forEach(el => el.textContent = '');
const endpoint = form.dataset.endpoint || form.action;
const formData = new FormData(form);
let payload = {};
formData.forEach((value, key) => {
if (payload[key]) {
if (!Array.isArray(payload[key])) payload[key] = [payload[key]];
payload[key].push(value);
} else {
payload[key] = value;
}
});
// Если есть файл, отправляем multipart, иначе json
let fetchOptions = {
method: 'POST',
headers: {
'X-CSRFToken': form.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
}
};
if (form.enctype === 'multipart/form-data') {
fetchOptions.body = formData;
delete fetchOptions.headers['Content-Type'];
} else {
fetchOptions.headers['Content-Type'] = 'application/json';
fetchOptions.body = JSON.stringify(payload);
}
const submitBtn = form.querySelector('button[type="submit"]');
const btnLabel = submitBtn?.querySelector('.btn-label');
const btnSpinner = submitBtn?.querySelector('.btn-spinner');
// Очищаем предыдущие ошибки
clearFormErrors(form);
// Показываем индикатор загрузки
if (submitBtn) submitBtn.disabled = true;
if (btnLabel) btnLabel.classList.add('d-none');
if (btnSpinner) btnSpinner.classList.remove('d-none');
try {
const response = await fetch(endpoint, fetchOptions);
const data = await response.json();
if (response.ok) {
// Успех: можно обновить данные или закрыть форму
if (options.onSuccess) options.onSuccess(data);
else location.reload();
} else {
// Ошибки: показываем в форме
if (data && typeof data === 'object') {
for (const [field, errors] of Object.entries(data)) {
const input = form.querySelector(`[name="${field}"]`);
if (input) {
input.classList.add('is-invalid');
let feedback = input.parentElement.querySelector('.invalid-feedback');
if (!feedback) {
feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
input.parentElement.appendChild(feedback);
}
feedback.textContent = Array.isArray(errors) ? errors.join(' ') : errors;
}
}
const endpoint = form.dataset.endpoint || form.action;
const formData = new FormData(form);
console.log('[EmployeeForm] Отправка данных на:', endpoint);
const response = await fetch(endpoint, {
method: 'PATCH',
body: formData,
headers: {
'X-CSRFToken': formData.get('csrfmiddlewaretoken') || ''
}
});
const data = await response.json();
if (response.ok) {
console.log('[EmployeeForm] Успешно сохранено');
showSuccessMessage('Изменения сохранены');
// Обновляем страницу через 1 секунду
setTimeout(() => {
location.reload();
}, 1000);
} else {
console.warn('[EmployeeForm] Ошибка валидации:', data);
displayFormErrors(form, data);
showErrorMessage('Исправьте ошибки в форме');
}
} catch (err) {
alert('Ошибка отправки формы: ' + err);
} catch (error) {
console.error('[EmployeeForm] Ошибка отправки:', error);
showErrorMessage('Произошла ошибка при сохранении');
} finally {
// Возвращаем кнопку в нормальное состояние
if (submitBtn) submitBtn.disabled = false;
if (btnLabel) btnLabel.classList.remove('d-none');
if (btnSpinner) btnSpinner.classList.add('d-none');
}
});
return {
setSubmitting: (v) => {},
setSubmitting: (state) => {
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) submitBtn.disabled = state;
},
refresh: () => location.reload()
};
}
/**
* Очистка всех ошибок в форме
*/
function clearFormErrors(form) {
// Убираем класс is-invalid со всех полей
form.querySelectorAll('.is-invalid').forEach(el => {
el.classList.remove('is-invalid');
});
// Очищаем все сообщения об ошибках
form.querySelectorAll('.invalid-feedback').forEach(el => {
el.textContent = '';
el.style.display = 'none';
});
}
/**
* Отображение ошибок валидации в полях формы
*/
function displayFormErrors(form, errors) {
// errors может быть объектом с полями или массивом строк
if (Array.isArray(errors)) {
// Общие ошибки (non_field_errors)
showErrorMessage(errors.join(', '));
return;
}
if (typeof errors === 'object') {
Object.keys(errors).forEach(fieldName => {
const errorMessages = Array.isArray(errors[fieldName])
? errors[fieldName]
: [errors[fieldName]];
// Ищем поле по имени
const field = form.querySelector(`[name="${fieldName}"]`);
if (field) {
// Добавляем класс ошибки
field.classList.add('is-invalid');
// Ищем или создаём контейнер для ошибки
let feedbackEl = field.parentElement.querySelector('.invalid-feedback');
if (!feedbackEl) {
feedbackEl = document.createElement('div');
feedbackEl.className = 'invalid-feedback';
field.parentElement.appendChild(feedbackEl);
}
// Показываем сообщение об ошибке
feedbackEl.textContent = errorMessages.join(', ');
feedbackEl.style.display = 'block';
console.log(`[EmployeeForm] Ошибка в поле ${fieldName}:`, errorMessages);
} else {
console.warn(`[EmployeeForm] Поле ${fieldName} не найдено в форме`);
}
});
}
}
/**
* Показ сообщения об успехе
*/
function showSuccessMessage(message) {
// Можно использовать toast или простой alert
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// Автоматически убираем через 3 секунды
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
/**
* Показ сообщения об ошибке
*/
function showErrorMessage(message) {
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// Автоматически убираем через 5 секунд
setTimeout(() => {
alertDiv.remove();
}, 5000);
}