From 785a82f54b58e29419835962ea82d9521c392f2e 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: Fri, 5 Dec 2025 12:28:59 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D0=B0=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BE=D1=82=D1=80=D1=83=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=BD=D0=B0=20AJAX=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Создан новый JS модуль employeesList.js - Список сотрудников загружается через API /api/v1/employees/ - Аватары получают правильные глобальные URL - Сохранена поддержка фильтрации и поиска - Передается ID текущего пользователя для скрытия кнопок - Добавлена обработка ошибок загрузки - Исправлена проблема с localhost URL в аватарах --- backend/static/js/components/employeesList.js | 223 ++++++++++++++++++ .../templates/employees/employees_list.html | 38 ++- 2 files changed, 248 insertions(+), 13 deletions(-) create mode 100644 backend/static/js/components/employeesList.js diff --git a/backend/static/js/components/employeesList.js b/backend/static/js/components/employeesList.js new file mode 100644 index 00000000..3687c592 --- /dev/null +++ b/backend/static/js/components/employeesList.js @@ -0,0 +1,223 @@ +/** + * Модуль для загрузки и отображения списка сотрудников + * Использует API для получения данных с корректными глобальными URL + */ + +export class EmployeesList { + constructor(options = {}) { + this.containerId = options.containerId || 'empList'; + this.container = document.getElementById(this.containerId); + if (!this.container) { + console.warn(`Container #${this.containerId} not found`); + return; + } + + this.apiUrl = '/api/v1/employees/'; + this.employees = []; + this.loading = false; + + // Параметры из URL + const urlParams = new URLSearchParams(window.location.search); + this.params = { + page: urlParams.get('page') || 1, + ordering: urlParams.get('o') || 'last_name', + search: urlParams.get('q') || '', + department: urlParams.get('department') || '', + position: urlParams.get('position') || '', + is_active: urlParams.get('is_active') || '' + }; + } + + /** + * Инициализация - загрузка и рендеринг + */ + async init() { + if (!this.container) return; + + try { + await this.loadEmployees(); + this.render(); + } catch (error) { + console.error('Failed to load employees:', error); + this.renderError(); + } + } + + /** + * Загрузка сотрудников через API + */ + async loadEmployees() { + if (this.loading) return; + this.loading = true; + + const params = new URLSearchParams(); + Object.entries(this.params).forEach(([key, value]) => { + if (value) params.append(key, value); + }); + + const response = await fetch(`${this.apiUrl}?${params}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + this.employees = data.results || data; + this.loading = false; + } + + /** + * Рендеринг списка сотрудников + */ + render() { + if (this.employees.length === 0) { + this.container.innerHTML = '
Нет сотрудников.
'; + return; + } + + const itemsHtml = this.employees.map(emp => this.createEmployeeCard(emp)).join(''); + this.container.innerHTML = itemsHtml; + } + + /** + * Создание карточки сотрудника + */ + createEmployeeCard(emp) { + const fullName = this.getFullName(emp); + const position = this.getPosition(emp); + const departments = this.getDepartments(emp); + const avatar = this.getAvatarHtml(emp); + + return ` +
+
+
+ ${avatar} +
+ +
+
+ ${emp.id ? + `${this.escapeHtml(fullName)}` : + `${this.escapeHtml(fullName)}` + } + ${position ? `— ${this.escapeHtml(position)}` : ''} +
+ ${departments ? `
${departments}
` : ''} +
+ +
+ ${this.getActionButtons(emp)} +
+
+ +
+
+ `; + } + + /** + * Получение полного имени + */ + getFullName(emp) { + let name = `${emp.last_name || ''} ${emp.first_name || ''}`.trim(); + if (emp.patronymic) { + name += ` ${emp.patronymic}`; + } + return name || 'Сотрудник'; + } + + /** + * Получение должности + */ + getPosition(emp) { + if (emp.position?.name) { + return emp.position.name; + } else if (emp.position_label) { + return emp.position_label; + } else if (emp.position) { + return `Должность #${emp.position}`; + } + return ''; + } + + /** + * Получение отделов + */ + getDepartments(emp) { + if (!emp.departments || emp.departments.length === 0) { + return ''; + } + + return emp.departments + .filter(d => d.name) + .map(d => { + if (d.id) { + return `${this.escapeHtml(d.name)}`; + } + return this.escapeHtml(d.name); + }) + .join(', '); + } + + /** + * Получение HTML аватара + */ + getAvatarHtml(emp) { + if (emp.avatar) { + return `${this.escapeHtml(this.getFullName(emp))}`; + } else { + return ''; + } + } + + /** + * Получение кнопок действий + */ + getActionButtons(emp) { + if (!emp.id) return ''; + + // Получаем ID текущего пользователя из глобальной переменной или data-атрибута + const currentUserId = window.currentUserId || document.body.dataset.userId; + if (currentUserId && String(currentUserId) === String(emp.id)) { + return ''; // Не показываем кнопки для себя + } + + return ` + + Чат + + `; + } + + /** + * Экранирование HTML + */ + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Рендеринг ошибки + */ + renderError() { + this.container.innerHTML = ` +
+ Не удалось загрузить список сотрудников. Попробуйте обновить страницу. +
+ `; + } +} + +/** + * Инициализация списка сотрудников + */ +export function initEmployeesList(options) { + const list = new EmployeesList(options); + list.init(); + return list; +} diff --git a/backend/templates/employees/employees_list.html b/backend/templates/employees/employees_list.html index 96b50e9c..50fdc7f6 100644 --- a/backend/templates/employees/employees_list.html +++ b/backend/templates/employees/employees_list.html @@ -22,6 +22,7 @@
+ {# Список загружается через JavaScript из employeesList.js #} {% if employees %} {% for emp in employees %}
import { ListFilter, createDataAttrMatcher } from '{% static "js/components/listFilter.js" %}'; import { initCreateEmployeeModal } from '{% static "js/components/createEmployeeModal.js" %}'; + import { initEmployeesList } from '{% static "js/components/employeesList.js" %}'; - // Инициализация фильтра списка - new ListFilter({ - listSelector: '#empList', - itemSelector: '.emp-row', - searchInputSelector: '#empFilter', - clearButtonSelector: '.ios-search-clear', - matchFn: createDataAttrMatcher(['name', 'depts']), - debounceMs: 300 - }); - - // Инициализация модала создания сотрудника - initCreateEmployeeModal({ - submitUrl: '{% url "employees:employee_create_modal" %}' + document.addEventListener('DOMContentLoaded', () => { + // Передаем ID текущего пользователя в глобальную переменную + window.currentUserId = {{ user.id|default:'null' }}; + + // Инициализация списка сотрудников через AJAX + const employeesList = initEmployeesList({ + containerId: 'empList' + }); + + // Инициализация фильтра списка (работает с data-атрибутами) + new ListFilter({ + listSelector: '#empList', + itemSelector: '.emp-row', + searchInputSelector: '#empFilter', + clearButtonSelector: '.ios-search-clear', + matchFn: createDataAttrMatcher(['name', 'depts']), + debounceMs: 300 + }); + + // Инициализация модала создания сотрудника + initCreateEmployeeModal({ + submitUrl: '{% url "employees:employee_create_modal" %}' + }); }); {% endblock %}