diff --git a/src/app/dashboard/settings/notifications/page.tsx b/src/app/dashboard/settings/notifications/page.tsx new file mode 100644 index 0000000..355c321 --- /dev/null +++ b/src/app/dashboard/settings/notifications/page.tsx @@ -0,0 +1,172 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiFetch, type Notification, type NotificationSeverity } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +type InboxFilter = 'all' | 'unread' | 'action_required' | 'critical' | 'intelligence' | 'system'; + +const FILTERS: { key: InboxFilter; label: string }[] = [ + { key: 'all', label: 'Toate' }, + { key: 'unread', label: 'Necitite' }, + { key: 'action_required', label: 'Necesită acțiune' }, + { key: 'critical', label: 'Critice' }, + { key: 'intelligence', label: 'Intelligence' }, + { key: 'system', label: 'Sistem' }, +]; + +const SEVERITY_COLORS: Record = { + CRITICAL: 'bg-signal-danger text-paper', + ACTION_REQUIRED: 'bg-signal-warn text-paper', + WARNING: 'bg-signal-warn/20 text-signal-warn', + INFORMATION: 'bg-paper-sunken text-ink-soft', + INTELLIGENCE: 'bg-bronze-wash text-bronze-deep', + SYSTEM: 'bg-paper-sunken text-ink-faint', +}; + +const SEVERITY_LABELS: Record = { + CRITICAL: 'Critic', + ACTION_REQUIRED: 'Acțiune', + WARNING: 'Avertisment', + INFORMATION: 'Informare', + INTELLIGENCE: 'Intelligence', + SYSTEM: 'Sistem', +}; + +export default function NotificationsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const queryClient = useQueryClient(); + const [filter, setFilter] = useState('all'); + + const { data: notifications = [], isLoading } = useQuery({ + queryKey: ['notifications', tenantId, filter], + queryFn: () => + apiFetch(`/v1/notifications?filter=${filter}`, { tenantId }), + enabled: Boolean(tenantId), + staleTime: 15_000, + }); + + const markRead = useMutation({ + mutationFn: (ids: string[]) => + apiFetch('/v1/notifications/mark-read', { method: 'POST', tenantId, body: { ids } }), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['notifications', tenantId] }); + await queryClient.invalidateQueries({ queryKey: ['notifications-unread', tenantId] }); + }, + }); + + const dismiss = useMutation({ + mutationFn: (id: string) => + apiFetch(`/v1/notifications/${id}/dismiss`, { method: 'POST', tenantId }), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['notifications', tenantId] }); + await queryClient.invalidateQueries({ queryKey: ['notifications-unread', tenantId] }); + }, + }); + + const unreadIds = notifications.filter((n) => !n.readAt).map((n) => n.id); + + return ( +
+
+
+

Notificări

+

+ {unreadIds.length > 0 + ? `${unreadIds.length} necitite în workspace.` + : 'Nicio notificare necitită.'} +

+
+ {unreadIds.length > 0 && ( + + )} +
+ +
+ {FILTERS.map(({ key, label }) => ( + + ))} +
+ + {isLoading &&

Se încarcă…

} + + {!isLoading && notifications.length === 0 && ( +
+

Nicio notificare

+

+ Notificările apar automat când se produc evenimente în workspace. +

+
+ )} + +
    + {notifications.map((notif) => ( +
  • + + {SEVERITY_LABELS[notif.severity]} + +
    +

    + {notif.title} +

    + {notif.body && ( +

    {notif.body}

    + )} +

    + {new Date(notif.createdAt).toLocaleString('ro-RO', { + day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', + })} + {notif.aggregatedCount > 1 && ` · ${notif.aggregatedCount} evenimente similare`} +

    +
    +
    + {!notif.readAt && ( + + )} + +
    +
  • + ))} +
+
+ ); +}