feat: переделана страница списка сотрудников на AJAX загрузку

- Создан новый JS модуль employeesList.js
- Список сотрудников загружается через API /api/v1/employees/
- Аватары получают правильные глобальные URL
- Сохранена поддержка фильтрации и поиска
- Передается ID текущего пользователя для скрытия кнопок
- Добавлена обработка ошибок загрузки
- Исправлена проблема с localhost URL в аватарах
This commit is contained in:
admin i 2025-12-05 12:28:59 +03:00
parent d22b297651
commit 785a82f54b
2 changed files with 248 additions and 13 deletions

View File

@ -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 = '<div class="alert alert-info shadow-sm">Нет сотрудников.</div>';
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 `
<article class="emp-row list-row"
data-name="${this.escapeHtml(fullName + ' ' + position)}"
data-depts="${this.escapeHtml(departments)}">
<header class="card-header">
<div class="card-icon">
${avatar}
</div>
<div class="card-meta flex-grow-1">
<div class="d-flex align-items-center gap-2 flex-wrap">
${emp.id ?
`<a href="/employees/${emp.id}/" class="card-title text-decoration-none">${this.escapeHtml(fullName)}</a>` :
`<span class="card-title">${this.escapeHtml(fullName)}</span>`
}
${position ? `<span class="emp-pos">— ${this.escapeHtml(position)}</span>` : ''}
</div>
${departments ? `<div class="card-subtitle">${departments}</div>` : ''}
</div>
<div class="ms-auto d-flex align-items-center gap-2">
${this.getActionButtons(emp)}
</div>
</header>
<div class="card-body"></div>
</article>
`;
}
/**
* Получение полного имени
*/
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 `<a href="/employees/departments/${d.id}/">${this.escapeHtml(d.name)}</a>`;
}
return this.escapeHtml(d.name);
})
.join(', ');
}
/**
* Получение HTML аватара
*/
getAvatarHtml(emp) {
if (emp.avatar) {
return `<img src="${emp.avatar}" alt="${this.escapeHtml(this.getFullName(emp))}">`;
} else {
return '<i class="bi-person"></i>';
}
}
/**
* Получение кнопок действий
*/
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 `
<a href="/communications/chats/private/${emp.id}/"
class="btn btn-ghost action-btn" title="Чат">
<i class="bi-chat-dots"></i><span class="d-none d-sm-inline ms-1">Чат</span>
</a>
`;
}
/**
* Экранирование HTML
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Рендеринг ошибки
*/
renderError() {
this.container.innerHTML = `
<div class="alert alert-danger shadow-sm">
Не удалось загрузить список сотрудников. Попробуйте обновить страницу.
</div>
`;
}
}
/**
* Инициализация списка сотрудников
*/
export function initEmployeesList(options) {
const list = new EmployeesList(options);
list.init();
return list;
}

View File

@ -22,6 +22,7 @@
<div class="card mb-3">
<div id="empList" class="list-view card-list">
{# Список загружается через JavaScript из employeesList.js #}
{% if employees %}
{% for emp in employees %}
<article class="emp-row list-row"
@ -129,20 +130,31 @@
<script type="module">
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" %}'
});
});
</script>
{% endblock %}