From b636e13382ce1f23787ef3bbc6976bec26fd6af9 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: Sun, 30 Nov 2025 10:50:09 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0?= =?UTF-8?q?/=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D0=B0/=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=BE=D1=82=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BE=D1=82=D1=80=D1=83=D0=B4=D0=BD=D0=B8=D0=BA=D0=B0=20=D1=81?= =?UTF-8?q?=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=BC=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=D0=BC=20=D1=81=D1=82=D0=B0=D1=80=D1=8B=D1=85=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...essengermessage_sender_account_and_more.py | 98 +++++++++++++++++++ backend/communications/urls.py | 5 - backend/communications/views.py | 35 ------- backend/employees/signals.py | 28 +++++- .../css/components/chat-list-enhanced.css | 34 +------ .../js/components/employeeFormHandler.js | 53 ++++++---- .../templates/communications/chat_list.html | 17 ---- .../communications/messengers/telegram.html | 80 --------------- .../communications/messengers/wechat.html | 94 ------------------ .../communications/messengers/whatsapp.html | 80 --------------- .../components/employee_form_personal.html | 27 +++++ .../components/employee_form_scripts.html | 96 ++++++++++++++++++ 12 files changed, 284 insertions(+), 363 deletions(-) create mode 100644 backend/communications/migrations/0014_remove_messengermessage_sender_account_and_more.py diff --git a/backend/communications/migrations/0014_remove_messengermessage_sender_account_and_more.py b/backend/communications/migrations/0014_remove_messengermessage_sender_account_and_more.py new file mode 100644 index 00000000..13c6c1dd --- /dev/null +++ b/backend/communications/migrations/0014_remove_messengermessage_sender_account_and_more.py @@ -0,0 +1,98 @@ +# Generated by Django 5.2.4 on 2025-11-30 07:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('communications', '0013_messengerintegration_messengeraccount_and_more'), + ] + + operations = [ + # Сначала удаляем индексы, которые зависят от полей + migrations.RemoveIndex( + model_name='messengermessage', + name='communicati_integra_af291e_idx', + ), + migrations.RemoveIndex( + model_name='messengermessage', + name='communicati_sender__496a25_idx', + ), + migrations.RemoveIndex( + model_name='messengermessage', + name='communicati_status_9d880b_idx', + ), + migrations.RemoveIndex( + model_name='messengermessage', + name='communicati_synced__4496a8_idx', + ), + # Удаляем unique_together + migrations.AlterUniqueTogether( + name='messengermessage', + unique_together=None, + ), + # Теперь удаляем поля + migrations.RemoveField( + model_name='messengermessage', + name='sender_account', + ), + migrations.RemoveField( + model_name='messengermessage', + name='recipient_account', + ), + migrations.RemoveField( + model_name='messengermessage', + name='integration', + ), + migrations.RemoveField( + model_name='messengermessage', + name='internal_message', + ), + # Удаляем индексы MessengerAccount + migrations.RemoveIndex( + model_name='messengeraccount', + name='communicati_user_id_acfa5e_idx', + ), + migrations.RemoveIndex( + model_name='messengeraccount', + name='communicati_externa_2dfbcf_idx', + ), + migrations.RemoveIndex( + model_name='messengeraccount', + name='communicati_phone_n_8df544_idx', + ), + # Удаляем unique_together MessengerAccount + migrations.AlterUniqueTogether( + name='messengeraccount', + unique_together=None, + ), + # Удаляем поля MessengerAccount + migrations.RemoveField( + model_name='messengeraccount', + name='user', + ), + migrations.RemoveField( + model_name='messengeraccount', + name='integration', + ), + # Удаляем индексы MessengerIntegration + migrations.RemoveIndex( + model_name='messengerintegration', + name='communicati_messeng_c8457c_idx', + ), + migrations.RemoveIndex( + model_name='messengerintegration', + name='communicati_status_7fb887_idx', + ), + # Теперь можно удалить модели + migrations.DeleteModel( + name='MessengerMessage', + ), + migrations.DeleteModel( + name='MessengerAccount', + ), + migrations.DeleteModel( + name='MessengerIntegration', + ), + ] diff --git a/backend/communications/urls.py b/backend/communications/urls.py index adca8746..6caffdc2 100644 --- a/backend/communications/urls.py +++ b/backend/communications/urls.py @@ -22,11 +22,6 @@ urlpatterns = [ name="start_private_chat" ), - # Messenger Views - path("messengers/whatsapp/", views.whatsapp_view, name="whatsapp"), - path("messengers/telegram/", views.telegram_view, name="telegram"), - path("messengers/wechat/", views.wechat_view, name="wechat"), - # API Endpoints # Chat management path("api/chat/create/", api_views.create_chat, name="api_create_chat"), diff --git a/backend/communications/views.py b/backend/communications/views.py index dfe24906..7910d148 100644 --- a/backend/communications/views.py +++ b/backend/communications/views.py @@ -452,38 +452,3 @@ def chat_mark_read(request, pk: int): return JsonResponse( {"ok": True, "last_read_at": int(ts.timestamp() * 1000)} ) - - -# ===== Messenger Views ===== - -@login_required -def whatsapp_view(request): - """WhatsApp интеграция""" - from django.shortcuts import render - return render(request, 'communications/messengers/whatsapp.html', { - 'messenger_name': 'WhatsApp', - 'messenger_icon': 'bi-whatsapp', - 'messenger_color': '#25D366' - }) - - -@login_required -def telegram_view(request): - """Telegram интеграция""" - from django.shortcuts import render - return render(request, 'communications/messengers/telegram.html', { - 'messenger_name': 'Telegram', - 'messenger_icon': 'bi-telegram', - 'messenger_color': '#0088cc' - }) - - -@login_required -def wechat_view(request): - """WeChat интеграция""" - from django.shortcuts import render - return render(request, 'communications/messengers/wechat.html', { - 'messenger_name': 'WeChat', - 'messenger_icon': 'bi-wechat', - 'messenger_color': '#09B83E' - }) diff --git a/backend/employees/signals.py b/backend/employees/signals.py index 59a031b9..429e06ce 100644 --- a/backend/employees/signals.py +++ b/backend/employees/signals.py @@ -1,4 +1,5 @@ # users/signals.py +import os from django.db.models.signals import post_save, pre_save from django.dispatch import receiver from django.utils import timezone @@ -9,7 +10,32 @@ from bots.models import BotSubscriber @receiver(pre_save, sender=Employee) -def unbind_bot_subscriber_on_contact_change(sender, instance: Employee, **kwargs): +def delete_old_avatar_on_change(sender, instance: Employee, **kwargs): + """Удаляем старый файл аватара при замене на новый или при удалении.""" + if instance.pk is None: + # Новый пользователь - нечего удалять + return + + try: + old = sender.objects.get(pk=instance.pk) + except sender.DoesNotExist: + return + + # Если аватар изменился или удален + if old.avatar and old.avatar != instance.avatar: + # Удаляем старый файл с диска + if os.path.isfile(old.avatar.path): + try: + os.remove(old.avatar.path) + except Exception as e: + # Логируем ошибку, но не прерываем сохранение + print(f"Error deleting old avatar: {e}") + + +@receiver(pre_save, sender=Employee) +def unbind_bot_subscriber_on_contact_change( + sender, instance: Employee, **kwargs +): # Игнорируем новых пользователей if instance.pk is None: return diff --git a/backend/static/css/components/chat-list-enhanced.css b/backend/static/css/components/chat-list-enhanced.css index 1ed2c66c..e892b86f 100644 --- a/backend/static/css/components/chat-list-enhanced.css +++ b/backend/static/css/components/chat-list-enhanced.css @@ -187,39 +187,7 @@ color: white; } -/* Мессенджеры - ссылки */ -.chat-type-btn[data-messenger] { - text-decoration: none; - transition: background-color 0.2s ease, transform 0.2s ease; -} - -.chat-type-btn[data-messenger]:hover { - background-color: var(--bs-light) !important; - transform: scale(1.05); -} - -.chat-type-btn[data-messenger] i { - transition: transform 0.2s ease; -} - -.chat-type-btn[data-messenger]:hover i { - transform: scale(1.1); -} - -/* Цвета иконок мессенджеров */ -.chat-type-btn[data-messenger="whatsapp"] i { - color: #25D366 !important; -} - -.chat-type-btn[data-messenger="telegram"] i { - color: #0088cc !important; -} - -.chat-type-btn[data-messenger="wechat"] i { - color: #09B83E !important; -} - -.chat-type-badge { +.chat-type-btn .badge { position: absolute; top: 0.25rem; right: 0.25rem; diff --git a/backend/static/js/components/employeeFormHandler.js b/backend/static/js/components/employeeFormHandler.js index b971d8b7..6265bc6c 100644 --- a/backend/static/js/components/employeeFormHandler.js +++ b/backend/static/js/components/employeeFormHandler.js @@ -177,20 +177,43 @@ export function initEmployeeForm(options = {}) { const skills = collectSkillsPayload(); if (skills) payload.skills_ids = skills; - // avatar (Base64) / clear - const avatarFile = document.getElementById('f_avatar')?.files?.[0] || null; - const clearAvatar = form.querySelector('input[name="clear_avatar"]')?.checked; + // avatar (файл через FormData) + const avatarInput = document.getElementById('avatarInput'); + const avatarFile = avatarInput?.files?.[0] || null; + + let body; + let headers = { + 'X-CSRFToken': form.querySelector('[name="csrfmiddlewaretoken"]')?.value || '' + }; + if (avatarFile) { - try { - const dataUrl = await readFileAsDataURL(avatarFile); - payload.avatar = dataUrl; // Base64ImageField ожидает data URL - } catch (_) {} - } else if (clearAvatar) { - payload.avatar = ''; // Очистка + // Если есть файл - используем FormData + const formData = new FormData(); + formData.append('avatar', avatarFile); + + // Добавляем остальные поля + Object.entries(payload).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach(v => formData.append(key, v)); + } else { + formData.append(key, value); + } + }); + + if (skills) { + skills.forEach(id => formData.append('skills_ids', id)); + } + + body = formData; + // Не устанавливаем Content-Type для FormData - браузер сам добавит boundary + } else { + // Если файла нет - используем JSON + headers['Content-Type'] = 'application/json'; + body = JSON.stringify(payload); } // Если нечего отправлять — просто обновим UI - if (Object.keys(payload).length === 0) { + if (Object.keys(payload).length === 0 && !avatarFile) { setSubmitting(false); if (window.bootstrap) { try { @@ -201,16 +224,10 @@ export function initEmployeeForm(options = {}) { return; } - const csrftoken = form.querySelector('input[name=csrfmiddlewaretoken]')?.value || ''; const resp = await fetch(endpoint, { method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - 'X-CSRFToken': csrftoken - }, - body: JSON.stringify(payload), + headers: headers, + body: body, credentials: 'same-origin' }); diff --git a/backend/templates/communications/chat_list.html b/backend/templates/communications/chat_list.html index dab7239e..cf01099e 100644 --- a/backend/templates/communications/chat_list.html +++ b/backend/templates/communications/chat_list.html @@ -61,23 +61,6 @@ 0 - - {# Разделитель #} -
- - {# Мессенджеры #} - - - - - - - - - diff --git a/backend/templates/communications/messengers/telegram.html b/backend/templates/communications/messengers/telegram.html index a72fc37c..e69de29b 100644 --- a/backend/templates/communications/messengers/telegram.html +++ b/backend/templates/communications/messengers/telegram.html @@ -1,80 +0,0 @@ -{# backend/templates/communications/messengers/telegram.html #} -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ messenger_name }}{% endblock %} - -{% block extra_css %} - - -{% endblock %} - -{% block content %} -
-
-
- {% include "includes/components/page_header.html" with icon=messenger_icon title=messenger_name %} - -
-
-
-
- -

Загрузка {{ messenger_name }}...

-
- -
-
-
- - -
-
-
-{% endblock %} diff --git a/backend/templates/communications/messengers/wechat.html b/backend/templates/communications/messengers/wechat.html index 367fd431..e69de29b 100644 --- a/backend/templates/communications/messengers/wechat.html +++ b/backend/templates/communications/messengers/wechat.html @@ -1,94 +0,0 @@ -{# backend/templates/communications/messengers/wechat.html #} -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ messenger_name }}{% endblock %} - -{% block extra_css %} - - -{% endblock %} - -{% block content %} -
-
-
- {% include "includes/components/page_header.html" with icon=messenger_icon title=messenger_name %} - -
- - Внимание: WeChat не имеет полноценной веб-версии. - Используйте официальное приложение. -
- -
-
-
-
- -

Загрузка {{ messenger_name }}...

-
- -
-
-
- - -
-
-
-{% endblock %} diff --git a/backend/templates/communications/messengers/whatsapp.html b/backend/templates/communications/messengers/whatsapp.html index 95402df3..e69de29b 100644 --- a/backend/templates/communications/messengers/whatsapp.html +++ b/backend/templates/communications/messengers/whatsapp.html @@ -1,80 +0,0 @@ -{# backend/templates/communications/messengers/whatsapp.html #} -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ messenger_name }}{% endblock %} - -{% block extra_css %} - - -{% endblock %} - -{% block content %} -
-
-
- {% include "includes/components/page_header.html" with icon=messenger_icon title=messenger_name %} - -
-
-
-
- -

Загрузка {{ messenger_name }}...

-
- -
-
-
- - -
-
-
-{% endblock %} diff --git a/backend/templates/employees/components/employee_form_personal.html b/backend/templates/employees/components/employee_form_personal.html index e4b25007..f3baa31f 100644 --- a/backend/templates/employees/components/employee_form_personal.html +++ b/backend/templates/employees/components/employee_form_personal.html @@ -9,6 +9,33 @@
+ {# Фотография #} +
+ +
+
+ {% if emp.avatar %} + Фото + {% else %} +
+ +
+ {% endif %} +
+
+ + Форматы: JPG, PNG, GIF. Максимум 5 МБ. + {% if emp.avatar %} + + {% endif %} +
+
+
+
{ + const file = e.target.files[0]; + if (!file) return; + + // Проверка размера (5 МБ) + if (file.size > 5 * 1024 * 1024) { + alert('Файл слишком большой. Максимум 5 МБ.'); + avatarInput.value = ''; + return; + } + + // Проверка типа + if (!file.type.match(/^image\/(jpeg|jpg|png|gif)$/i)) { + alert('Неподдерживаемый формат. Используйте JPG, PNG или GIF.'); + avatarInput.value = ''; + return; + } + + // Показываем предпросмотр + const reader = new FileReader(); + reader.onload = (event) => { + if (avatarPreview.tagName === 'IMG') { + avatarPreview.src = event.target.result; + } else { + // Заменяем placeholder на img + const img = document.createElement('img'); + img.id = 'avatarPreview'; + img.src = event.target.result; + img.alt = 'Фото'; + img.className = 'rounded'; + img.style.cssText = 'width: 120px; height: 120px; object-fit: cover; border: 2px solid #dee2e6;'; + avatarPreview.replaceWith(img); + } + }; + reader.readAsDataURL(file); + }); + + // Удаление фото + if (removeBtn) { + removeBtn.addEventListener('click', async () => { + if (!confirm('Удалить фотографию?')) return; + + try { + const form = document.getElementById('apiEditForm'); + const endpoint = form?.dataset?.endpoint; + if (!endpoint) { + alert('Ошибка: не указан endpoint'); + return; + } + + const response = await fetch(endpoint, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '' + }, + body: JSON.stringify({ avatar: null }) + }); + + if (response.ok) { + // Заменяем изображение на placeholder + const placeholder = document.createElement('div'); + placeholder.id = 'avatarPreview'; + placeholder.className = 'rounded d-flex align-items-center justify-content-center bg-light text-muted'; + placeholder.style.cssText = 'width: 120px; height: 120px; border: 2px solid #dee2e6;'; + placeholder.innerHTML = ''; + + document.getElementById('avatarPreview').replaceWith(placeholder); + removeBtn.remove(); + avatarInput.value = ''; + + alert('Фотография удалена'); + } else { + const data = await response.json(); + alert('Ошибка: ' + (data.error || 'Не удалось удалить фото')); + } + } catch (err) { + console.error('Avatar remove error:', err); + alert('Ошибка при удалении фото'); + } + }); + } }