diff --git a/backend/static/js/components/employeeFormHandler.js b/backend/static/js/components/employeeFormHandler.js index bb7341b3..95ab9ee9 100644 --- a/backend/static/js/components/employeeFormHandler.js +++ b/backend/static/js/components/employeeFormHandler.js @@ -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} + + `; + 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} + + `; + document.body.appendChild(alertDiv); + + // Автоматически убираем через 5 секунд + setTimeout(() => { + alertDiv.remove(); + }, 5000); +}