Merge branch 'develop'
This commit is contained in:
commit
fdd23ab26f
@ -63,7 +63,8 @@ def get_notifications(request):
|
||||
{
|
||||
'id': n.id,
|
||||
'title': n.title,
|
||||
'message': n.message, # Полное сообщение для списка
|
||||
'message': n.message,
|
||||
'short_message': n.short_message or n.message[:150], # Краткое сообщение для превью
|
||||
'category': n.notification_type.category.code,
|
||||
'category_name': n.notification_type.category.name,
|
||||
'icon': n.notification_type.category.icon,
|
||||
|
||||
@ -184,7 +184,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'Остаток: {budget.remaining_amount}₽ '
|
||||
f'({remaining_ratio*100:.1f}%)'
|
||||
),
|
||||
action_url='/procurement/budgets/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
elif remaining_ratio <= LOW_BUDGET_THRESHOLD:
|
||||
@ -198,7 +198,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'Остаток: {budget.remaining_amount}₽ '
|
||||
f'({remaining_ratio*100:.1f}%)'
|
||||
),
|
||||
action_url='/procurement/budgets/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
@ -289,7 +289,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'ожидает вашего согласования. '
|
||||
f'Сумма: {procurement_request.total_cost}₽'
|
||||
),
|
||||
action_url=f'/procurement/orders/{procurement_request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
@ -330,7 +330,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'{request.user.get_full_name()} одобрил '
|
||||
f'заявку "{procurement_request.title}".'
|
||||
),
|
||||
action_url=f'/procurement/orders/{procurement_request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
@ -353,7 +353,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'Ваша заявка "{procurement_request.title}" '
|
||||
f'была полностью одобрена. Можно приступать к закупке.'
|
||||
),
|
||||
action_url=f'/procurement/orders/{procurement_request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
@ -402,7 +402,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'заявку "{procurement_request.title}". '
|
||||
f'Причина: {approval.comment}'
|
||||
),
|
||||
action_url=f'/procurement/orders/{procurement_request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
@ -513,7 +513,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'взята в работу пользователем '
|
||||
f'{request.user.get_full_name()}.'
|
||||
),
|
||||
action_url=f'/procurement/orders/{procurement_request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
@ -530,7 +530,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'взята в работу пользователем '
|
||||
f'{request.user.get_full_name()}.'
|
||||
),
|
||||
action_url=f'/procurement/orders/{procurement_request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
@ -576,7 +576,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'Заявка "{procurement_request.title}" '
|
||||
f'успешно завершена.'
|
||||
),
|
||||
action_url=f'/procurement/orders/{procurement_request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
@ -659,7 +659,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
f'была отменена автором. '
|
||||
f'Причина: {reason or "не указана"}'
|
||||
),
|
||||
action_url=f'/procurement/orders/{procurement_request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
|
||||
@ -255,9 +255,6 @@ def get_event_recipients(event):
|
||||
|
||||
def get_calendar_url(event):
|
||||
"""
|
||||
Возвращает URL календаря в зависимости от типа события.
|
||||
Для всех типов событий используем параметр event_id для прямого открытия.
|
||||
Возвращает URL календаря.
|
||||
"""
|
||||
# Возвращаем главную страницу календаря с параметром event_id
|
||||
# Это позволит открыть модал события автоматически
|
||||
return f'/?event_id={event.id}'
|
||||
return '/calendar'
|
||||
|
||||
@ -146,7 +146,7 @@ def _create_message_notifications_sync(instance):
|
||||
title=f'Вас упомянул {author.get_full_name() or author.username}',
|
||||
message=truncate_message(content, 100),
|
||||
content_object=instance,
|
||||
action_url=f'/communications/chats/{chat.id}/?message={instance.id}',
|
||||
action_url='/messages',
|
||||
metadata={
|
||||
'chat_id': chat.id,
|
||||
'chat_name': get_chat_name(chat),
|
||||
@ -170,7 +170,7 @@ def _create_message_notifications_sync(instance):
|
||||
title=f'{author.get_full_name() or author.username} ответил на ваше сообщение',
|
||||
message=truncate_message(content, 100),
|
||||
content_object=instance,
|
||||
action_url=f'/communications/chats/{chat.id}/?message={instance.id}',
|
||||
action_url='/messages',
|
||||
metadata={
|
||||
'chat_id': chat.id,
|
||||
'chat_name': get_chat_name(chat),
|
||||
@ -219,7 +219,7 @@ def _create_message_notifications_sync(instance):
|
||||
title=title,
|
||||
message=truncate_message(content, 150 if is_announcement else 100),
|
||||
content_object=instance,
|
||||
action_url=f'/communications/chats/{chat.id}/',
|
||||
action_url='/messages',
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
@ -260,7 +260,7 @@ def create_chat_added_notifications(sender, instance, action, pk_set, **kwargs):
|
||||
title=f'Вас добавили в чат',
|
||||
message=f'Вы были добавлены в чат "{get_chat_name(chat)}"',
|
||||
content_object=chat,
|
||||
action_url=f'/communications/chats/{chat.id}/',
|
||||
action_url='/messages',
|
||||
metadata={
|
||||
'chat_id': chat.id,
|
||||
'chat_type': chat.type,
|
||||
|
||||
@ -358,7 +358,7 @@ def check_all_acknowledged(sender, instance, created, **kwargs):
|
||||
f'"{document.title}"'
|
||||
),
|
||||
content_object=document,
|
||||
action_url='/documents/',
|
||||
action_url='/documents',
|
||||
metadata={
|
||||
'document_id': document.id,
|
||||
'total_recipients': total_recipients,
|
||||
@ -477,7 +477,7 @@ def create_document_ready_notification(document, recipient, send_immediately=Tru
|
||||
f'Требуется ознакомление.'
|
||||
),
|
||||
content_object=document,
|
||||
action_url='/documents/',
|
||||
action_url='/documents',
|
||||
metadata={
|
||||
'document_id': document.id,
|
||||
'uploaded_by_id': (
|
||||
@ -549,7 +549,7 @@ def notify_on_new_comment(sender, instance, created, **kwargs):
|
||||
f'комментарий к документу "{document.title}"'
|
||||
),
|
||||
content_object=comment,
|
||||
action_url=f'/documents/{document.pk}/#comment-{comment.pk}',
|
||||
action_url='/documents',
|
||||
action_text='Посмотреть',
|
||||
metadata={
|
||||
'document_id': document.id,
|
||||
@ -589,7 +589,7 @@ def notify_on_new_comment(sender, instance, created, **kwargs):
|
||||
f'к вашему документу "{document.title}"'
|
||||
),
|
||||
content_object=comment,
|
||||
action_url=f'/documents/{document.pk}/#comment-{comment.pk}',
|
||||
action_url='/documents',
|
||||
action_text='Посмотреть',
|
||||
metadata={
|
||||
'document_id': document.id,
|
||||
@ -646,7 +646,7 @@ def notify_on_related_document_added(sender, instance, action, pk_set, **kwargs)
|
||||
f'с документом "{main_document.title}"'
|
||||
),
|
||||
content_object=main_document,
|
||||
action_url=f'/documents/{main_document.pk}/',
|
||||
action_url='/documents',
|
||||
action_text='Посмотреть',
|
||||
metadata={
|
||||
'document_id': main_document.id,
|
||||
|
||||
@ -77,7 +77,7 @@ def create_comment_notification(sender, instance, created, **kwargs):
|
||||
f' прокомментировал: {truncate_text(comment.text, 80)}'
|
||||
),
|
||||
content_object=post,
|
||||
action_url=f'/post/{post.id}/',
|
||||
action_url='/',
|
||||
metadata={
|
||||
'post_id': post.id,
|
||||
'post_title': post.title,
|
||||
@ -115,7 +115,7 @@ def notify_new_post(post):
|
||||
title=f'Новая публикация {context}',
|
||||
message=f'{post.title}',
|
||||
content_object=post,
|
||||
action_url=f'/post/{post.id}/',
|
||||
action_url='/',
|
||||
metadata={
|
||||
'post_id': post.id,
|
||||
'post_type': post.type,
|
||||
@ -191,7 +191,7 @@ def notify_post_reaction(post, user):
|
||||
f'понравилась публикация "{post.title}"'
|
||||
),
|
||||
content_object=post,
|
||||
action_url=f'/post/{post.id}/',
|
||||
action_url='/',
|
||||
metadata={
|
||||
'post_id': post.id,
|
||||
'post_title': post.title,
|
||||
|
||||
@ -537,7 +537,7 @@ class RequestNotificationProcessor(BaseNotificationProcessor):
|
||||
title=title_map.get(notification_type, f'Обновление заявки #{request_obj.id}'),
|
||||
message=truncate_text(request_obj.comment or '', 150),
|
||||
content_object=request_obj,
|
||||
action_url=f'/requests/{request_obj.id}/',
|
||||
action_url='/requests',
|
||||
metadata={
|
||||
'request_id': request_obj.id,
|
||||
'request_type': request_obj.type,
|
||||
@ -594,7 +594,7 @@ class DocumentNotificationProcessor(BaseNotificationProcessor):
|
||||
title=f'Новый документ: {document.title}',
|
||||
message=truncate_text(document.description or '', 150),
|
||||
content_object=document,
|
||||
action_url=f'/documents/{document.id}/',
|
||||
action_url='/documents',
|
||||
metadata={
|
||||
'document_id': document.id,
|
||||
'document_title': document.title,
|
||||
|
||||
@ -80,7 +80,7 @@ def notify_on_request_status_change(sender, instance, created, **kwargs):
|
||||
f'Создана заявка "{instance.title}" '
|
||||
f'на сумму {instance.total_cost}₽'
|
||||
),
|
||||
action_url=f'/procurement/requests/{instance.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
return
|
||||
@ -189,7 +189,7 @@ def notify_approver(approval):
|
||||
f'ожидает вашего согласования. '
|
||||
f'Сумма: {approval.request.total_cost}₽'
|
||||
),
|
||||
action_url=f'/procurement/requests/{approval.request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
@ -201,7 +201,7 @@ def notify_requestor(request, notification_type_code, title, message):
|
||||
notification_type_code=notification_type_code,
|
||||
title=title,
|
||||
message=message,
|
||||
action_url=f'/procurement/requests/{request.id}/',
|
||||
action_url='/procurement',
|
||||
send_immediately=True,
|
||||
)
|
||||
|
||||
|
||||
@ -269,7 +269,7 @@ def create_comment_notification(sender, instance, created, **kwargs):
|
||||
f'"{request_type}": {instance.text[:100]}'
|
||||
),
|
||||
content_object=request_obj,
|
||||
action_url=f"/requests/{request_obj.id}/",
|
||||
action_url="/requests",
|
||||
metadata={
|
||||
"request_id": request_obj.id,
|
||||
"request_type": request_obj.type,
|
||||
@ -457,7 +457,7 @@ def notify_new_request(request_obj):
|
||||
title=title,
|
||||
message=message,
|
||||
content_object=request_obj,
|
||||
action_url=f"/requests/{request_obj.id}/",
|
||||
action_url="/requests",
|
||||
metadata={
|
||||
"request_id": request_obj.id,
|
||||
"request_type": request_obj.type,
|
||||
@ -562,7 +562,7 @@ def notify_status_change(request_obj, old_status, new_status):
|
||||
title=title,
|
||||
message=message,
|
||||
content_object=request_obj,
|
||||
action_url=f"/requests/{request_obj.id}/",
|
||||
action_url="/requests",
|
||||
metadata={
|
||||
"request_id": request_obj.id,
|
||||
"request_type": request_obj.type,
|
||||
|
||||
256
frontend/src/app/notifications/page.tsx
Normal file
256
frontend/src/app/notifications/page.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
'use client';
|
||||
|
||||
import { AppShell } from '@/components/AppShell';
|
||||
import { useNotifications } from '@/hooks/useApi';
|
||||
import { Bell, Check, CheckCheck, Filter, Search, Trash2 } from 'lucide-react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { ru } from 'date-fns/locale/ru';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const { notifications: notificationsData, loading, markAsRead, markAllAsRead } = useNotifications();
|
||||
const notifications = Array.isArray(notificationsData) ? notificationsData : [];
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [filterRead, setFilterRead] = useState<'all' | 'unread' | 'read'>('all');
|
||||
const [filterCategory, setFilterCategory] = useState<string>('');
|
||||
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||
|
||||
// Уникальные категории
|
||||
const categories = useMemo(() => {
|
||||
const cats = new Set<string>();
|
||||
notifications.forEach((n: any) => {
|
||||
if (n.category_name) cats.add(n.category_name);
|
||||
});
|
||||
return Array.from(cats).sort();
|
||||
}, [notifications]);
|
||||
|
||||
// Фильтрация
|
||||
const filteredNotifications = useMemo(() => {
|
||||
return notifications.filter((n: any) => {
|
||||
// Фильтр по прочитанности
|
||||
if (filterRead === 'unread' && n.is_read) return false;
|
||||
if (filterRead === 'read' && !n.is_read) return false;
|
||||
|
||||
// Фильтр по категории
|
||||
if (filterCategory && n.category_name !== filterCategory) return false;
|
||||
|
||||
// Поиск
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
const title = (n.title || '').toLowerCase();
|
||||
const message = (n.message || '').toLowerCase();
|
||||
if (!title.includes(q) && !message.includes(q)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [notifications, search, filterRead, filterCategory]);
|
||||
|
||||
const unreadCount = notifications.filter((n: any) => !n.is_read).length;
|
||||
|
||||
const handleNotificationClick = async (notification: any) => {
|
||||
if (!notification.is_read) {
|
||||
await markAsRead(notification.id);
|
||||
}
|
||||
if (notification.action_url) {
|
||||
window.location.href = notification.action_url;
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryIconEmoji = (categoryCode?: string, icon?: string) => {
|
||||
// Маппинг категорий на эмодзи
|
||||
const iconMap: Record<string, string> = {
|
||||
'communications': '💬',
|
||||
'messages': '💬',
|
||||
'requests': '📝',
|
||||
'documents': '📄',
|
||||
'calendar': '📅',
|
||||
'system': '⚙️',
|
||||
'finance': '💰',
|
||||
'procurement': '🛒',
|
||||
'equipment': '🖥️',
|
||||
};
|
||||
|
||||
return iconMap[categoryCode || ''] || '📋';
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<div className="mx-auto max-w-5xl px-4 py-6">
|
||||
{/* Заголовок */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Уведомления</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
{unreadCount > 0 ? `${unreadCount} непрочитанных` : 'Все прочитано'}
|
||||
</p>
|
||||
</div>
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
onClick={markAllAsRead}
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-sky-600 px-4 py-2 text-sm font-medium text-white hover:bg-sky-700 transition"
|
||||
>
|
||||
<CheckCheck size={16} />
|
||||
Прочитать все
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Поиск и фильтры */}
|
||||
<div className="mb-4 space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search size={16} className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Поиск по уведомлениям..."
|
||||
className="w-full rounded-lg border border-gray-200 bg-gray-50 py-2.5 pl-9 pr-3 text-sm text-gray-800 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFiltersOpen((v) => !v)}
|
||||
className={`relative inline-flex items-center justify-center rounded-lg border p-2.5 transition ${
|
||||
filtersOpen
|
||||
? 'border-sky-400 bg-sky-50 text-sky-600'
|
||||
: 'border-gray-200 bg-gray-50 text-gray-500 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Filter size={16} />
|
||||
{(filterRead !== 'all' || filterCategory) && (
|
||||
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-sky-500 px-1 text-[10px] font-bold text-white">
|
||||
{[filterRead !== 'all', filterCategory].filter(Boolean).length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{filtersOpen && (
|
||||
<div className="flex flex-col gap-2 rounded-xl border border-gray-200 bg-gray-50 p-3">
|
||||
<select
|
||||
value={filterRead}
|
||||
onChange={(e) => setFilterRead(e.target.value as any)}
|
||||
className="rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-800"
|
||||
>
|
||||
<option value="all">Все уведомления</option>
|
||||
<option value="unread">Только непрочитанные</option>
|
||||
<option value="read">Только прочитанные</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={filterCategory}
|
||||
onChange={(e) => setFilterCategory(e.target.value)}
|
||||
className="rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-800"
|
||||
>
|
||||
<option value="">Все категории</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{(filterRead !== 'all' || filterCategory) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFilterRead('all');
|
||||
setFilterCategory('');
|
||||
}}
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 transition"
|
||||
>
|
||||
Очистить фильтры
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Список уведомлений */}
|
||||
{loading ? (
|
||||
<div className="rounded-xl bg-gray-50 p-12 text-center">
|
||||
<div className="mx-auto mb-3 h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-sky-500"></div>
|
||||
<p className="text-sm text-gray-500">Загрузка уведомлений...</p>
|
||||
</div>
|
||||
) : filteredNotifications.length === 0 ? (
|
||||
<div className="rounded-xl bg-gray-50 p-12 text-center">
|
||||
<Bell size={48} className="mx-auto mb-3 text-gray-300" />
|
||||
<p className="text-base font-medium text-gray-700">
|
||||
{search || filterRead !== 'all' || filterCategory
|
||||
? 'Нет уведомлений по заданным критериям'
|
||||
: 'Нет уведомлений'}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
{search || filterRead !== 'all' || filterCategory
|
||||
? 'Попробуйте изменить фильтры'
|
||||
: 'Все новые уведомления появятся здесь'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredNotifications.map((notification: any) => (
|
||||
<article
|
||||
key={notification.id}
|
||||
onClick={() => handleNotificationClick(notification)}
|
||||
className={`group cursor-pointer rounded-xl border transition hover:shadow-md ${
|
||||
notification.is_read
|
||||
? 'border-gray-200 bg-white'
|
||||
: 'border-sky-200 bg-sky-50/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-4 p-4">
|
||||
{/* Иконка */}
|
||||
<div
|
||||
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-xl ${
|
||||
notification.is_read ? 'bg-gray-100' : 'bg-sky-100'
|
||||
}`}
|
||||
style={
|
||||
notification.color
|
||||
? { backgroundColor: notification.color + '20', color: notification.color }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{getCategoryIconEmoji(notification.category, notification.icon)}
|
||||
</div>
|
||||
|
||||
{/* Содержимое */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-start justify-between gap-2">
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-sky-600 transition">
|
||||
{notification.title}
|
||||
</h3>
|
||||
{!notification.is_read && (
|
||||
<div className="h-2 w-2 shrink-0 rounded-full bg-sky-500 mt-1.5"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mb-2 text-sm text-gray-600 line-clamp-2">
|
||||
{notification.short_message || notification.message}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-gray-400">
|
||||
{notification.category_name && (
|
||||
<span className="rounded bg-gray-100 px-2 py-0.5 font-medium text-gray-600">
|
||||
{notification.category_name}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{formatDistanceToNow(new Date(notification.created_at), {
|
||||
addSuffix: true,
|
||||
locale: ru,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@ -179,7 +179,8 @@ export function useNotifications() {
|
||||
async function fetchNotifications() {
|
||||
try {
|
||||
const data = await apiClient.getNotifications();
|
||||
const notifs = data.results || data;
|
||||
// Бэкенд возвращает { notifications: [...] }
|
||||
const notifs = data.notifications || data.results || data;
|
||||
// Убеждаемся что notifs это массив
|
||||
const notificationsArray = Array.isArray(notifs) ? notifs : [];
|
||||
setNotifications(notificationsArray);
|
||||
|
||||
@ -792,13 +792,13 @@ class ApiClient {
|
||||
}
|
||||
|
||||
async markNotificationAsRead(id: number): Promise<void> {
|
||||
await this.request(`/api/v1/notifications/${id}/mark_read/`, {
|
||||
await this.request(`/api/v1/notifications/${id}/read/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
async markAllNotificationsAsRead(): Promise<void> {
|
||||
await this.request('/api/v1/notifications/mark_all_read/', {
|
||||
await this.request('/api/v1/notifications/read-all/', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user