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:
parent
d1df011bad
commit
66225d1cd7
218
backend/static/js/components/createDepartmentModal.js
Normal file
218
backend/static/js/components/createDepartmentModal.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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>
|
||||
@ -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 %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user