432 lines
18 KiB
HTML
432 lines
18 KiB
HTML
{% 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 %} |