feat: переделана лента новых сотрудников на AJAX загрузку
- Создан новый JS модуль newEmployeesScroller.js - Новые сотрудники теперь загружаются через API с клиента - Аватары используют глобальные URL (как в модале) - Убран серверный рендеринг из feed_list view - Шаблон использует пустой контейнер для JS инициализации - Добавлен полный collapse блок с последним комментарием - Добавлена форма быстрого комментария с emoji picker - Исправлена проблема с localhost URL в аватарах
This commit is contained in:
parent
3090d0eba7
commit
c9a440c3d3
@ -133,18 +133,11 @@ def _post_back_url(post: dict, user) -> str:
|
||||
|
||||
def feed_list(request):
|
||||
"""
|
||||
Лента компании - посты загружаются через API на клиенте.
|
||||
Блок «новые сотрудники» загружается с сервера.
|
||||
Лента компании - посты и новые сотрудники загружаются через API на клиенте.
|
||||
"""
|
||||
# Загружаем только новых сотрудников с сервера
|
||||
new_employees = _api_list_new_employees(request, limit=10)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"feed/feed_list.html",
|
||||
{
|
||||
"new_employees": new_employees,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -365,10 +365,11 @@ export class FeedList {
|
||||
collapseDiv.id = `ccoll-${post.id}`;
|
||||
collapseDiv.className = 'collapse';
|
||||
|
||||
// Заглушка - показываем кнопку "Показать все комментарии"
|
||||
// Контейнер с padding
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'px-3 pt-2';
|
||||
|
||||
// Кнопка "Показать все комментарии"
|
||||
const linkContainer = document.createElement('div');
|
||||
linkContainer.className = 'd-flex justify-content-between align-items-center mb-2';
|
||||
|
||||
@ -381,11 +382,141 @@ export class FeedList {
|
||||
|
||||
linkContainer.appendChild(showAllLink);
|
||||
contentDiv.appendChild(linkContainer);
|
||||
|
||||
// Последний комментарий (если есть)
|
||||
if (post.last_comment) {
|
||||
const commentDiv = this.createLastComment(post.last_comment);
|
||||
contentDiv.appendChild(commentDiv);
|
||||
}
|
||||
|
||||
collapseDiv.appendChild(contentDiv);
|
||||
|
||||
// Форма быстрого комментария
|
||||
const form = this.createQuickCommentForm(post.id);
|
||||
collapseDiv.appendChild(form);
|
||||
|
||||
return collapseDiv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание последнего комментария
|
||||
*/
|
||||
createLastComment(comment) {
|
||||
const commentDiv = document.createElement('div');
|
||||
commentDiv.className = 'comment-item';
|
||||
|
||||
// Header с автором и датой
|
||||
const headerDiv = document.createElement('div');
|
||||
headerDiv.className = 'comment-header';
|
||||
|
||||
// Автор
|
||||
if (comment.author?.id) {
|
||||
const authorLink = document.createElement('a');
|
||||
authorLink.href = `/employees/${comment.author.id}/`;
|
||||
authorLink.className = 'comment-author';
|
||||
authorLink.textContent = comment.author.full_name || 'Сотрудник';
|
||||
headerDiv.appendChild(authorLink);
|
||||
} else {
|
||||
const authorSpan = document.createElement('span');
|
||||
authorSpan.className = 'comment-author';
|
||||
authorSpan.textContent = comment.author?.full_name || 'Сотрудник';
|
||||
headerDiv.appendChild(authorSpan);
|
||||
}
|
||||
|
||||
// Разделитель
|
||||
const separator = document.createElement('span');
|
||||
separator.className = 'comment-separator';
|
||||
separator.textContent = '·';
|
||||
headerDiv.appendChild(separator);
|
||||
|
||||
// Дата
|
||||
const timeEl = document.createElement('time');
|
||||
timeEl.className = 'comment-date';
|
||||
timeEl.textContent = comment.created_at_display || comment.created_at || '';
|
||||
headerDiv.appendChild(timeEl);
|
||||
|
||||
commentDiv.appendChild(headerDiv);
|
||||
|
||||
// Тело комментария (обрезанное до 220 символов)
|
||||
const bodyDiv = document.createElement('div');
|
||||
bodyDiv.className = 'comment-body';
|
||||
let text = comment.text || '';
|
||||
if (text.length > 220) {
|
||||
text = text.substring(0, 217) + '...';
|
||||
}
|
||||
bodyDiv.textContent = text;
|
||||
commentDiv.appendChild(bodyDiv);
|
||||
|
||||
return commentDiv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание формы быстрого комментария
|
||||
*/
|
||||
createQuickCommentForm(postId) {
|
||||
const form = document.createElement('form');
|
||||
form.className = 'px-3 pb-3 pt-2 comment-form-quick';
|
||||
form.dataset.postId = postId;
|
||||
|
||||
// Message field container
|
||||
const messageField = document.createElement('div');
|
||||
messageField.className = 'message-field message-field--compact';
|
||||
|
||||
// Emoji picker dropdown
|
||||
const emojiDropdown = document.createElement('div');
|
||||
emojiDropdown.className = 'dropdown message-emoji';
|
||||
|
||||
const emojiBtn = document.createElement('button');
|
||||
emojiBtn.type = 'button';
|
||||
emojiBtn.className = 'btn btn-ghost btn-emoji message-icon-btn';
|
||||
emojiBtn.dataset.bsToggle = 'dropdown';
|
||||
emojiBtn.setAttribute('aria-expanded', 'false');
|
||||
emojiBtn.title = 'Вставить смайлик';
|
||||
|
||||
const emojiIcon = document.createElement('i');
|
||||
emojiIcon.className = 'bi-emoji-smile';
|
||||
emojiBtn.appendChild(emojiIcon);
|
||||
|
||||
const emojiMenu = document.createElement('div');
|
||||
emojiMenu.className = 'dropdown-menu dropdown-menu-start message-emoji-menu';
|
||||
|
||||
const emojiPicker = document.createElement('emoji-picker');
|
||||
emojiPicker.dataset.emojiPicker = '';
|
||||
emojiPicker.className = 'chat-emoji-picker';
|
||||
emojiMenu.appendChild(emojiPicker);
|
||||
|
||||
emojiDropdown.appendChild(emojiBtn);
|
||||
emojiDropdown.appendChild(emojiMenu);
|
||||
|
||||
messageField.appendChild(emojiDropdown);
|
||||
|
||||
// Textarea
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.name = 'text';
|
||||
textarea.className = 'form-control message-input';
|
||||
textarea.rows = 1;
|
||||
textarea.placeholder = 'Оставить комментарий…';
|
||||
textarea.required = true;
|
||||
|
||||
messageField.appendChild(textarea);
|
||||
|
||||
// Send button
|
||||
const sendBtn = document.createElement('button');
|
||||
sendBtn.className = 'btn btn-primary message-send';
|
||||
sendBtn.type = 'submit';
|
||||
sendBtn.title = 'Отправить';
|
||||
|
||||
const sendIcon = document.createElement('i');
|
||||
sendIcon.className = 'bi-send';
|
||||
sendBtn.appendChild(sendIcon);
|
||||
|
||||
messageField.appendChild(sendBtn);
|
||||
|
||||
form.appendChild(messageField);
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать лоадер
|
||||
*/
|
||||
|
||||
153
backend/static/js/components/newEmployeesScroller.js
Normal file
153
backend/static/js/components/newEmployeesScroller.js
Normal file
@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Модуль для загрузки и отображения новых сотрудников
|
||||
* Использует API для получения данных с корректными глобальными URL
|
||||
*/
|
||||
|
||||
export class NewEmployeesScroller {
|
||||
constructor(containerId = 'newEmployeesContainer') {
|
||||
this.container = document.getElementById(containerId);
|
||||
if (!this.container) {
|
||||
console.warn(`Container #${containerId} not found`);
|
||||
return;
|
||||
}
|
||||
this.apiUrl = '/api/v1/employees/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация - загрузка и рендеринг
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
const employees = await this.fetchNewEmployees();
|
||||
if (employees && employees.length > 0) {
|
||||
this.render(employees);
|
||||
} else {
|
||||
this.container.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load new employees:', error);
|
||||
this.container.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка новых сотрудников через API
|
||||
*/
|
||||
async fetchNewEmployees() {
|
||||
// Дата 14 дней назад
|
||||
const since = new Date();
|
||||
since.setDate(since.getDate() - 14);
|
||||
const sinceISO = since.toISOString();
|
||||
|
||||
const params = new URLSearchParams({
|
||||
active: 'true',
|
||||
created_at__gte: sinceISO,
|
||||
ordering: '-created_at',
|
||||
page_size: '10'
|
||||
});
|
||||
|
||||
const response = await fetch(`${this.apiUrl}?${params}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.results || data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Рендеринг списка сотрудников
|
||||
*/
|
||||
render(employees) {
|
||||
const scrollerHtml = this.createScrollerHtml(employees);
|
||||
this.container.innerHTML = scrollerHtml;
|
||||
this.container.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание HTML разметки скроллера
|
||||
*/
|
||||
createScrollerHtml(employees) {
|
||||
const items = employees.map(emp => this.createEmployeeItem(emp)).join('');
|
||||
|
||||
return `
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0 text-secondary"></h6>
|
||||
</div>
|
||||
|
||||
<div class="join-scroller js-join-scroller" aria-label="Новые сотрудники">
|
||||
<div class="join-rail">
|
||||
${items}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0 text-secondary"></h6>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание элемента сотрудника
|
||||
*/
|
||||
createEmployeeItem(emp) {
|
||||
const empId = emp.id || emp.pk;
|
||||
const empName = emp.short_name || emp.full_name || emp.email || 'Сотрудник';
|
||||
const deptName = emp.department?.name || emp.department_name || '';
|
||||
const avatar = emp.avatar;
|
||||
|
||||
// Аватар или иконка
|
||||
let avatarHtml;
|
||||
if (avatar) {
|
||||
avatarHtml = `<img src="${avatar}" alt="" loading="lazy">`;
|
||||
} else {
|
||||
avatarHtml = `
|
||||
<i class="bi-person" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">${this.escapeHtml(empName)}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Навыки
|
||||
let skillsHtml = '';
|
||||
if (emp.skills && emp.skills.length > 0) {
|
||||
const skillsList = emp.skills.map(s => this.escapeHtml(s.name)).join(', ');
|
||||
skillsHtml = `<div class="skills-line"><b>Навыки:</b> ${skillsList}</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="join-item">
|
||||
<a href="/employees/${empId}/"
|
||||
class="join-ava text-decoration-none"
|
||||
title="${this.escapeHtml(empName)}">
|
||||
${avatarHtml}
|
||||
</a>
|
||||
|
||||
<div class="join-info">
|
||||
<div class="name">${this.escapeHtml(empName)}</div>
|
||||
${deptName ? `<div class="sub">${this.escapeHtml(deptName)}</div>` : ''}
|
||||
${skillsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Экранирование HTML
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация при загрузке DOM
|
||||
*/
|
||||
export function initNewEmployeesScroller(containerId) {
|
||||
const scroller = new NewEmployeesScroller(containerId);
|
||||
scroller.init();
|
||||
return scroller;
|
||||
}
|
||||
@ -21,55 +21,10 @@
|
||||
{% include "includes/components/page_header.html" with icon="bi-megaphone" title="Лента" search_id="feedFilter" search_placeholder="Поиск по названию, автору или содержанию" %}
|
||||
{% endif %}
|
||||
|
||||
{% if new_employees %}
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0 text-secondary"></h6>
|
||||
</div>
|
||||
|
||||
<div class="join-scroller js-join-scroller" aria-label="Новые сотрудники">
|
||||
<div class="join-rail">
|
||||
{% for emp in new_employees %}
|
||||
{% firstof emp.id emp.pk as emp_id %}
|
||||
{% firstof emp.avatar emp.avatar as ava %}
|
||||
{% firstof emp.short_name emp.get_short_name emp.full_name emp.get_full_name emp.email "Сотрудник" as emp_name %}
|
||||
{% firstof emp.department.name emp.department_name as dept_name %}
|
||||
|
||||
|
||||
<div class="join-item">
|
||||
<a href="{% url 'employees:employee_detail' emp_id %}"
|
||||
class="join-ava text-decoration-none"
|
||||
title="{{ emp_name }}">
|
||||
{% if ava %}
|
||||
<img src="{{ ava.url|default:ava }}" alt="" loading="lazy">
|
||||
{% else %}
|
||||
<i class="bi-person" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{ emp_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<div class="join-info">
|
||||
<div class="name">{{ emp_name }}</div>
|
||||
{% if dept_name %}<div class="sub">{{ dept_name }}</div>{% endif %}
|
||||
|
||||
{# НАВЫКИ — печать всех, без итерации по строкам #}
|
||||
{% if emp.skills and emp.skills.0.name %}
|
||||
<div class="skills-line"><b>Навыки:</b>
|
||||
{% for s in emp.skills %}{% if not forloop.first %}, {% endif %}{{ s.name }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0 text-secondary"></h6>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Контейнер для новых сотрудников - загружается через JS #}
|
||||
<div id="newEmployeesContainer" style="display: none;">
|
||||
{# Загружается через newEmployeesScroller.js #}
|
||||
</div>
|
||||
|
||||
<section class="card mb-3">
|
||||
<div class="card-header">
|
||||
@ -98,6 +53,7 @@
|
||||
import { initCreatePostModal } from "{% static 'js/components/createPostModal.js' %}";
|
||||
import { FeedList } from '{% static "js/components/feedList.js" %}';
|
||||
import { ListFilter, createDataAttrMatcher } from '{% static "js/components/listFilter.js" %}';
|
||||
import { initNewEmployeesScroller } from '{% static "js/components/newEmployeesScroller.js" %}';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initAutoScroller({
|
||||
@ -117,6 +73,9 @@
|
||||
params: { type: 'company' }
|
||||
});
|
||||
|
||||
// 🆕 Загрузка новых сотрудников через AJAX
|
||||
initNewEmployeesScroller('newEmployeesContainer');
|
||||
|
||||
// Обновляем ленту после создания поста
|
||||
if (createModal) {
|
||||
document.getElementById('createPostForm')?.addEventListener('submit', async (e) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user