diff --git a/backend/communications/api_views.py b/backend/communications/api_views.py
index db4506fb..4e0d0772 100644
--- a/backend/communications/api_views.py
+++ b/backend/communications/api_views.py
@@ -307,16 +307,117 @@ def pin_message(request, message_id):
# ============ ATTACHMENTS ============
+@login_required
+@require_POST
+def upload_message_with_attachments(request):
+ """Отправка сообщения с вложениями"""
+ chat_id = request.POST.get('chat_id')
+ content = request.POST.get('content', '').strip()
+
+ if not chat_id:
+ return JsonResponse(
+ {'ok': False, 'error': 'chat_id required'},
+ status=400
+ )
+
+ chat = get_object_or_404(Chat, pk=chat_id)
+
+ # Проверка доступа к чату
+ membership = ChatMembership.objects.filter(
+ chat=chat,
+ user=request.user,
+ is_active=True,
+ can_send_messages=True
+ ).first()
+
+ if not membership:
+ return JsonResponse(
+ {'ok': False, 'error': 'Cannot send messages to this chat'},
+ status=403
+ )
+
+ # Если нет ни текста, ни файлов
+ if not content and not request.FILES:
+ return JsonResponse(
+ {'ok': False, 'error': 'Content or files required'},
+ status=400
+ )
+
+ with transaction.atomic():
+ # Создаем сообщение
+ message = Message.objects.create(
+ chat=chat,
+ author=request.user,
+ content=content or '',
+ has_attachments=bool(request.FILES)
+ )
+
+ # Обрабатываем все загруженные файлы
+ attachments = []
+ for file_key in request.FILES:
+ uploaded_file = request.FILES[file_key]
+
+ # Определение типа файла
+ mime_type = uploaded_file.content_type or ''
+ if mime_type.startswith('image/'):
+ file_type = 'image'
+ elif mime_type.startswith('video/'):
+ file_type = 'video'
+ elif mime_type.startswith('audio/'):
+ file_type = 'audio'
+ else:
+ file_type = 'file'
+
+ attachment = MessageAttachment.objects.create(
+ message=message,
+ file=uploaded_file,
+ file_type=file_type,
+ file_name=uploaded_file.name,
+ file_size=uploaded_file.size,
+ mime_type=mime_type
+ )
+
+ attachments.append({
+ 'id': attachment.id,
+ 'file_name': attachment.file_name,
+ 'file_type': attachment.file_type,
+ 'file_url': attachment.file.url,
+ 'file_size': attachment.file_size,
+ 'mime_type': attachment.mime_type
+ })
+
+ avatar_url = None
+ if hasattr(request.user, 'avatar') and request.user.avatar:
+ avatar_url = request.user.avatar.url
+
+ return JsonResponse({
+ 'ok': True,
+ 'message_id': message.id,
+ 'chat_id': chat.id,
+ 'author': {
+ 'id': request.user.id,
+ 'full_name': request.user.get_full_name(),
+ 'avatar': avatar_url
+ },
+ 'content': message.content,
+ 'created_at': message.created_at.isoformat(),
+ 'attachments': attachments
+ })
+
+
@login_required
@require_POST
def upload_attachment(request):
- """Загрузка вложения"""
+ """Загрузка вложения к существующему сообщению"""
if 'file' not in request.FILES:
return JsonResponse({'ok': False, 'error': 'No file'}, status=400)
message_id = request.POST.get('message_id')
if not message_id:
- return JsonResponse({'ok': False, 'error': 'message_id required'}, status=400)
+ return JsonResponse(
+ {'ok': False, 'error': 'message_id required'},
+ status=400
+ )
message = get_object_or_404(Message, pk=message_id)
diff --git a/backend/communications/consumers.py b/backend/communications/consumers.py
index 232aa2cc..06cb9515 100644
--- a/backend/communications/consumers.py
+++ b/backend/communications/consumers.py
@@ -42,6 +42,20 @@ def serialize_message(m: Message) -> dict:
"has_attachments": m.has_attachments,
}
+ # Вложения
+ if m.has_attachments:
+ attachments = []
+ for att in m.attachments.all():
+ attachments.append({
+ "id": att.id,
+ "file_name": att.file_name,
+ "file_type": att.file_type,
+ "file_url": att.file.url,
+ "file_size": att.file_size,
+ "mime_type": att.mime_type,
+ })
+ data["attachments"] = attachments
+
# Ответ на сообщение
if m.reply_to_id:
data["reply_to"] = {
diff --git a/backend/communications/urls.py b/backend/communications/urls.py
index 41d3ea06..3d01c05f 100644
--- a/backend/communications/urls.py
+++ b/backend/communications/urls.py
@@ -69,6 +69,11 @@ urlpatterns = [
),
# Attachments
+ path(
+ "api/upload-message/",
+ api_views.upload_message_with_attachments,
+ name="api_upload_message"
+ ),
path(
"api/attachment/upload/",
api_views.upload_attachment,
diff --git a/backend/static/js/components/chatMessageTemplates.js b/backend/static/js/components/chatMessageTemplates.js
index d83ef46a..e27f7623 100644
--- a/backend/static/js/components/chatMessageTemplates.js
+++ b/backend/static/js/components/chatMessageTemplates.js
@@ -80,6 +80,17 @@ export function nameHTML(userId, name, url, meId, profileUrl, detailUrlTemplate)
return `${esc(name || 'Сотрудник')}`;
}
+/**
+ * Форматирует размер файла
+ * @param {number} bytes - Размер в байтах
+ * @returns {string} Размер в человекочитаемом формате
+ */
+export function 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';
+}
+
/**
* Создаёт разделитель дня
* @param {string} dateStr - Дата в текстовом формате
@@ -141,6 +152,52 @@ export function createMessageElement(msg, options = {}) {
);
const time = formatTime(ts);
const text = esc(msg.content || '').replace(/\n/g, '
');
+
+ // Формирование вложений
+ let attachmentsHTML = '';
+ if (msg.has_attachments && msg.attachments && msg.attachments.length > 0) {
+ attachmentsHTML = '