432 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% load static %}
{% load api_tags %}
{% block title %}Чат{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/message-reactions.css' %}">
<link rel="stylesheet" href="{% static 'css/message-context-menu.css' %}">
<link rel="stylesheet" href="{% static 'css/message-selection.css' %}">
<link rel="stylesheet" href="{% static 'css/components/chat-polls.css' %}">
{% endblock %}
{% block content %}
<div class="container chat-root"
id="chatDetailApp"
data-chat-id="{{ chat.id }}"
data-user-id="{{ user.id }}"
data-user-name="{{ user.get_full_name|default:user.username }}"
data-user-avatar="{% if user.avatar %}{{ user.avatar.url }}{% endif %}"
data-upload-url="{% url 'api:v1:upload_message' %}"
data-messages-url="{% url 'api:v1:chat_messages' chat.pk %}"
data-mark-read-url="{% url 'communications:chat_mark_read' chat.pk %}"
data-edit-url-template="/api/v1/communications/messages/{id}/edit/"
data-last-read-timestamp="{{ chat.last_read_timestamp|default:'0' }}">
<div class="row gx-0">
<div class="col-12 chat-col">
<!-- Заголовок -->
<div class="section-header">
{% if chat.type == 'global' %}
<i class="bi-globe fs-2 text-primary"></i>
<h2 class="title mb-0">Глобальный чат</h2>
{% elif chat.type == 'department' %}
<i class="bi-diagram-3 fs-2 text-primary"></i>
<h2 class="title mb-0">Чат отдела: {{ chat.department.name }}</h2>
{% elif chat.type == 'announcement' %}
<i class="bi-megaphone fs-2 text-warning"></i>
<h2 class="title mb-0">{{ chat.name|default:"Объявление" }}</h2>
{% if chat.is_blocked %}
<span class="badge bg-danger ms-2">
<i class="bi-lock-fill"></i> Заблокировано
</span>
{% endif %}
{% if not is_announcement_creator %}
<span class="badge bg-info ms-2">
<i class="bi-eye"></i> Только просмотр
</span>
{% endif %}
{% else %}
<i class="bi-person fs-2 text-primary"></i>
<h2 class="title mb-0">
{% for u in chat.participants.all %}
{% if u != user %}
{% if not forloop.first %}, {% endif %}
<a href="{% url 'employees:employee_detail' u.pk %}" class="text-decoration-none">
{{ u.get_full_name|default:u.username }}
</a>
{% endif %}
{% endfor %}
</h2>
{% endif %}
<div class="ms-auto d-flex gap-2">
{% if chat.type == 'announcement' and user.is_staff %}
{% if chat.is_blocked %}
<button type="button" class="btn btn-success btn-sm" onclick="unblockAnnouncement({{ chat.id }})">
<i class="bi-unlock"></i> Разблокировать
</button>
{% else %}
<button type="button" class="btn btn-danger btn-sm" onclick="blockAnnouncement({{ chat.id }})">
<i class="bi-lock"></i> Заблокировать
</button>
{% endif %}
{% endif %}
<a href="{% url 'communications:chat_list' %}" class="btn btn-ghost">
<i class="bi-arrow-left"></i> К чатам
</a>
</div>
</div>
<!-- Корпус чата -->
<article class="card chat-box">
<!-- Предупреждение о блокировке -->
{% if chat.is_blocked %}
<div class="alert alert-danger m-3 mb-0" role="alert">
<div class="d-flex align-items-center">
<i class="bi-lock-fill fs-4 me-2"></i>
<div>
<strong>Это объявление заблокировано администратором</strong>
<br>
<small>
Заблокировано: {{ chat.blocked_at|date:"d.m.Y в H:i" }}
{% if chat.blocked_by %}
пользователем <strong>{{ chat.blocked_by.get_full_name|default:chat.blocked_by.username }}</strong>
{% endif %}
</small>
</div>
</div>
</div>
{% endif %}
<!-- Информация об объявлении -->
{% if chat.type == 'announcement' and not is_announcement_creator %}
<div class="alert alert-light border m-3 mb-0 py-2" role="alert">
<small class="text-secondary">
<i class="bi-info-circle me-1"></i>
Объявление от <strong>{{ chat.created_by.get_full_name|default:chat.created_by.username }}</strong>
</small>
</div>
{% endif %}
<!-- Кнопка «вниз» -->
<button id="scrollBtn" class="scroll-to-bottom" type="button" aria-label="Прокрутить вниз">
<i class="bi-arrow-down"></i>
</button>
<!-- Лента сообщений -->
<div id="chatScroll"
class="chat-scroll"
data-me-id="{{ user.id }}"
data-me-avatar="{% if user.avatar %}{{ user.avatar.url }}{% endif %}"
data-profile-url="{% url 'employees:profile' %}"
data-detail-url-template="{% url 'employees:employee_detail' 0 %}"
data-chat-id="{{ chat.id }}"
data-fetch-url="{% url 'api:v1:chat_messages' chat.pk %}"
data-page-size="{{ messages_page_size }}"
data-oldest-id="{{ messages_oldest_id }}"
data-oldest-ts="{{ messages_oldest_ts }}"
data-has-more="{% if messages_has_more %}1{% else %}0{% endif %}"
data-last-read-ts="{% if last_read_at %}{{ last_read_at|date:'U' }}000{% endif %}">
<!-- Индикатор загрузки истории (для прокрутки вверх) -->
<div class="history-loader d-none" data-history-loader>
<div class="spinner-border spinner-border-sm text-secondary" role="status" aria-hidden="true"></div>
<span class="small">Загружаем ранние сообщения…</span>
</div>
<!-- Индикатор первоначальной загрузки -->
<div class="initial-loader text-center py-5" id="initialLoader">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<p class="text-secondary">Загружаем сообщения...</p>
</div>
<!-- Сообщения будут добавлены через JavaScript -->
</div>
<!-- Индикатор печати -->
<div id="typing" class="small text-secondary ps-3 pt-2 d-none">
<span class="typing-indicator">
<span></span><span></span><span></span>
</span>
Собеседник печатает…
</div>
<!-- Композер -->
<div class="chat-composer">
{% if can_send_messages %}
<form id="chatForm"
class="composer-wrap"
method="post"
action="{% url 'api:v1:upload_message' %}"
enctype="multipart/form-data"
data-chat-composer="1"
data-chat-id="{{ chat.id }}"
data-upload-url="{% url 'api:v1:upload_message' %}"
data-me-id="{{ user.id }}"
data-me-name="{{ user.get_full_name|default:user.username }}"
data-me-avatar="{% if user.avatar %}{{ user.avatar.url }}{% endif %}"
data-profile-url="{% url 'employees:profile' %}"
data-detail-url-template="{% url 'employees:employee_detail' 0 %}"
onsubmit="return false;">
{% csrf_token %}
<input type="hidden" name="chat_id" value="{{ chat.id }}">
<div class="message-field">
<!-- Кнопка вложений с выпадающим меню -->
<div class="dropdown message-attach">
<button type="button" class="btn btn-ghost btn-attachment message-icon-btn"
id="attachmentDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
title="Прикрепить файл">
<i class="bi-paperclip"></i>
</button>
<ul class="dropdown-menu dropdown-menu-start" aria-labelledby="attachmentDropdown">
<li>
<a class="dropdown-item" href="#" id="attachDocument">
<i class="bi-file-earmark text-primary me-2"></i>
Документ
</a>
</li>
<li>
<a class="dropdown-item" href="#" id="attachImage">
<i class="bi-image text-success me-2"></i>
Фото или видео
</a>
</li>
<li>
<a class="dropdown-item" href="#" id="attachCamera">
<i class="bi-camera text-info me-2"></i>
Камера
</a>
</li>
<li>
<a class="dropdown-item" href="#" id="attachAudio">
<i class="bi-mic text-danger me-2"></i>
Аудио
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="#" id="createPoll">
<i class="bi-bar-chart text-warning me-2"></i>
Создать голосование
</a>
</li>
</ul>
</div>
<!-- Кнопка выбора эмодзи -->
<div class="dropdown message-emoji">
<button type="button" class="btn btn-ghost btn-emoji message-icon-btn"
id="emojiDropdown"
data-bs-toggle="dropdown"
data-emoji-toggle="1"
aria-expanded="false"
title="Вставить смайлик">
<i class="bi-emoji-smile"></i>
</button>
<div class="dropdown-menu dropdown-menu-start message-emoji-menu" aria-labelledby="emojiDropdown">
<emoji-picker data-emoji-picker class="chat-emoji-picker"></emoji-picker>
</div>
</div>
<!-- Поле ввода текста -->
<textarea name="content"
id="id_content"
class="form-control composer-textarea message-input"
rows="1"
placeholder="Напишите сообщение…"></textarea>
<!-- Кнопка отправки -->
<button type="submit" class="btn btn-primary btn-send message-send" title="Отправить">
<i class="bi-send-fill"></i>
</button>
</div>
<!-- Скрытые input для файлов -->
<input type="file" id="documentInput" class="d-none" accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.zip,.rar">
<input type="file" id="imageInput" class="d-none" accept="image/*,video/*" multiple>
<input type="file" id="cameraInput" class="d-none" accept="image/*" capture="environment">
<input type="file" id="audioInput" class="d-none" accept="audio/*">
<!-- Превью прикрепленных файлов -->
<div id="attachmentPreview" class="attachment-preview d-none"></div>
</form>
{% else %}
<div class="text-center text-secondary py-3">
<i class="bi-lock fs-4"></i>
<p class="mb-0 mt-2">
{% if chat.type == 'announcement' %}
Только автор объявления может публиковать сообщения
{% else %}
У вас нет прав для отправки сообщений в этот чат
{% endif %}
</p>
</div>
{% endif %}
</div>
</article>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
{# ========== THIRD-PARTY ========== #}
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
{# ========== CHAT DETAIL - NEW ENHANCED VERSION ========== #}
<script type="module" src="{% static 'js/chat-detail-enhanced.js' %}"></script>
{# ========== CHAT DETAIL PAGE - CORE FUNCTIONALITY ========== #}
<script type="module" src="{% static 'js/pages/chatDetail.js' %}"></script>
<script>
// Блокировка объявления
async function blockAnnouncement(chatId) {
if (!confirm('Вы уверены, что хотите заблокировать это объявление? Оно исчезнет из списка чатов всех пользователей.')) {
return;
}
try {
const response = await fetch(`/communications/api/announcement/${chatId}/block/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
});
const data = await response.json();
if (response.ok) {
location.reload();
} else {
alert(data.error || 'Ошибка блокировки');
}
} catch (error) {
console.error('Error blocking announcement:', error);
alert('Ошибка при блокировке объявления');
}
}
// Разблокировка объявления
async function unblockAnnouncement(chatId) {
try {
const response = await fetch(`/communications/api/announcement/${chatId}/unblock/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
});
const data = await response.json();
if (response.ok) {
location.reload();
} else {
alert(data.error || 'Ошибка разблокировки');
}
} catch (error) {
console.error('Error unblocking announcement:', error);
alert('Ошибка при разблокировке объявления');
}
}
</script>
<!-- Модальное окно создания голосования -->
<div class="modal fade" id="pollModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi-bar-chart me-2"></i>Создать голосование
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="pollQuestion" class="form-label">Вопрос</label>
<input type="text" class="form-control" id="pollQuestion"
placeholder="Введите вопрос" maxlength="500">
</div>
<div class="mb-3">
<label class="form-label">Варианты ответа</label>
<div id="pollOptions">
<div class="input-group mb-2">
<span class="input-group-text">1</span>
<input type="text" class="form-control poll-option"
placeholder="Вариант ответа" maxlength="200">
</div>
<div class="input-group mb-2">
<span class="input-group-text">2</span>
<input type="text" class="form-control poll-option"
placeholder="Вариант ответа" maxlength="200">
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addPollOption">
<i class="bi-plus-circle me-1"></i>Добавить вариант
</button>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="pollAnonymous">
<label class="form-check-label" for="pollAnonymous">
Анонимное голосование
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="pollMultiple">
<label class="form-check-label" for="pollMultiple">
Множественный выбор
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="pollQuiz">
<label class="form-check-label" for="pollQuiz">
Викторина (с правильным ответом)
</label>
</div>
</div>
<div class="mb-3" id="quizCorrectAnswer" style="display: none;">
<label class="form-label">Правильный ответ</label>
<select class="form-select" id="pollCorrectOption">
<option value="">Выберите правильный вариант</option>
</select>
</div>
<div class="mb-3">
<label for="pollCloses" class="form-label">
Автоматическое закрытие (минуты, необязательно)
</label>
<input type="number" class="form-control" id="pollCloses"
min="1" placeholder="Оставьте пустым для бессрочного">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Отмена
</button>
<button type="button" class="btn btn-primary" id="submitPoll">
Создать
</button>
</div>
</div>
</div>
</div>
{% endblock %}