import { computed, ref } from 'vue'; import { defineStore } from 'pinia'; import { type ApiInAppNotification, deleteNotification, listNotifications, markAllNotificationsRead, markNotificationRead, } from '../api/notifications'; /** * Pinia store для in-app уведомлений (bell-icon в AppLayout). * * Polling: AppLayout вызывает `load()` на mount + через `usePolling` каждые * 30 сек (как dealsState/admin views). Page Visibility API в usePolling * пускает paus'у при свёрнутой вкладке. * * Optimistic-updates: markRead уменьшает unreadCount + ставит read_at до * ответа API; на fail — silently revert (без toast'а, чтобы не спамить * пользователя при каждом sync-failure). */ export const useNotificationsStore = defineStore('notifications', () => { const items = ref([]); const unreadCount = ref(0); const total = ref(0); const loading = ref(false); const fetchError = ref(false); const sortedItems = computed(() => [...items.value].sort((a, b) => { const ta = a.created_at ? new Date(a.created_at).getTime() : 0; const tb = b.created_at ? new Date(b.created_at).getTime() : 0; return tb - ta; }), ); async function load(limit = 50, unreadOnly = false): Promise { loading.value = true; fetchError.value = false; try { const response = await listNotifications({ limit, unreadOnly }); items.value = response.items; unreadCount.value = response.unread_count; total.value = response.total; } catch { fetchError.value = true; } finally { loading.value = false; } } async function markRead(id: number): Promise { const item = items.value.find((n) => n.id === id); if (!item || item.read_at !== null) return; // Optimistic. const previousReadAt = item.read_at; const nowIso = new Date().toISOString(); item.read_at = nowIso; unreadCount.value = Math.max(0, unreadCount.value - 1); try { const response = await markNotificationRead(id); item.read_at = response.read_at; } catch { // Revert. item.read_at = previousReadAt; unreadCount.value++; } } async function markAllRead(): Promise { if (unreadCount.value === 0) return; // Optimistic. const previousReadAt = new Map(); const nowIso = new Date().toISOString(); items.value.forEach((n) => { if (n.read_at === null) { previousReadAt.set(n.id, n.read_at); n.read_at = nowIso; } }); const previousUnread = unreadCount.value; unreadCount.value = 0; try { await markAllNotificationsRead(); } catch { // Revert. previousReadAt.forEach((readAt, id) => { const item = items.value.find((n) => n.id === id); if (item) item.read_at = readAt; }); unreadCount.value = previousUnread; } } async function remove(id: number): Promise { const item = items.value.find((n) => n.id === id); if (!item) return; const wasUnread = item.read_at === null; const previousItems = [...items.value]; const previousTotal = total.value; const previousUnread = unreadCount.value; // Optimistic. items.value = items.value.filter((n) => n.id !== id); total.value = Math.max(0, total.value - 1); if (wasUnread) unreadCount.value = Math.max(0, unreadCount.value - 1); try { await deleteNotification(id); } catch { // Revert. items.value = previousItems; total.value = previousTotal; unreadCount.value = previousUnread; } } function reset(): void { items.value = []; unreadCount.value = 0; total.value = 0; fetchError.value = false; } return { items, unreadCount, total, loading, fetchError, sortedItems, load, markRead, markAllRead, remove, reset, }; });