/** * employeeFormHandler.js * Обработка отправки формы редактирования сотрудника через AJAX * с отображением ошибок валидации inline без перезагрузки страницы */ export function initEmployeeForm(options = {}) { const formId = options.formId || 'apiEditForm'; const form = document.getElementById(formId); if (!form) { console.warn('[EmployeeForm] Форма не найдена, инициализация пропущена'); return null; } // Проверяем, не была ли форма уже инициализирована if (form.dataset.employeeFormInitialized === 'true') { console.warn('[EmployeeForm] Форма уже инициализирована, пропускаем повторную инициализацию'); return { setSubmitting: (state) => { const submitBtn = form.querySelector('button[type="submit"]'); if (submitBtn) submitBtn.disabled = state; }, refresh: () => location.reload() }; } console.info('[EmployeeForm] Инициализация AJAX обработчика формы'); // Помечаем форму как инициализированную form.dataset.employeeFormInitialized = 'true'; // Перехватываем submit формы form.addEventListener('submit', async (e) => { e.preventDefault(); 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 endpoint = form.dataset.endpoint || form.action; const formData = new FormData(form); // ДЕТАЛЬНЫЕ ЛОГИ: проверяем все поля в FormData console.log('[EmployeeForm] Отправка данных на:', endpoint); console.log('[EmployeeForm] Все поля в FormData:'); for (let [key, value] of formData.entries()) { if (value instanceof File) { console.log(` ${key}: [File] name="${value.name}", size=${value.size}, type="${value.type}"`); } else { console.log(` ${key}: "${value}" (length: ${String(value).length})`); } } // Проверяем поле avatar особенно внимательно const avatarInput = form.querySelector('input[name="avatar"]'); if (avatarInput) { console.log('[EmployeeForm] Avatar input найден:'); console.log(' - type:', avatarInput.type); console.log(' - files.length:', avatarInput.files?.length || 0); if (avatarInput.files && avatarInput.files.length > 0) { console.log(' - files[0]:', avatarInput.files[0]); } else { console.log(' - НЕТ выбранных файлов'); } console.log(' - value:', avatarInput.value); // КРИТИЧНО: если файл не выбран, удаляем поле avatar из FormData if (!avatarInput.files || avatarInput.files.length === 0) { console.warn('[EmployeeForm] Аватар не выбран - УДАЛЯЕМ поле avatar из FormData'); formData.delete('avatar'); } } else { console.log('[EmployeeForm] Avatar input НЕ НАЙДЕН в форме'); } // Логируем финальное состояние FormData после очистки console.log('[EmployeeForm] Финальный FormData (после очистки):'); for (let [key, value] of formData.entries()) { if (value instanceof File) { console.log(` ${key}: [File] name="${value.name}", size=${value.size}`); } else { console.log(` ${key}: "${value}"`); } } 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); // Логируем полный текст ошибки detail для отладки if (data.detail) { console.error('[EmployeeForm] Полный текст ошибки detail:', data.detail); } displayFormErrors(form, data); showErrorMessage('Исправьте ошибки в форме'); } } 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: (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') { // Обрабатываем специальный случай с полем "detail" if (errors.detail && Object.keys(errors).length === 1) { // Пытаемся распарсить detail, если там есть информация о полях const detailStr = String(errors.detail); // Проверяем, есть ли в detail информация о конкретных полях // Формат: "Internal server error: {'field': [ErrorDetail(...)]}" const fieldMatch = detailStr.match(/\{'(\w+)':\s*\[(.*?)\]/); if (fieldMatch) { const fieldName = fieldMatch[1]; const errorText = fieldMatch[2]; // Извлекаем читаемое сообщение об ошибке const msgMatch = errorText.match(/ErrorDetail\(string='([^']+)'/); const errorMessage = msgMatch ? msgMatch[1] : errorText; // Показываем ошибку в конкретном поле 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 = errorMessage; feedbackEl.style.display = 'block'; console.log(`[EmployeeForm] Ошибка в поле ${fieldName}:`, errorMessage); } else { // Если поле не найдено, показываем общую ошибку showErrorMessage(errorMessage); } } else { // Если не удалось распарсить, показываем как общую ошибку showErrorMessage(detailStr); } return; } // Стандартная обработка ошибок по полям 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 if (fieldName === 'non_field_errors') { // Общие ошибки формы showErrorMessage(errorMessages.join(', ')); } else { console.warn(`[EmployeeForm] Поле ${fieldName} не найдено в форме`); // Показываем как общую ошибку showErrorMessage(`${fieldName}: ${errorMessages.join(', ')}`); } }); } } /** * Показ сообщения об успехе */ 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); }