From c9a440c3d37012449d72caa27d257a1d00c81d32 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 10:56:23 +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=D0=BB=D0=B5=D0=BD=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D1=81=D0=BE=D1=82=D1=80=D1=83?= =?UTF-8?q?=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20=D0=BD=D0=B0=20AJAX=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Создан новый JS модуль newEmployeesScroller.js - Новые сотрудники теперь загружаются через API с клиента - Аватары используют глобальные URL (как в модале) - Убран серверный рендеринг из feed_list view - Шаблон использует пустой контейнер для JS инициализации - Добавлен полный collapse блок с последним комментарием - Добавлена форма быстрого комментария с emoji picker - Исправлена проблема с localhost URL в аватарах --- backend/feed/views_front.py | 9 +- backend/static/js/components/feedList.js | 133 ++++++++++++++- .../js/components/newEmployeesScroller.js | 153 ++++++++++++++++++ backend/templates/feed/feed_list.html | 57 +------ 4 files changed, 294 insertions(+), 58 deletions(-) create mode 100644 backend/static/js/components/newEmployeesScroller.js diff --git a/backend/feed/views_front.py b/backend/feed/views_front.py index 9eb41004..40e1387f 100644 --- a/backend/feed/views_front.py +++ b/backend/feed/views_front.py @@ -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, - }, ) diff --git a/backend/static/js/components/feedList.js b/backend/static/js/components/feedList.js index 4d4ed581..83d1e72b 100644 --- a/backend/static/js/components/feedList.js +++ b/backend/static/js/components/feedList.js @@ -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; + } + /** * Показать лоадер */ diff --git a/backend/static/js/components/newEmployeesScroller.js b/backend/static/js/components/newEmployeesScroller.js new file mode 100644 index 00000000..d8a48744 --- /dev/null +++ b/backend/static/js/components/newEmployeesScroller.js @@ -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 ` +
+
+
+
+ +
+
+ ${items} +
+
+ +
+
+
+
+ `; + } + + /** + * Создание элемента сотрудника + */ + 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 = ``; + } else { + avatarHtml = ` + + ${this.escapeHtml(empName)} + `; + } + + // Навыки + let skillsHtml = ''; + if (emp.skills && emp.skills.length > 0) { + const skillsList = emp.skills.map(s => this.escapeHtml(s.name)).join(', '); + skillsHtml = `
Навыки: ${skillsList}
`; + } + + return ` +
+ + ${avatarHtml} + + +
+
${this.escapeHtml(empName)}
+ ${deptName ? `
${this.escapeHtml(deptName)}
` : ''} + ${skillsHtml} +
+
+ `; + } + + /** + * Экранирование 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; +} diff --git a/backend/templates/feed/feed_list.html b/backend/templates/feed/feed_list.html index 2aed6b10..44693ba5 100644 --- a/backend/templates/feed/feed_list.html +++ b/backend/templates/feed/feed_list.html @@ -21,55 +21,10 @@ {% include "includes/components/page_header.html" with icon="bi-megaphone" title="Лента" search_id="feedFilter" search_placeholder="Поиск по названию, автору или содержанию" %} {% endif %} - {% if new_employees %} -
-
-
-
- -
-
- {% 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 %} - - -
- - {% if ava %} - - {% else %} - - {{ emp_name }} - {% endif %} - - -
-
{{ emp_name }}
- {% if dept_name %}
{{ dept_name }}
{% endif %} - - {# НАВЫКИ — печать всех, без итерации по строкам #} - {% if emp.skills and emp.skills.0.name %} -
Навыки: - {% for s in emp.skills %}{% if not forloop.first %}, {% endif %}{{ s.name }}{% endfor %} -
- {% endif %} - - -
-
- {% endfor %} -
-
-
-
-
-
- {% endif %} + {# Контейнер для новых сотрудников - загружается через JS #} +
@@ -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) => {