Add modal for department creation

- Create modal_create_department.html component with simple form (name, description)
- Create createDepartmentModal.js handler for form submission via API
- Update department_list.html to use modal instead of link
- Modal integrates with API v1/departments/ endpoint
- Includes validation, error handling, and success notifications
- Now both employees and departments use consistent modal creation pattern
This commit is contained in:
admin i 2025-11-20 09:52:34 +03:00
parent d1df011bad
commit 66225d1cd7
3 changed files with 271 additions and 3 deletions

View File

@ -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(`<strong>${label}:</strong> ${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('<br>');
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}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
const container = document.querySelector('.container') || document.body;
container.prepend(alertDiv);
setTimeout(() => alertDiv.remove(), 5000);
}
/**
* Блокировка/разблокировка кнопки отправки
*/
function setSubmitButtonState(loading) {
submitBtn.disabled = loading;
submitBtn.innerHTML = loading
? '<span class="spinner-border spinner-border-sm me-1"></span>Создание...'
: '<i class="bi-check-lg me-1"></i>Создать';
}
/**
* Получить 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);
}
});
}

View File

@ -0,0 +1,42 @@
{% comment %}
Модальное окно для создания отдела
Использование: {% include "employees/components/modal_create_department.html" %}
{% endcomment %}
<div class="modal fade" id="createDepartmentModal" tabindex="-1" aria-labelledby="createDepartmentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createDepartmentModalLabel">
<i class="bi-diagram-3 me-2"></i>Создать отдел
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<form id="createDepartmentForm" novalidate>
{% csrf_token %}
<div class="modal-body">
<div id="createDepartmentErrors" class="alert alert-danger d-none mb-3"></div>
<div class="mb-3">
<label for="dept_name" class="form-label">Название <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="dept_name" name="name" required maxlength="255">
</div>
<div class="mb-3">
<label for="dept_description" class="form-label">Описание</label>
<textarea class="form-control" id="dept_description" name="description" rows="4"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary" id="createDepartmentSubmit">
<i class="bi-check-lg me-1"></i>Создать
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -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="Создать отдел" %}
<div class="feed-card">
<div id="deptList" class="py-1">
@ -72,8 +71,12 @@
</div>
</div>
{# Модальное окно для создания отдела #}
{% include "employees/components/modal_create_department.html" %}
<script type="module">
import { initDepartmentFilter } from "{% static 'js/components/departmentFilterHandler.js' %}";
import { initCreateDepartmentModal } from "{% static 'js/components/createDepartmentModal.js' %}";
initDepartmentFilter({
inputId: 'deptFilter',
@ -84,6 +87,11 @@
emptyNoticeId: 'deptEmptyNotice',
rowSelector: '.dept-row'
});
// Инициализация модала создания отдела
initCreateDepartmentModal({
submitUrl: '{% url "api:v1:departments-list" %}'
});
</script>
{% endblock %}