diff --git a/backend/static/js/components/createDepartmentModal.js b/backend/static/js/components/createDepartmentModal.js new file mode 100644 index 00000000..3beb4317 --- /dev/null +++ b/backend/static/js/components/createDepartmentModal.js @@ -0,0 +1,218 @@ +/** + * createDepartmentModal.js + * Обработчик модального окна создания отдела + */ + +export function initCreateDepartmentModal(options = {}) { + const { + modalId = 'createDepartmentModal', + formId = 'createDepartmentForm', + errorsId = 'createDepartmentErrors', + submitBtnId = 'createDepartmentSubmit', + submitUrl = '/api/v1/departments/' + } = options; + + const modal = document.getElementById(modalId); + const form = document.getElementById(formId); + const errorsDiv = document.getElementById(errorsId); + const submitBtn = document.getElementById(submitBtnId); + + if (!modal || !form) { + console.error('Modal or form not found!', { modalId, formId }); + return; + } + + console.log('Create department modal initialized'); + + // Маппинг полей для человекочитаемых названий + const fieldLabels = { + 'name': 'Название', + 'description': 'Описание' + }; + + /** + * Очистка формы и ошибок + */ + function clearFormErrors() { + errorsDiv.classList.add('d-none'); + errorsDiv.innerHTML = ''; + form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid')); + form.querySelectorAll('.invalid-feedback').forEach(el => el.remove()); + } + + /** + * Валидация обязательных полей + */ + function validateRequiredFields() { + const name = form.querySelector('[name="name"]'); + let hasErrors = false; + + if (!name.value.trim()) { + name.classList.add('is-invalid'); + const feedback = document.createElement('div'); + feedback.className = 'invalid-feedback'; + feedback.textContent = 'Название обязательно для заполнения'; + name.parentElement.appendChild(feedback); + hasErrors = true; + } + + return !hasErrors; + } + + /** + * Отображение ошибок по полям + */ + function displayFieldErrors(errors) { + const errorMessages = []; + + for (const [fieldName, messages] of Object.entries(errors)) { + const field = form.querySelector(`[name="${fieldName}"]`); + const errorText = Array.isArray(messages) ? messages.join(', ') : messages; + const label = fieldLabels[fieldName] || fieldName; + + errorMessages.push(`${label}: ${errorText}`); + + if (field) { + field.classList.add('is-invalid'); + const feedback = document.createElement('div'); + feedback.className = 'invalid-feedback'; + feedback.textContent = errorText; + field.parentElement.appendChild(feedback); + } + } + + if (errorMessages.length > 0) { + errorsDiv.innerHTML = errorMessages.join('
'); + errorsDiv.classList.remove('d-none'); + } + } + + /** + * Показать общее сообщение об ошибке + */ + function displayGeneralError(message) { + errorsDiv.textContent = message; + errorsDiv.classList.remove('d-none'); + } + + /** + * Показать уведомление об успехе + */ + function showSuccessNotification(message) { + const alertDiv = document.createElement('div'); + alertDiv.className = 'alert alert-success alert-dismissible fade show'; + alertDiv.setAttribute('role', 'alert'); + alertDiv.innerHTML = ` + ${message} + + `; + + const container = document.querySelector('.container') || document.body; + container.prepend(alertDiv); + + setTimeout(() => alertDiv.remove(), 5000); + } + + /** + * Блокировка/разблокировка кнопки отправки + */ + function setSubmitButtonState(loading) { + submitBtn.disabled = loading; + submitBtn.innerHTML = loading + ? 'Создание...' + : 'Создать'; + } + + /** + * Получить JWT токен из мета-тега + */ + function getJWTToken() { + const meta = document.querySelector('meta[name="api-access"]'); + return meta ? meta.content : ''; + } + + // Очистка формы при открытии модала + modal.addEventListener('show.bs.modal', function() { + form.reset(); + clearFormErrors(); + }); + + // Обработка отправки формы + form.addEventListener('submit', async function(e) { + e.preventDefault(); + e.stopPropagation(); + + console.log('Form submit event triggered'); + + // Очистка предыдущих ошибок + clearFormErrors(); + + // Валидация + if (!validateRequiredFields()) { + displayGeneralError('Пожалуйста, заполните название отдела'); + return; + } + + // Блокируем кнопку + setSubmitButtonState(true); + + // Собираем данные + const formData = { + name: form.querySelector('[name="name"]').value.trim(), + description: form.querySelector('[name="description"]').value.trim() + }; + + const token = getJWTToken(); + const headers = { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + try { + const response = await fetch(submitUrl, { + method: 'POST', + headers: headers, + body: JSON.stringify(formData) + }); + + const data = await response.json(); + + if (response.ok) { + // Успех - закрываем модал + bootstrap.Modal.getInstance(modal).hide(); + + // Показываем уведомление + showSuccessNotification(`Отдел "${formData.name}" успешно создан`); + + // Перезагружаем страницу или перенаправляем + if (data.id) { + setTimeout(() => { + window.location.href = `/employees/departments/${data.id}/`; + }, 1000); + } else { + setTimeout(() => { + window.location.reload(); + }, 1000); + } + } else { + // Обработка ошибок + if (data.errors && typeof data.errors === 'object') { + displayFieldErrors(data.errors); + } else if (data.detail) { + displayGeneralError(data.detail); + } else { + displayGeneralError('Произошла ошибка при создании отдела'); + } + } + } catch (error) { + console.error('Network error:', error); + displayGeneralError('Ошибка сети. Проверьте подключение и попробуйте снова.'); + } finally { + setSubmitButtonState(false); + } + }); +} diff --git a/backend/templates/employees/components/modal_create_department.html b/backend/templates/employees/components/modal_create_department.html index e69de29b..7297549e 100644 --- a/backend/templates/employees/components/modal_create_department.html +++ b/backend/templates/employees/components/modal_create_department.html @@ -0,0 +1,42 @@ +{% comment %} +Модальное окно для создания отдела +Использование: {% include "employees/components/modal_create_department.html" %} +{% endcomment %} + + diff --git a/backend/templates/employees/department_list.html b/backend/templates/employees/department_list.html index cc7ed638..22e6a0fd 100644 --- a/backend/templates/employees/department_list.html +++ b/backend/templates/employees/department_list.html @@ -18,11 +18,10 @@ {# ===== НОВАЯ ШАПКА С КОМПОНЕНТОМ ===== #} {% if can_create_departments or perms.employees.add_department or request.user.is_staff or request.user.is_superuser %} - {% url 'employees:department_create' as dept_create_url %} + {% include "includes/components/page_header.html" with icon="bi-diagram-3" title="Отделы" count=departments|length count_label="Всего:" search_id="deptFilter" search_placeholder="Поиск по названию или руководителю" create_as_card=True create_modal="createDepartmentModal" create_text="Создать отдел" %} {% else %} - {% with dept_create_url='' %}{% endwith %} + {% include "includes/components/page_header.html" with icon="bi-diagram-3" title="Отделы" count=departments|length count_label="Всего:" search_id="deptFilter" search_placeholder="Поиск по названию или руководителю" %} {% endif %} - {% include "includes/components/page_header.html" with icon="bi-diagram-3" title="Отделы" count=departments|length count_label="Всего:" search_id="deptFilter" search_placeholder="Поиск по названию или руководителю" create_as_card=True create_url=dept_create_url create_text="Создать отдел" %}
@@ -72,8 +71,12 @@
+{# Модальное окно для создания отдела #} +{% include "employees/components/modal_create_department.html" %} + {% endblock %}