diff --git a/src/app/dashboard/alerts/page.tsx b/src/app/dashboard/alerts/page.tsx new file mode 100644 index 0000000..2392d1c --- /dev/null +++ b/src/app/dashboard/alerts/page.tsx @@ -0,0 +1,236 @@ +'use client'; + +import Link from 'next/link'; +import { useQuery } from '@tanstack/react-query'; +import { apiFetch, type Transaction, type Goal, type Decision, type Notification } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Task { + id: string; + title: string; + status: string; + dueDate?: string; +} + +interface Alert { + id: string; + level: 'critical' | 'warning' | 'info'; + module: string; + message: string; + href: string; + detail?: string; +} + +const LEVEL_CFG = { + critical: { + borderClass: 'border-l-signal-danger', + bgClass: 'bg-[color:oklch(var(--signal-danger)/0.05)]', + badgeClass: 'bg-signal-danger text-white', + label: 'CRITIC', + icon: '!', + }, + warning: { + borderClass: 'border-l-signal-warn', + bgClass: 'bg-[color:oklch(var(--signal-warn)/0.05)]', + badgeClass: 'bg-signal-warn text-white', + label: 'AVERTISMENT', + icon: '⚠', + }, + info: { + borderClass: 'border-l-bronze-deep', + bgClass: 'bg-bronze-wash/20', + badgeClass: 'bg-bronze-deep text-white', + label: 'INFO', + icon: 'i', + }, +}; + +export default function AlertsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + + const { data: tasks = [] } = useQuery({ + queryKey: ['tasks-alerts', tenantId], + queryFn: () => apiFetch('/v1/tasks', { tenantId }), + enabled: Boolean(tenantId), + }); + const { data: transactions = [] } = useQuery({ + queryKey: ['tx-alerts', tenantId], + queryFn: () => apiFetch('/v1/transactions', { tenantId }), + enabled: Boolean(tenantId), + }); + const { data: goals = [] } = useQuery({ + queryKey: ['goals-alerts', tenantId], + queryFn: () => apiFetch('/v1/goals', { tenantId }), + enabled: Boolean(tenantId), + }); + const { data: decisions = [] } = useQuery({ + queryKey: ['decisions-alerts', tenantId], + queryFn: () => apiFetch('/v1/decisions', { tenantId }), + enabled: Boolean(tenantId), + }); + const { data: notifications = [] } = useQuery({ + queryKey: ['notif-alerts', tenantId], + queryFn: () => apiFetch('/v1/notifications?filter=unread', { tenantId }), + enabled: Boolean(tenantId), + }); + + const now = new Date(); + const in7Days = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); + + const alerts: Alert[] = []; + + const overdueTasks = tasks.filter((t) => t.status !== 'done' && t.dueDate && new Date(t.dueDate) < now); + if (overdueTasks.length > 0) + alerts.push({ + id: 'tasks-overdue', + level: 'critical', + module: 'Sarcini', + message: `${overdueTasks.length} sarcini restante`, + href: '/dashboard/tasks', + detail: overdueTasks.slice(0, 3).map((t) => t.title).join(', '), + }); + + const missingEvidence = transactions.filter((t) => t.evidenceStatus === 'missing'); + if (missingEvidence.length > 0) + alerts.push({ + id: 'tx-missing', + level: 'critical', + module: 'Tranzacții', + message: `${missingEvidence.length} tranzacții fără dovadă contabilă`, + href: '/dashboard/transactions', + }); + + const overdueDecisionReviews = decisions.filter( + (d) => d.selectedOption && d.reviewDueAt && new Date(d.reviewDueAt) < now, + ); + if (overdueDecisionReviews.length > 0) + alerts.push({ + id: 'decisions-reviews', + level: 'critical', + module: 'Decizii', + message: `${overdueDecisionReviews.length} revizuiri de decizii restante`, + href: '/dashboard/decisions/register', + }); + + const criticalNotifs = notifications.filter((n) => n.severity === 'CRITICAL'); + if (criticalNotifs.length > 0) + alerts.push({ + id: 'notif-critical', + level: 'critical', + module: 'Sistem', + message: `${criticalNotifs.length} notificări critice necitite`, + href: '/dashboard/settings/notifications', + }); + + const riskyGoals = goals.filter((g) => g.status === 'at_risk'); + if (riskyGoals.length > 0) + alerts.push({ + id: 'goals-risk', + level: 'warning', + module: 'Obiective', + message: `${riskyGoals.length} obiective la risc`, + href: '/dashboard/goals', + detail: riskyGoals.slice(0, 2).map((g) => g.metric).join(', '), + }); + + const urgentTasks = tasks.filter( + (t) => + t.status !== 'done' && + t.dueDate && + new Date(t.dueDate) >= now && + new Date(t.dueDate) <= in7Days, + ); + if (urgentTasks.length > 0) + alerts.push({ + id: 'tasks-urgent', + level: 'warning', + module: 'Sarcini', + message: `${urgentTasks.length} sarcini scadente în 7 zile`, + href: '/dashboard/tasks', + }); + + const partialTx = transactions.filter((t) => t.evidenceStatus === 'partial'); + if (partialTx.length > 0) + alerts.push({ + id: 'tx-partial', + level: 'warning', + module: 'Tranzacții', + message: `${partialTx.length} tranzacții documentate parțial`, + href: '/dashboard/transactions', + }); + + const pendingDecisions = decisions.filter((d) => !d.selectedOption); + if (pendingDecisions.length > 5) + alerts.push({ + id: 'decisions-pending', + level: 'info', + module: 'Decizii', + message: `${pendingDecisions.length} decizii în așteptare`, + href: '/dashboard/decisions', + }); + + return ( +
+
+

Alerte & Scadențe

+

+ Monitorizare cross-modul —{' '} + {alerts.length === 0 + ? 'Totul în regulă' + : `${alerts.length} alertă${alerts.length === 1 ? '' : 'e'} activă${alerts.length === 1 ? '' : 'e'}`} +

+
+ + {alerts.length === 0 ? ( +
+

+

Nicio alertă activă

+

+ Toate sarcinile, obiectivele, tranzacțiile și deciziile sunt în ordine. +

+
+ ) : ( +
    + {alerts.map((alert) => { + const cfg = LEVEL_CFG[alert.level]; + return ( +
  • + + {cfg.icon} + +
    +
    + + {alert.module} + + + {cfg.label} + +
    +

    {alert.message}

    + {alert.detail && ( +

    {alert.detail}

    + )} +
    + + Vezi → + +
  • + ); + })} +
+ )} +
+ ); +}