From 1e365accf08061944fb4fea584a9ec6b358bc82a 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, 28 Nov 2025 10:09:23 +0300 Subject: [PATCH] =?UTF-8?q?Fix:=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20ChatComposer.js=20=D0=B8=20?= =?UTF-8?q?=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0=20=D1=88=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=BE=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/communications/views.py | 41 +--- backend/static/js/chat-detail-init.js | 0 backend/static/js/components/chatComposer.js | 230 ++++++++++++++++++ .../static/js/components/chatContextMenu.js | 0 .../static/js/components/chatFileUpload.js | 0 .../templates/communications/chat_detail.html | 223 ++--------------- 6 files changed, 252 insertions(+), 242 deletions(-) create mode 100644 backend/static/js/chat-detail-init.js create mode 100644 backend/static/js/components/chatComposer.js create mode 100644 backend/static/js/components/chatContextMenu.js create mode 100644 backend/static/js/components/chatFileUpload.js diff --git a/backend/communications/views.py b/backend/communications/views.py index a7ed2bd7..12abb5e1 100644 --- a/backend/communications/views.py +++ b/backend/communications/views.py @@ -118,14 +118,10 @@ class ChatListView(LoginRequiredMixin, ListView): return qs -class ChatDetailView(LoginRequiredMixin, DetailView, FormView): +class ChatDetailView(LoginRequiredMixin, DetailView): model = Chat template_name = "communications/chat_detail.html" context_object_name = "chat" - form_class = MessageForm - - def get_success_url(self): - return reverse("communications:chat_detail", kwargs={"pk": self.object.pk}) def _user_has_access(self, chat: Chat, user) -> bool: if chat.type == "global": @@ -157,7 +153,6 @@ class ChatDetailView(LoginRequiredMixin, DetailView, FormView): context["messages"] = chat.messages.select_related("author").order_by( "created_at" ) - context["form"] = context.get("form", MessageForm()) context["participants"] = chat.get_participants # отдаём клиенту last_read_at и «первое непрочитанное» @@ -190,40 +185,6 @@ class ChatDetailView(LoginRequiredMixin, DetailView, FormView): return context - def post(self, request, *args, **kwargs): - self.object = self.get_object() - user = request.user - - if not ChatDetailView._user_has_access(self, self.object, user): - from django.http import HttpResponseForbidden - - return HttpResponseForbidden("forbidden") - - form = self.get_form() - if form.is_valid(): - msg = form.save(commit=False) - msg.chat = self.object - msg.author = user - msg.save() - - # автору сразу «прочитано» — делаем безопасно - ts = msg.created_at - updated = ChatReadState.objects.filter( - chat=self.object, user=user, last_read_at__lt=ts - ).update(last_read_at=ts) - if not updated: - try: - ChatReadState.objects.create( - chat=self.object, user=user, last_read_at=ts - ) - except IntegrityError: - ChatReadState.objects.filter( - chat=self.object, user=user, last_read_at__lt=ts - ).update(last_read_at=ts) - - return redirect(self.get_success_url()) - return self.render_to_response(self.get_context_data(form=form)) - def start_private_chat(request, employee_pk): from employees.models import Employee diff --git a/backend/static/js/chat-detail-init.js b/backend/static/js/chat-detail-init.js new file mode 100644 index 00000000..e69de29b diff --git a/backend/static/js/components/chatComposer.js b/backend/static/js/components/chatComposer.js new file mode 100644 index 00000000..97046045 --- /dev/null +++ b/backend/static/js/components/chatComposer.js @@ -0,0 +1,230 @@ +/** + * ChatComposer.js + * Отвечает за логику ввода текста, выбора файлов и отправки сообщений. + * Полностью заменяет стандартную отправку формы. + */ +class ChatComposer { + constructor(config) { + this.chatId = config.chatId; + this.apiEndpoint = config.apiEndpoint || '/api/v1/communications/upload-message/'; + this.csrfToken = this.getCookie('csrftoken'); + + // DOM Elements + this.form = document.getElementById('chatForm'); + this.textarea = document.getElementById('id_content'); + this.previewContainer = document.getElementById('attachmentPreview'); + + // File Inputs + this.inputs = { + document: document.getElementById('documentInput'), + image: document.getElementById('imageInput'), + camera: document.getElementById('cameraInput'), + audio: document.getElementById('audioInput') + }; + + // Buttons + this.buttons = { + document: document.getElementById('attachDocument'), + image: document.getElementById('attachImage'), + camera: document.getElementById('attachCamera'), + audio: document.getElementById('attachAudio'), + send: this.form?.querySelector('.btn-send') + }; + + // State + this.selectedFiles = []; + this.isSubmitting = false; + + this.init(); + } + + init() { + if (!this.form) { + console.error('ChatComposer: Form not found'); + return; + } + + this.bindEvents(); + console.log('ChatComposer: Initialized'); + } + + bindEvents() { + // 1. Блокировка стандартной отправки + this.form.addEventListener('submit', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.handleSubmit(); + }); + + // 2. Обработка кнопок вложений + if (this.buttons.document) this.buttons.document.addEventListener('click', (e) => { e.preventDefault(); this.inputs.document.click(); }); + if (this.buttons.image) this.buttons.image.addEventListener('click', (e) => { e.preventDefault(); this.inputs.image.click(); }); + if (this.buttons.camera) this.buttons.camera.addEventListener('click', (e) => { e.preventDefault(); this.inputs.camera.click(); }); + if (this.buttons.audio) this.buttons.audio.addEventListener('click', (e) => { e.preventDefault(); this.inputs.audio.click(); }); + + // 3. Обработка выбора файлов (change event) + Object.values(this.inputs).forEach(input => { + if (input) { + input.addEventListener('change', (e) => this.handleFileSelect(e.target.files)); + } + }); + + // 4. Enter для отправки (если не Shift+Enter) + this.textarea.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + this.form.dispatchEvent(new Event('submit')); + } + }); + } + + handleFileSelect(files) { + if (!files || files.length === 0) return; + + // Добавляем новые файлы к существующим + this.selectedFiles = [...this.selectedFiles, ...Array.from(files)]; + + // Очищаем input, чтобы можно было выбрать тот же файл снова + Object.values(this.inputs).forEach(input => { if(input) input.value = ''; }); + + this.updatePreview(); + } + + removeFile(index) { + this.selectedFiles.splice(index, 1); + this.updatePreview(); + } + + updatePreview() { + if (!this.previewContainer) return; + + if (this.selectedFiles.length === 0) { + this.previewContainer.classList.add('d-none'); + this.previewContainer.innerHTML = ''; + return; + } + + this.previewContainer.classList.remove('d-none'); + this.previewContainer.innerHTML = ''; + + this.selectedFiles.forEach((file, index) => { + const item = document.createElement('div'); + item.className = 'attachment-item d-flex align-items-center gap-2 p-2 border rounded mb-2 bg-white'; + + let icon = 'bi-file-earmark'; + if (file.type.startsWith('image/')) icon = 'bi-image'; + else if (file.type.startsWith('video/')) icon = 'bi-camera-video'; + else if (file.type.startsWith('audio/')) icon = 'bi-music-note'; + + item.innerHTML = ` + +
+
${file.name}
+
${this.formatFileSize(file.size)}
+
+ + `; + + item.querySelector('.remove-file-btn').addEventListener('click', () => this.removeFile(index)); + this.previewContainer.appendChild(item); + }); + } + + async handleSubmit() { + if (this.isSubmitting) return; + + const content = this.textarea.value.trim(); + + // Валидация + if (!content && this.selectedFiles.length === 0) { + // Можно добавить визуальный эффект ошибки + this.textarea.focus(); + return; + } + + this.isSubmitting = true; + this.toggleLoading(true); + + try { + const formData = new FormData(); + formData.append('chat_id', this.chatId); + formData.append('content', content); // Отправляем даже пустой текст + + // Добавляем файлы + this.selectedFiles.forEach((file, index) => { + formData.append(`file_${index}`, file); + }); + + console.log(`ChatComposer: Sending message. Content len: ${content.length}, Files: ${this.selectedFiles.length}`); + + const response = await fetch(this.apiEndpoint, { + method: 'POST', + headers: { + 'X-CSRFToken': this.csrfToken + }, + body: formData + }); + + const result = await response.json(); + + if (response.ok && result.ok) { + this.resetForm(); + } else { + console.error('ChatComposer: Server error', result); + alert('Ошибка отправки: ' + (result.error || 'Неизвестная ошибка')); + } + + } catch (error) { + console.error('ChatComposer: Network error', error); + alert('Ошибка сети. Проверьте подключение.'); + } finally { + this.isSubmitting = false; + this.toggleLoading(false); + this.textarea.focus(); + } + } + + resetForm() { + this.textarea.value = ''; + this.selectedFiles = []; + this.updatePreview(); + + // Сброс высоты textarea (если используется авто-высота) + this.textarea.style.height = 'auto'; + } + + toggleLoading(isLoading) { + if (this.buttons.send) { + this.buttons.send.disabled = isLoading; + this.buttons.send.innerHTML = isLoading + ? '' + : ''; + } + } + + formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + } + + getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } +} + +// Экспорт для использования +window.ChatComposer = ChatComposer; diff --git a/backend/static/js/components/chatContextMenu.js b/backend/static/js/components/chatContextMenu.js new file mode 100644 index 00000000..e69de29b diff --git a/backend/static/js/components/chatFileUpload.js b/backend/static/js/components/chatFileUpload.js new file mode 100644 index 00000000..e69de29b diff --git a/backend/templates/communications/chat_detail.html b/backend/templates/communications/chat_detail.html index 3d39d8e3..4f37ea04 100644 --- a/backend/templates/communications/chat_detail.html +++ b/backend/templates/communications/chat_detail.html @@ -148,7 +148,7 @@
Собеседник печатает…
-
+ {% csrf_token %}
@@ -219,212 +219,31 @@
+{% block extra_js %} + + + + + + + - {% endblock %}