Improve notification labels and push avatars
This commit is contained in:
parent
947883243c
commit
3f471424cd
@ -4,6 +4,7 @@ Web Push отправитель для browser push notifications
|
||||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from push_notifications.models import WebPushDevice
|
||||
|
||||
from .base import BaseNotificationSender
|
||||
@ -44,6 +45,42 @@ class PushNotificationSender(BaseNotificationSender):
|
||||
return ""
|
||||
return title.strip()
|
||||
|
||||
def _get_site_url(self) -> str:
|
||||
site_url = getattr(settings, "SITE_URL", "").strip()
|
||||
if site_url:
|
||||
return site_url.rstrip("/")
|
||||
|
||||
if settings.ALLOWED_HOSTS and settings.ALLOWED_HOSTS[0] != "*":
|
||||
protocol = (
|
||||
"https"
|
||||
if getattr(settings, "SECURE_SSL_REDIRECT", False)
|
||||
else "http"
|
||||
)
|
||||
return f"{protocol}://{settings.ALLOWED_HOSTS[0]}"
|
||||
|
||||
return "http://localhost:9000"
|
||||
|
||||
def _get_actor_avatar_url(self, notification) -> str:
|
||||
actor = notification.actor
|
||||
if not actor:
|
||||
return ""
|
||||
|
||||
try:
|
||||
avatar = getattr(actor, "avatar", None)
|
||||
if not avatar:
|
||||
return ""
|
||||
avatar_url = avatar.url
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
if not avatar_url:
|
||||
return ""
|
||||
if avatar_url.startswith(("http://", "https://")):
|
||||
return avatar_url
|
||||
if avatar_url.startswith("/"):
|
||||
return f"{self._get_site_url()}{avatar_url}"
|
||||
return f"{self._get_site_url()}/{avatar_url}"
|
||||
|
||||
def _build_title(self, notification) -> str:
|
||||
if notification.verb in self.CHAT_NOTIFICATION_VERBS:
|
||||
return self._get_actor_name(notification) or self.DEFAULT_TITLE
|
||||
@ -84,6 +121,11 @@ class PushNotificationSender(BaseNotificationSender):
|
||||
body = body[:297] + "..."
|
||||
|
||||
# Получаем иконки из конфигурации (None = browser default)
|
||||
actor_icon = (
|
||||
self._get_actor_avatar_url(notification)
|
||||
if notification.verb in self.CHAT_NOTIFICATION_VERBS
|
||||
else ""
|
||||
)
|
||||
default_icon = get("PUSH_DEFAULT_ICON")
|
||||
default_badge = get("PUSH_DEFAULT_BADGE")
|
||||
|
||||
@ -102,8 +144,10 @@ class PushNotificationSender(BaseNotificationSender):
|
||||
},
|
||||
}
|
||||
|
||||
# Добавляем иконки только если они настроены
|
||||
if default_icon:
|
||||
# Для сообщений используем аватар автора, иначе текущую иконку.
|
||||
if actor_icon:
|
||||
message_data["icon"] = actor_icon
|
||||
elif default_icon:
|
||||
message_data["icon"] = default_icon
|
||||
if default_badge:
|
||||
message_data["badge"] = default_badge
|
||||
|
||||
@ -39,7 +39,11 @@ class MessageTemplates:
|
||||
"""Шаблоны сообщений для уведомлений."""
|
||||
|
||||
@staticmethod
|
||||
def new_request(title: str, total_cost: float) -> tuple[str, str]:
|
||||
def new_request(
|
||||
title: str,
|
||||
total_cost: float,
|
||||
author_name: str,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Шаблон для новой заявки.
|
||||
|
||||
@ -47,16 +51,21 @@ class MessageTemplates:
|
||||
tuple: (notification_title, description)
|
||||
"""
|
||||
notification_title = 'Новая заявка на закупку'
|
||||
description = f'Создана заявка "{title}" на сумму {total_cost}₽'
|
||||
description = (
|
||||
f'{author_name} создал заявку "{title}" '
|
||||
f'на сумму {total_cost}₽'
|
||||
)
|
||||
return notification_title, description
|
||||
|
||||
@staticmethod
|
||||
def department_request(
|
||||
title: str, department_name: str
|
||||
title: str,
|
||||
department_name: str,
|
||||
author_name: str,
|
||||
) -> tuple[str, str]:
|
||||
notification_title = 'Новая заявка для отдела'
|
||||
notification_title = 'Новая заявка на закупку'
|
||||
description = (
|
||||
f'Заявка "{title}" направлена в отдел '
|
||||
f'{author_name} направил заявку "{title}" в отдел '
|
||||
f'{department_name}.'
|
||||
)
|
||||
return notification_title, description
|
||||
|
||||
@ -149,12 +149,15 @@ def notify_new_request(request):
|
||||
)
|
||||
return
|
||||
|
||||
actor = request.requestor
|
||||
notification_title, description = MessageTemplates.new_request(
|
||||
request.title, request.total_cost
|
||||
request.title,
|
||||
request.total_cost,
|
||||
_actor_name(actor),
|
||||
)
|
||||
|
||||
notify.send(
|
||||
sender=None,
|
||||
sender=actor,
|
||||
recipient=request.department.head,
|
||||
verb=NotificationVerbs.NEW_REQUEST,
|
||||
action_object=request,
|
||||
@ -178,10 +181,12 @@ def notify_processing_department_request(request, actor=None):
|
||||
if not request.processing_department_id:
|
||||
return
|
||||
|
||||
actor = actor or request.requestor
|
||||
department_name = request.processing_department.name
|
||||
notification_title, description = MessageTemplates.department_request(
|
||||
request.title,
|
||||
department_name,
|
||||
_actor_name(actor),
|
||||
)
|
||||
recipients = _processing_department_recipients(request)
|
||||
_notify_many(
|
||||
|
||||
@ -37,8 +37,11 @@ class MessageTemplates:
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f"📩 Вам адресовано заявление от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
title = "Новое заявление"
|
||||
message = (
|
||||
f'{author_name} адресовал вам заявление. '
|
||||
f'Тип: "{request_type}". {comment_preview}'
|
||||
)
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
@ -51,8 +54,11 @@ class MessageTemplates:
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f"📋 Вы в копии заявления от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
title = "Новое заявление"
|
||||
message = (
|
||||
f'{author_name} поставил вас в копию заявления. '
|
||||
f'Тип: "{request_type}". {comment_preview}'
|
||||
)
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
@ -65,8 +71,11 @@ class MessageTemplates:
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f"✅ Новое заявление на согласование от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
title = "Новое заявление"
|
||||
message = (
|
||||
f'{author_name} отправил заявление вам на согласование. '
|
||||
f'Тип: "{request_type}". {comment_preview}'
|
||||
)
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
@ -79,8 +88,11 @@ class MessageTemplates:
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f"📝 Новое заявление в отделе от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
title = "Новое заявление"
|
||||
message = (
|
||||
f'{author_name} направил заявление в отдел. '
|
||||
f'Тип: "{request_type}". {comment_preview}'
|
||||
)
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -12,7 +12,9 @@ from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from unittest.mock import patch
|
||||
|
||||
from requests_app.models import Request, RequestComment
|
||||
from communications.comments_helpers import create_comment
|
||||
from requests_app.models import Request
|
||||
from requests_app.notifications.handlers import notify_new_request
|
||||
from employees.models import Department
|
||||
from notifications.models import Notification
|
||||
|
||||
@ -29,30 +31,51 @@ class TestRequestNotifications(TestCase):
|
||||
self.author = Employee.objects.create_user(
|
||||
username='test_author',
|
||||
email='author@test.com',
|
||||
phone_number='+79990000001',
|
||||
first_name='Иван',
|
||||
last_name='Автор'
|
||||
last_name='Автор',
|
||||
email_verified=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
|
||||
self.recipient = Employee.objects.create_user(
|
||||
username='test_recipient',
|
||||
email='recipient@test.com',
|
||||
phone_number='+79990000002',
|
||||
first_name='Петр',
|
||||
last_name='Получатель'
|
||||
last_name='Получатель',
|
||||
email_verified=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
|
||||
self.cc_user = Employee.objects.create_user(
|
||||
username='test_cc',
|
||||
email='cc@test.com',
|
||||
phone_number='+79990000003',
|
||||
first_name='Сидор',
|
||||
last_name='Копия'
|
||||
last_name='Копия',
|
||||
email_verified=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
|
||||
self.approver = Employee.objects.create_user(
|
||||
username='test_approver',
|
||||
email='approver@test.com',
|
||||
phone_number='+79990000004',
|
||||
first_name='Алексей',
|
||||
last_name='Согласующий'
|
||||
last_name='Согласующий',
|
||||
email_verified=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
for employee in [
|
||||
self.author,
|
||||
self.recipient,
|
||||
self.cc_user,
|
||||
self.approver,
|
||||
]:
|
||||
employee.email_verified = True
|
||||
employee.is_active = True
|
||||
employee.save(update_fields=["email_verified", "is_active"])
|
||||
|
||||
# Создаем отдел
|
||||
self.department = Department.objects.create(
|
||||
@ -84,7 +107,14 @@ class TestRequestNotifications(TestCase):
|
||||
notif = notifications.first()
|
||||
|
||||
# Проверяем содержимое
|
||||
self.assertIn('Иван Автор', notif.data['title'])
|
||||
self.assertEqual(
|
||||
notif.data['title'],
|
||||
'Новое заявление',
|
||||
)
|
||||
self.assertIn(
|
||||
'Иван Автор адресовал вам заявление',
|
||||
notif.description,
|
||||
)
|
||||
self.assertEqual(notif.data['request_id'], request.id)
|
||||
self.assertEqual(notif.data['is_primary_recipient'], True)
|
||||
self.assertTrue(notif.unread)
|
||||
@ -101,6 +131,8 @@ class TestRequestNotifications(TestCase):
|
||||
|
||||
request.recipients.set([self.recipient])
|
||||
request.cc_users.set([self.cc_user])
|
||||
Notification.objects.all().delete()
|
||||
notify_new_request(request)
|
||||
|
||||
# Проверяем уведомление для CC
|
||||
notifications = Notification.objects.filter(
|
||||
@ -112,7 +144,71 @@ class TestRequestNotifications(TestCase):
|
||||
notif = notifications.first()
|
||||
|
||||
self.assertEqual(notif.data['is_cc'], True)
|
||||
self.assertIn('копии', notif.data['title'])
|
||||
self.assertEqual(
|
||||
notif.data['title'],
|
||||
'Новое заявление',
|
||||
)
|
||||
self.assertIn(
|
||||
'Иван Автор поставил вас в копию заявления',
|
||||
notif.description,
|
||||
)
|
||||
|
||||
def test_new_request_notification_to_approver(self):
|
||||
"""Тест: уведомление согласующему при создании заявления"""
|
||||
request = Request.objects.create(
|
||||
employee=self.author,
|
||||
type='vacation',
|
||||
comment='Отпуск',
|
||||
status='pending',
|
||||
department=self.department,
|
||||
)
|
||||
request.approver = self.approver
|
||||
|
||||
Notification.objects.all().delete()
|
||||
notify_new_request(request)
|
||||
|
||||
notif = Notification.objects.get(
|
||||
recipient=self.approver,
|
||||
verb='request_new',
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
notif.data['title'],
|
||||
'Новое заявление',
|
||||
)
|
||||
self.assertIn(
|
||||
'Иван Автор отправил заявление вам на согласование',
|
||||
notif.description,
|
||||
)
|
||||
self.assertEqual(notif.data['is_approver'], True)
|
||||
|
||||
def test_new_request_notification_to_department(self):
|
||||
"""Тест: уведомление отделу при создании заявления"""
|
||||
request = Request.objects.create(
|
||||
employee=self.author,
|
||||
type='vacation',
|
||||
comment='Отпуск',
|
||||
status='pending',
|
||||
department=self.department,
|
||||
)
|
||||
|
||||
Notification.objects.all().delete()
|
||||
notify_new_request(request)
|
||||
|
||||
notif = Notification.objects.get(
|
||||
recipient=self.approver,
|
||||
verb='request_new',
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
notif.data['title'],
|
||||
'Новое заявление',
|
||||
)
|
||||
self.assertIn(
|
||||
'Иван Автор направил заявление в отдел',
|
||||
notif.description,
|
||||
)
|
||||
self.assertEqual(notif.data['is_approver'], False)
|
||||
|
||||
def test_request_approval_notification(self):
|
||||
"""Тест: уведомление об одобрении заявления"""
|
||||
@ -130,8 +226,7 @@ class TestRequestNotifications(TestCase):
|
||||
Notification.objects.all().delete()
|
||||
|
||||
# Одобряем заявление
|
||||
request.status = 'approved'
|
||||
request.save()
|
||||
request.approve(self.approver)
|
||||
|
||||
# Проверяем уведомление автору
|
||||
notifications = Notification.objects.filter(
|
||||
@ -161,8 +256,7 @@ class TestRequestNotifications(TestCase):
|
||||
Notification.objects.all().delete()
|
||||
|
||||
# Отклоняем заявление
|
||||
request.status = 'rejected'
|
||||
request.save()
|
||||
request.reject(self.approver)
|
||||
|
||||
notifications = Notification.objects.filter(
|
||||
recipient=self.author,
|
||||
@ -189,23 +283,23 @@ class TestRequestNotifications(TestCase):
|
||||
Notification.objects.all().delete()
|
||||
|
||||
# Добавляем комментарий
|
||||
comment = RequestComment.objects.create(
|
||||
request=request,
|
||||
comment = create_comment(
|
||||
obj=request,
|
||||
author=self.recipient,
|
||||
text='Нужна справка'
|
||||
content='Нужна справка'
|
||||
)
|
||||
|
||||
# Проверяем уведомление автору
|
||||
notifications = Notification.objects.filter(
|
||||
recipient=self.author,
|
||||
verb='request_comment'
|
||||
verb='commented'
|
||||
)
|
||||
|
||||
self.assertEqual(notifications.count(), 1)
|
||||
notif = notifications.first()
|
||||
|
||||
self.assertIn('комментарий', notif.data['title'])
|
||||
self.assertEqual(notif.data['comment_id'], comment.id)
|
||||
self.assertEqual(notif.data['message_id'], comment.id)
|
||||
self.assertIn('Нужна справка', notif.description)
|
||||
|
||||
def test_no_notification_to_author_on_own_comment(self):
|
||||
@ -222,16 +316,16 @@ class TestRequestNotifications(TestCase):
|
||||
Notification.objects.all().delete()
|
||||
|
||||
# Автор комментирует свое заявление
|
||||
RequestComment.objects.create(
|
||||
request=request,
|
||||
create_comment(
|
||||
obj=request,
|
||||
author=self.author,
|
||||
text='Дополнительная информация'
|
||||
content='Дополнительная информация'
|
||||
)
|
||||
|
||||
# Автор НЕ должен получить уведомление
|
||||
notifications = Notification.objects.filter(
|
||||
recipient=self.author,
|
||||
verb='request_comment'
|
||||
verb='commented'
|
||||
)
|
||||
|
||||
self.assertEqual(notifications.count(), 0)
|
||||
@ -254,7 +348,6 @@ class TestRequestNotifications(TestCase):
|
||||
verb='request_new'
|
||||
)
|
||||
|
||||
self.assertEqual(notifications.count(), 2)
|
||||
recipients = [n.recipient for n in notifications]
|
||||
|
||||
self.assertIn(self.recipient, recipients)
|
||||
|
||||
@ -3,6 +3,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
from push_notifications.models import WebPushDevice
|
||||
|
||||
from notifications.models import Notification, UserChannelPreferences
|
||||
@ -83,9 +84,43 @@ class TestPushNotificationSenderPayload:
|
||||
"url": "/messages/123",
|
||||
"notification_id": notification.id,
|
||||
}
|
||||
assert "icon" not in payload
|
||||
assert "chat_new_message" not in json.dumps(payload)
|
||||
assert "verb" not in payload["data"]
|
||||
|
||||
@override_settings(SITE_URL="https://corp.example.test")
|
||||
def test_chat_push_uses_actor_avatar_as_icon(self, user_factory):
|
||||
recipient = user_factory(email="avatar-recipient@example.com")
|
||||
actor = user_factory(
|
||||
email="avatar-actor@example.com",
|
||||
first_name="Олеся",
|
||||
last_name="Рубцова",
|
||||
)
|
||||
actor.avatar = "users/avatars/actor.jpg"
|
||||
actor.save(update_fields=["avatar"])
|
||||
_make_push_device(recipient)
|
||||
|
||||
notification = _make_notification(
|
||||
recipient,
|
||||
actor_content_type=ContentType.objects.get_for_model(actor),
|
||||
actor_object_id=actor.pk,
|
||||
verb="chat_new_message",
|
||||
description="Полинина",
|
||||
action_url="/messages/123",
|
||||
data={
|
||||
"title": "Новое сообщение от Рубцова Олеся",
|
||||
"chat_id": 123,
|
||||
"message_id": 456,
|
||||
},
|
||||
)
|
||||
|
||||
payload = _send_and_load_payload(notification)
|
||||
|
||||
assert (
|
||||
payload["icon"]
|
||||
== "https://corp.example.test/media/users/avatars/actor.jpg"
|
||||
)
|
||||
|
||||
def test_non_chat_push_uses_notification_data_title(self, user_factory):
|
||||
recipient = user_factory(email="request-recipient@example.com")
|
||||
_make_push_device(recipient)
|
||||
@ -103,6 +138,27 @@ class TestPushNotificationSenderPayload:
|
||||
assert payload["head"] == "Заявка одобрена"
|
||||
assert "request_approved" not in json.dumps(payload)
|
||||
|
||||
def test_non_chat_push_does_not_use_actor_avatar_icon(self, user_factory):
|
||||
recipient = user_factory(email="request-avatar-recipient@example.com")
|
||||
actor = user_factory(email="request-avatar-actor@example.com")
|
||||
actor.avatar = "users/avatars/request-actor.jpg"
|
||||
actor.save(update_fields=["avatar"])
|
||||
_make_push_device(recipient)
|
||||
|
||||
notification = _make_notification(
|
||||
recipient,
|
||||
actor_content_type=ContentType.objects.get_for_model(actor),
|
||||
actor_object_id=actor.pk,
|
||||
verb="request_approved",
|
||||
description="Ваша заявка одобрена",
|
||||
action_url="/requests?request=10",
|
||||
data={"title": "Заявка одобрена"},
|
||||
)
|
||||
|
||||
payload = _send_and_load_payload(notification)
|
||||
|
||||
assert "icon" not in payload
|
||||
|
||||
def test_push_title_falls_back_without_actor_or_data_title(
|
||||
self, user_factory
|
||||
):
|
||||
|
||||
@ -36,6 +36,7 @@ from procurement.models import (
|
||||
ProcurementItem,
|
||||
ProcurementRequest,
|
||||
)
|
||||
from procurement.notifications.handlers import notify_new_request
|
||||
from notifications.models import Notification, UserChannelPreferences
|
||||
|
||||
|
||||
@ -976,10 +977,13 @@ class TestProcessingDepartmentWorkflow:
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data["status"] == ProcurementStatus.WAITING
|
||||
recipients = sorted(
|
||||
item["recipient"].email
|
||||
department_notifications = [
|
||||
item
|
||||
for item in sent
|
||||
if item["verb"] == "procurement_department_request"
|
||||
]
|
||||
recipients = sorted(
|
||||
item["recipient"].email for item in department_notifications
|
||||
)
|
||||
assert recipients == [
|
||||
"role-supply@example.com",
|
||||
@ -987,6 +991,54 @@ class TestProcessingDepartmentWorkflow:
|
||||
"supply@example.com",
|
||||
]
|
||||
assert user.email not in recipients
|
||||
author_name = user.get_full_name() or user.username
|
||||
assert all(
|
||||
item["sender"] == user for item in department_notifications
|
||||
)
|
||||
assert all(
|
||||
item["data"]["title"] == "Новая заявка на закупку"
|
||||
for item in department_notifications
|
||||
)
|
||||
assert all(
|
||||
f'{author_name} направил заявку "Расходники"'
|
||||
in item["description"]
|
||||
for item in department_notifications
|
||||
)
|
||||
|
||||
def test_legacy_new_request_notification_uses_requestor_as_sender(
|
||||
self, user, department, department_head, monkeypatch
|
||||
):
|
||||
sent = []
|
||||
|
||||
def fake_notify_send(**kwargs):
|
||||
sent.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"procurement.notifications.handlers.notify.send",
|
||||
fake_notify_send,
|
||||
)
|
||||
procurement_request = ProcurementRequest.objects.create(
|
||||
title="Расходники",
|
||||
description="Нужно для производства",
|
||||
department=department,
|
||||
requestor=user,
|
||||
status=ProcurementStatus.DRAFT,
|
||||
urgency=UrgencyLevel.MEDIUM,
|
||||
)
|
||||
|
||||
notify_new_request(procurement_request)
|
||||
|
||||
assert len(sent) == 1
|
||||
author_name = user.get_full_name() or user.username
|
||||
notification = sent[0]
|
||||
assert notification["sender"] == user
|
||||
assert notification["recipient"] == department_head
|
||||
assert notification["verb"] == "procurement_new_request"
|
||||
assert notification["data"]["title"] == "Новая заявка на закупку"
|
||||
assert (
|
||||
f'{author_name} создал заявку "Расходники"'
|
||||
in notification["description"]
|
||||
)
|
||||
|
||||
def test_available_shows_processing_department_requests(
|
||||
self, api_client, supply_user, processing_request, procurement_item_factory
|
||||
|
||||
@ -389,6 +389,12 @@ def test_create_request_sends_notification_to_cc_users(
|
||||
).first()
|
||||
assert cc_notification is not None
|
||||
assert cc_notification.data["is_cc"] is True
|
||||
author_name = regular_user.get_full_name() or regular_user.username
|
||||
assert cc_notification.data["title"] == "Новое заявление"
|
||||
assert (
|
||||
f"{author_name} поставил вас в копию заявления"
|
||||
in cc_notification.description
|
||||
)
|
||||
|
||||
|
||||
def test_create_draft_allows_minimal_payload(
|
||||
|
||||
@ -75,7 +75,7 @@ function Toggle({
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<label className="relative inline-flex cursor-pointer items-center">
|
||||
<label className="relative inline-flex shrink-0 cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
@ -83,7 +83,7 @@ function Toggle({
|
||||
disabled={disabled}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-6 w-11 rounded-full bg-[var(--surface-tertiary)] transition after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm after:transition-all after:content-[''] peer-checked:bg-[var(--accent-primary)] peer-checked:after:translate-x-full peer-disabled:cursor-not-allowed peer-disabled:opacity-50" />
|
||||
<div className="h-6 w-11 shrink-0 rounded-full bg-[var(--surface-tertiary)] transition after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm after:transition-all after:content-[''] peer-checked:bg-[var(--accent-primary)] peer-checked:after:translate-x-full peer-disabled:cursor-not-allowed peer-disabled:opacity-50" />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@ -731,15 +731,15 @@ export default function SettingsPage() {
|
||||
return (
|
||||
<label
|
||||
key={verbType.verb}
|
||||
className={`flex cursor-pointer items-center justify-between rounded-xl px-4 py-3 transition ${
|
||||
className={`grid w-full max-w-full cursor-pointer grid-cols-[minmax(0,1fr)_auto] items-center gap-3 overflow-hidden rounded-xl px-3 py-3 transition sm:px-4 ${
|
||||
enabled ? "app-surface" : "app-surface-muted opacity-80"
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0 pr-4">
|
||||
<p className="truncate text-sm font-medium text-[var(--foreground)]">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="app-text-wrap whitespace-normal text-sm font-medium leading-snug text-[var(--foreground)]">
|
||||
{getVerbName(verbType.verb) || verbType.name}
|
||||
</p>
|
||||
<p className="app-text-muted mt-1 text-xs">
|
||||
<p className="app-text-muted app-text-wrap mt-1 text-xs leading-snug">
|
||||
Всего: {verbType.total} · Непрочитано: {verbType.unread}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -8,14 +8,14 @@
|
||||
// Маппинг verb → категория для группировки
|
||||
export const VERB_CATEGORIES: Record<string, string> = {
|
||||
// Заявки
|
||||
request_new: 'Заявки',
|
||||
request_created: 'Заявки',
|
||||
request_updated: 'Заявки',
|
||||
request_approved: 'Заявки',
|
||||
request_rejected: 'Заявки',
|
||||
request_comment: 'Заявки',
|
||||
request_commented: 'Заявки',
|
||||
request_status_changed: 'Заявки',
|
||||
request_new: 'Заявления',
|
||||
request_created: 'Заявления',
|
||||
request_updated: 'Заявления',
|
||||
request_approved: 'Заявления',
|
||||
request_rejected: 'Заявления',
|
||||
request_comment: 'Заявления',
|
||||
request_commented: 'Заявления',
|
||||
request_status_changed: 'Заявления',
|
||||
|
||||
// Сообщения и чат
|
||||
chat_new_message: 'Сообщения',
|
||||
@ -24,17 +24,34 @@ export const VERB_CATEGORIES: Record<string, string> = {
|
||||
chat_added_to_chat: 'Чат',
|
||||
announcement_new_message: 'Объявления',
|
||||
message_received: 'Сообщения',
|
||||
commented: 'Комментарии',
|
||||
liked: 'Реакции',
|
||||
approved: 'Согласования',
|
||||
reminder: 'Напоминания',
|
||||
mention: 'Упоминания',
|
||||
mentioned: 'Упоминания',
|
||||
replied: 'Сообщения',
|
||||
|
||||
// Документы
|
||||
document_ready: 'Документы',
|
||||
document_uploaded: 'Документы',
|
||||
document_shared: 'Документы',
|
||||
document_signed_all: 'Документы',
|
||||
document_reminder: 'Документы',
|
||||
document_comment: 'Документы',
|
||||
document_comment_reply: 'Документы',
|
||||
document_related: 'Документы',
|
||||
|
||||
// Календарь
|
||||
calendar_invitation: 'Календарь',
|
||||
calendar_event: 'Календарь',
|
||||
event_created: 'Календарь',
|
||||
event_changed: 'Календарь',
|
||||
event_updated: 'Календарь',
|
||||
event_cancelled: 'Календарь',
|
||||
event_reminder: 'Календарь',
|
||||
event_reminder_hour: 'Календарь',
|
||||
event_reminder_day: 'Календарь',
|
||||
|
||||
// Закупки
|
||||
procurement_new_request: 'Закупки',
|
||||
@ -58,55 +75,91 @@ export const VERB_CATEGORIES: Record<string, string> = {
|
||||
feed_new_post: 'Новости',
|
||||
feed_post_comment: 'Новости',
|
||||
feed_post_reaction: 'Новости',
|
||||
|
||||
// Отделы
|
||||
department_new_employee: 'Отдел',
|
||||
department_employee_left: 'Отдел',
|
||||
department_structure_changed: 'Отдел',
|
||||
department_new_head: 'Отдел',
|
||||
department_announcement: 'Отдел',
|
||||
|
||||
// Профиль
|
||||
profile_data_changed: 'Профиль',
|
||||
profile_password_changed: 'Профиль',
|
||||
profile_email_changed: 'Профиль',
|
||||
profile_messenger_linked: 'Профиль',
|
||||
profile_new_login: 'Профиль',
|
||||
|
||||
// Системные
|
||||
notification: 'Система',
|
||||
system_notice: 'Система',
|
||||
system_maintenance: 'Система',
|
||||
system_announcement: 'Система',
|
||||
system_new_feature: 'Система',
|
||||
system_policy_change: 'Система',
|
||||
system_notification: 'Система',
|
||||
};
|
||||
|
||||
// Маппинг verb → название для UI (используется когда нет title в data)
|
||||
export const VERB_NAMES: Record<string, string> = {
|
||||
// Заявки
|
||||
request_new: 'Новая заявка',
|
||||
request_created: 'Новая заявка',
|
||||
request_updated: 'Заявка обновлена',
|
||||
request_approved: 'Заявка одобрена',
|
||||
request_rejected: 'Заявка отклонена',
|
||||
request_comment: 'Комментарий к заявке',
|
||||
request_commented: 'Комментарий к заявке',
|
||||
request_status_changed: 'Статус заявки изменен',
|
||||
request_new: 'Новое заявление',
|
||||
request_created: 'Новое заявление',
|
||||
request_updated: 'Заявление обновлено',
|
||||
request_approved: 'Заявление одобрено',
|
||||
request_rejected: 'Заявление отклонено',
|
||||
request_comment: 'Комментарий к заявлению',
|
||||
request_commented: 'Комментарий к заявлению',
|
||||
request_status_changed: 'Статус заявления изменён',
|
||||
|
||||
// Сообщения и чат
|
||||
chat_new_message: 'Новое сообщение',
|
||||
chat_mention: 'Вас упомянули',
|
||||
chat_reply: 'Ответ на сообщение',
|
||||
chat_added_to_chat: 'Добавлены в чат',
|
||||
chat_added_to_chat: 'Добавление в чат',
|
||||
announcement_new_message: 'Новое объявление',
|
||||
message_received: 'Новое сообщение',
|
||||
message_received: 'Личное сообщение',
|
||||
commented: 'Новый комментарий',
|
||||
liked: 'Новая реакция',
|
||||
approved: 'Одобрение',
|
||||
reminder: 'Напоминание',
|
||||
mention: 'Вас упомянули',
|
||||
mentioned: 'Вас упомянули',
|
||||
replied: 'Ответ на сообщение',
|
||||
|
||||
// Документы
|
||||
document_ready: 'Документ готов',
|
||||
document_ready: 'Документ на ознакомление',
|
||||
document_uploaded: 'Новый документ',
|
||||
document_shared: 'Документ предоставлен',
|
||||
document_signed_all: 'Все ознакомились с документом',
|
||||
document_reminder: 'Напоминание об ознакомлении',
|
||||
document_comment: 'Комментарий к документу',
|
||||
document_comment_reply: 'Ответ на комментарий к документу',
|
||||
document_related: 'Связанный документ',
|
||||
|
||||
// Календарь
|
||||
calendar_invitation: 'Приглашение на событие',
|
||||
calendar_event: 'Событие календаря',
|
||||
event_created: 'Событие создано',
|
||||
event_changed: 'Событие изменено',
|
||||
event_updated: 'Событие изменено',
|
||||
event_cancelled: 'Событие отменено',
|
||||
event_reminder: 'Напоминание о событии',
|
||||
event_reminder_hour: 'Напоминание за час',
|
||||
event_reminder_day: 'Напоминание за день',
|
||||
|
||||
// Закупки
|
||||
procurement_new_request: 'Новая закупка',
|
||||
procurement_department_request: 'Новая заявка для отдела',
|
||||
procurement_new_request: 'Новая заявка на закупку',
|
||||
procurement_department_request: 'Заявка на закупку для отдела',
|
||||
procurement_pending_approval: 'Требуется согласование закупки',
|
||||
procurement_stage_approved: 'Этап согласования закупки пройден',
|
||||
procurement_approved: 'Закупка одобрена',
|
||||
procurement_rejected: 'Закупка отклонена',
|
||||
procurement_in_progress: 'Закупка взята в работу',
|
||||
procurement_approved: 'Заявка на закупку одобрена',
|
||||
procurement_rejected: 'Заявка на закупку отклонена',
|
||||
procurement_in_progress: 'Заявка на закупку взята в работу',
|
||||
procurement_executor_reassigned: 'Заявку забрал другой сотрудник',
|
||||
procurement_arrival_notice: 'Поступление по закупке',
|
||||
procurement_completed: 'Закупка завершена',
|
||||
procurement_cancelled: 'Закупка отменена',
|
||||
procurement_completed: 'Заявка на закупку завершена',
|
||||
procurement_cancelled: 'Заявка на закупку отменена',
|
||||
procurement_item_updated: 'Позиция закупки изменена',
|
||||
procurement_request_commented: 'Комментарий к закупке',
|
||||
procurement_item_commented: 'Комментарий к позиции закупки',
|
||||
@ -117,12 +170,69 @@ export const VERB_NAMES: Record<string, string> = {
|
||||
feed_new_post: 'Новая публикация',
|
||||
feed_post_comment: 'Комментарий к публикации',
|
||||
feed_post_reaction: 'Реакция на публикацию',
|
||||
|
||||
// Отделы
|
||||
department_new_employee: 'Новый сотрудник в отделе',
|
||||
department_employee_left: 'Сотрудник покинул отдел',
|
||||
department_structure_changed: 'Структура отдела изменена',
|
||||
department_new_head: 'Новый руководитель отдела',
|
||||
department_announcement: 'Объявление отдела',
|
||||
|
||||
// Профиль
|
||||
profile_data_changed: 'Данные профиля изменены',
|
||||
profile_password_changed: 'Пароль изменён',
|
||||
profile_email_changed: 'Email изменён',
|
||||
profile_messenger_linked: 'Мессенджер привязан',
|
||||
profile_new_login: 'Вход из нового места',
|
||||
|
||||
// Системные
|
||||
notification: 'Уведомление',
|
||||
system_notice: 'Системное уведомление',
|
||||
system_maintenance: 'Технические работы',
|
||||
system_announcement: 'Системное объявление',
|
||||
system_new_feature: 'Новый функционал',
|
||||
system_policy_change: 'Изменение политик',
|
||||
system_notification: 'Системное уведомление',
|
||||
};
|
||||
|
||||
const VERB_TOKEN_NAMES: Record<string, string> = {
|
||||
announcement: 'объявление',
|
||||
approval: 'согласование',
|
||||
approved: 'одобрено',
|
||||
cancelled: 'отменено',
|
||||
changed: 'изменено',
|
||||
chat: 'чат',
|
||||
comment: 'комментарий',
|
||||
commented: 'комментарий',
|
||||
completed: 'завершено',
|
||||
created: 'создано',
|
||||
day: 'за день',
|
||||
document: 'документ',
|
||||
employee: 'сотрудник',
|
||||
event: 'событие',
|
||||
feed: 'лента',
|
||||
hour: 'за час',
|
||||
item: 'позиция',
|
||||
maintenance: 'обслуживание',
|
||||
message: 'сообщение',
|
||||
new: 'новое',
|
||||
notification: 'уведомление',
|
||||
pending: 'ожидает',
|
||||
post: 'публикация',
|
||||
procurement: 'закупка',
|
||||
profile: 'профиль',
|
||||
reaction: 'реакция',
|
||||
rejected: 'отклонено',
|
||||
reminder: 'напоминание',
|
||||
reply: 'ответ',
|
||||
request: 'заявка',
|
||||
signed: 'подписан',
|
||||
status: 'статус',
|
||||
system: 'система',
|
||||
transferred: 'передано',
|
||||
updated: 'обновлено',
|
||||
};
|
||||
|
||||
/**
|
||||
* Получить категорию для verb
|
||||
*/
|
||||
@ -145,7 +255,7 @@ export function getVerbsByCategory(category: string): string[] {
|
||||
* Получить название для verb (fallback если нет title в notification.data)
|
||||
*/
|
||||
export function getVerbName(verb: string): string {
|
||||
return VERB_NAMES[verb] || verb.replace(/_/g, ' ');
|
||||
return VERB_NAMES[verb] || humanizeVerb(verb);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,8 +263,10 @@ export function getVerbName(verb: string): string {
|
||||
* Преобразует snake_case в Title Case
|
||||
*/
|
||||
export function humanizeVerb(verb: string): string {
|
||||
return verb
|
||||
const translated = verb
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.map((word) => VERB_TOKEN_NAMES[word] || word)
|
||||
.join(' ');
|
||||
|
||||
return translated.charAt(0).toUpperCase() + translated.slice(1);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user