From 3ab23973b522a6607bfd1da268933873eda49ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 17 Dec 2025 09:13:51 +0300 Subject: [PATCH] feat: add employee dismissal UI and filter inactive employees - Added dismissal form on employee profile page with permissions - Shows warning when dismissing (LDAP deactivation notice) - Inactive employees filtered by default in employee list (active=true) - Team wheel now shows only active employees - Auto-submit filter form on status change - Added redirect to set active=true by default on first visit --- backend/employees/views_front.py | 9 ++ backend/static/js/components/employeesList.js | 3 +- backend/static/js/components/teamWheel.js | 18 +-- backend/templates/employees/_filters.html | 8 +- .../components/employee_detail_scripts.html | 122 ++++++++++++++++++ .../templates/employees/employee_detail.html | 71 +++++++++- .../templates/employees/employees_list.html | 11 ++ 7 files changed, 228 insertions(+), 14 deletions(-) diff --git a/backend/employees/views_front.py b/backend/employees/views_front.py index 1a60d435..10333346 100644 --- a/backend/employees/views_front.py +++ b/backend/employees/views_front.py @@ -209,6 +209,15 @@ def employee_list(request): Для HTML страницы - данные загружаются через JavaScript на клиенте. """ + # Если параметр active отсутствует и это HTML-запрос, + # редиректим с active=true по умолчанию + if request.GET.get("format") != "json" and "active" not in request.GET: + params = request.GET.copy() + params["active"] = "true" + from django.http import QueryDict + query_string = params.urlencode() + return redirect(f"{request.path}?{query_string}") + api = get_api_client(request) # JSON-ветка для автокомплита diff --git a/backend/static/js/components/employeesList.js b/backend/static/js/components/employeesList.js index 24ec4f51..31ba9946 100644 --- a/backend/static/js/components/employeesList.js +++ b/backend/static/js/components/employeesList.js @@ -26,7 +26,8 @@ export class EmployeesList { search: urlParams.get('q') || '', department: urlParams.get('department') || '', position: urlParams.get('position') || '', - is_active: urlParams.get('is_active') || '' + // По умолчанию показываем только активных сотрудников + active: urlParams.get('active') !== null ? urlParams.get('active') : 'true' }; // Intersection Observer для бесконечной прокрутки diff --git a/backend/static/js/components/teamWheel.js b/backend/static/js/components/teamWheel.js index f522f5fe..24ae8841 100644 --- a/backend/static/js/components/teamWheel.js +++ b/backend/static/js/components/teamWheel.js @@ -67,14 +67,16 @@ export function initTeamWheel(options) { // Источник данных const src = document.getElementById(dataSourceId); - const members = Array.from(src ? src.children : []).map(n => ({ - id: Number(n.getAttribute('data-id') || '0'), - name: n.getAttribute('data-name') || '', - role: n.getAttribute('data-role') || '', - email: n.getAttribute('data-email') || '', - avatar: n.getAttribute('data-avatar') || '', - active: n.getAttribute('data-active') === '1' - })); + const members = Array.from(src ? src.children : []) + .map(n => ({ + id: Number(n.getAttribute('data-id') || '0'), + name: n.getAttribute('data-name') || '', + role: n.getAttribute('data-role') || '', + email: n.getAttribute('data-email') || '', + avatar: n.getAttribute('data-avatar') || '', + active: n.getAttribute('data-active') === '1' + })) + .filter(m => m.active); // Показываем только активных сотрудников // Если никого нет — показываем руководителя if (!members.length) { diff --git a/backend/templates/employees/_filters.html b/backend/templates/employees/_filters.html index 8b33c6fa..6eafcc16 100644 --- a/backend/templates/employees/_filters.html +++ b/backend/templates/employees/_filters.html @@ -25,10 +25,10 @@
- + + +
diff --git a/backend/templates/employees/components/employee_detail_scripts.html b/backend/templates/employees/components/employee_detail_scripts.html index 5cf37193..cebd7717 100644 --- a/backend/templates/employees/components/employee_detail_scripts.html +++ b/backend/templates/employees/components/employee_detail_scripts.html @@ -79,5 +79,127 @@ } }); } + + // 5. Обработчик формы кадровых событий + const actionForm = document.getElementById('employeeActionForm'); + const actionType = document.getElementById('actionType'); + const actionDate = document.getElementById('actionDate'); + const dismissalWarning = document.getElementById('dismissalWarning'); + const actionSuccess = document.getElementById('actionSuccess'); + const actionError = document.getElementById('actionError'); + + if (actionForm && actionType) { + // Установка текущей даты по умолчанию + if (actionDate) { + const now = new Date(); + const offset = now.getTimezoneOffset() * 60000; + const localISOTime = new Date(now - offset).toISOString().slice(0, 16); + actionDate.value = localISOTime; + } + + // Показ предупреждения при выборе увольнения + actionType.addEventListener('change', (e) => { + if (dismissalWarning) { + if (e.target.value === 'dismissed') { + dismissalWarning.classList.remove('d-none'); + } else { + dismissalWarning.classList.add('d-none'); + } + } + }); + + // Отправка формы + actionForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + // Скрываем предыдущие сообщения + if (actionSuccess) actionSuccess.classList.add('d-none'); + if (actionError) actionError.classList.add('d-none'); + + const formData = new FormData(actionForm); + const data = { + employee: parseInt(formData.get('employee')), + action: formData.get('action'), + date: formData.get('date'), + comment: formData.get('comment') || '' + }; + + const submitBtn = actionForm.querySelector('button[type="submit"]'); + const originalText = submitBtn.innerHTML; + submitBtn.disabled = true; + submitBtn.innerHTML = 'Сохранение...'; + + try { + const response = await fetch('/api/v1/employee-actions/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': formData.get('csrfmiddlewaretoken') + }, + body: JSON.stringify(data) + }); + + if (response.ok) { + const result = await response.json(); + + // Показываем сообщение об успехе + if (actionSuccess) { + const actionNames = { + 'hired': 'Принят на работу', + 'dismissed': 'Уволен', + 'transferred': 'Переведен', + 'on_leave': 'В отпуске', + 'returned_from_leave': 'Вернулся из отпуска', + 'on_maternity': 'В декрете', + 'returned_from_maternity': 'Вернулся из декрета', + 'rehired': 'Восстановлен' + }; + actionSuccess.textContent = `Событие "${actionNames[data.action]}" успешно создано.`; + actionSuccess.classList.remove('d-none'); + } + + // Сбрасываем форму + actionForm.reset(); + if (actionDate) { + const now = new Date(); + const offset = now.getTimezoneOffset() * 60000; + const localISOTime = new Date(now - offset).toISOString().slice(0, 16); + actionDate.value = localISOTime; + } + if (dismissalWarning) dismissalWarning.classList.add('d-none'); + + // Перезагружаем страницу через 2 секунды для отображения нового события + setTimeout(() => { + window.location.reload(); + }, 2000); + } else { + const error = await response.json(); + let errorMsg = 'Ошибка при создании события.'; + + if (error.detail) { + errorMsg = error.detail; + } else if (error.error) { + errorMsg = error.error; + } else if (typeof error === 'object') { + errorMsg = Object.values(error).flat().join('. '); + } + + if (actionError) { + actionError.textContent = errorMsg; + actionError.classList.remove('d-none'); + } + } + } catch (error) { + console.error('Error creating employee action:', error); + if (actionError) { + actionError.textContent = 'Произошла ошибка при отправке данных.'; + actionError.classList.remove('d-none'); + } + } finally { + submitBtn.disabled = false; + submitBtn.innerHTML = originalText; + } + }); + } }); diff --git a/backend/templates/employees/employee_detail.html b/backend/templates/employees/employee_detail.html index 19349ea4..976848bb 100644 --- a/backend/templates/employees/employee_detail.html +++ b/backend/templates/employees/employee_detail.html @@ -238,7 +238,7 @@ {% if a.history and a.history|length %}
- История + Подробнее
{% for h in a.history %} {% with new_action=h.changes.action.new|default:a.action %} @@ -311,6 +311,75 @@ {% endwith %} {% endif %} + {# === УПРАВЛЕНИЕ КАДРОВЫМИ СОБЫТИЯМИ === #} + {% if request.user.is_authenticated and not is_me %} + {% if perms.employees.add_employeeaction or request.user.is_staff or request.user.is_superuser %} +
+
+ + +
+
+
+ {% csrf_token %} + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Внимание: Увольнение деактивирует сотрудника в системе и LDAP (если настроено). +
+ +
+ + +
+ +
+
+
+
+
+
+
+
+
+ {% endif %} + {% endif %} + {# ====== НАВЫКИ ====== #}
{ + const form = e.target.closest('form'); + if (form) { + form.submit(); + } + }); + } + // Инициализация модала создания сотрудника initCreateEmployeeModal({ submitUrl: '{% url "employees:employee_create_modal" %}'