From 7f6ada6d39d9d62af64f933bd65e7b5023f8816a Mon Sep 17 00:00:00 2001 From: Esteban Date: Thu, 12 Feb 2026 15:26:19 -0600 Subject: [PATCH] feat: alerts page and alert components (AlertCard, AlertsSection, AlertsTabs) --- src/app/(dashboard)/alerts/page.tsx | 61 +++++++++++++++ src/components/alerts/AlertCard.tsx | 100 ++++++++++++++++++++++++ src/components/alerts/AlertsSection.tsx | 70 +++++++++++++++++ src/components/alerts/AlertsTabs.tsx | 39 +++++++++ src/components/dashboard/AlertsFeed.tsx | 5 +- 5 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 src/app/(dashboard)/alerts/page.tsx create mode 100644 src/components/alerts/AlertCard.tsx create mode 100644 src/components/alerts/AlertsSection.tsx create mode 100644 src/components/alerts/AlertsTabs.tsx diff --git a/src/app/(dashboard)/alerts/page.tsx b/src/app/(dashboard)/alerts/page.tsx new file mode 100644 index 0000000..f141618 --- /dev/null +++ b/src/app/(dashboard)/alerts/page.tsx @@ -0,0 +1,61 @@ +'use client' + +import { useMemo } from 'react' +import AlertsSection from '@/components/alerts/AlertsSection' +import type { AlertCardData } from '@/components/alerts/AlertCard' +import { trpc } from '@/lib/trpc-client' + +export default function AlertsPage() { + const utils = trpc.useUtils() + + const alertsQuery = trpc.alertas.list.useQuery( + { page: 1, limit: 100 }, + { refetchOnWindowFocus: false } + ) + + const acknowledgeMutation = trpc.alertas.reconocer.useMutation({ + onSuccess: () => { + utils.alertas.list.invalidate() + utils.alertas.conteoActivas.invalidate() + utils.clientes.dashboardStats.invalidate() + }, + }) + const resolveMutation = trpc.alertas.resolver.useMutation({ + onSuccess: () => { + utils.alertas.list.invalidate() + utils.alertas.conteoActivas.invalidate() + utils.clientes.dashboardStats.invalidate() + }, + }) + + const alerts: AlertCardData[] = useMemo(() => { + const list = alertsQuery.data?.alertas ?? [] + return list.map((a) => ({ + id: a.id, + title: a.titulo, + device: a.dispositivo?.nombre ?? '—', + description: a.mensaje, + severity: a.severidad, + timestamp: a.createdAt instanceof Date ? a.createdAt : new Date(a.createdAt), + status: a.estado, + })) + }, [alertsQuery.data]) + + const handleAcknowledge = (id: string) => { + acknowledgeMutation.mutate({ id }) + } + const handleResolve = (id: string) => { + resolveMutation.mutate({ id }) + } + + return ( +
+ +
+ ) +} diff --git a/src/components/alerts/AlertCard.tsx b/src/components/alerts/AlertCard.tsx new file mode 100644 index 0000000..8d95a18 --- /dev/null +++ b/src/components/alerts/AlertCard.tsx @@ -0,0 +1,100 @@ +'use client' + +import { cn, formatRelativeTime } from '@/lib/utils' + +export type AlertSeverity = 'CRITICAL' | 'WARNING' | 'INFO' + +export type AlertStatus = 'ACTIVA' | 'RECONOCIDA' | 'RESUELTA' + +export interface AlertCardData { + id: string + title: string + device: string + description: string + severity: AlertSeverity + timestamp: Date | string + status: AlertStatus +} + +interface AlertCardProps { + alert: AlertCardData + onAcknowledge?: (id: string) => void + onResolve?: (id: string) => void +} + +const severityStyles = { + CRITICAL: { + bar: 'bg-red-500', + badge: 'bg-red-500/20 text-red-400 border-red-500/40', + label: 'CRÍTICO', + }, + WARNING: { + bar: 'bg-amber-500', + badge: 'bg-amber-500/20 text-amber-400 border-amber-500/40', + label: 'ADVERTENCIA', + }, + INFO: { + bar: 'bg-blue-500', + badge: 'bg-blue-500/20 text-blue-400 border-blue-500/40', + label: 'INFO', + }, +} + +export default function AlertCard({ alert, onAcknowledge, onResolve }: AlertCardProps) { + const style = severityStyles[alert.severity] + const ts = typeof alert.timestamp === 'string' ? new Date(alert.timestamp) : alert.timestamp + + return ( +
+
+
+

+ {alert.title} +

+

{alert.device}

+

{alert.description}

+

{formatRelativeTime(ts)}

+
+ +
+ + {style.label} + + {alert.status === 'ACTIVA' && ( +
+ + +
+ )} + {alert.status !== 'ACTIVA' && ( + + {alert.status === 'RECONOCIDA' ? 'Leída' : 'Resuelta'} + + )} +
+
+ ) +} diff --git a/src/components/alerts/AlertsSection.tsx b/src/components/alerts/AlertsSection.tsx new file mode 100644 index 0000000..0bcf4ed --- /dev/null +++ b/src/components/alerts/AlertsSection.tsx @@ -0,0 +1,70 @@ +'use client' + +import type { AlertCardData, AlertSeverity } from './AlertCard' +import AlertCard from './AlertCard' +import AlertsTabs, { type AlertsTab } from './AlertsTabs' +import { useMemo, useState } from 'react' +import { AlertTriangle } from 'lucide-react' + +interface AlertsSectionProps { + alerts: AlertCardData[] + isLoading?: boolean + onAcknowledge?: (id: string) => void + onResolve?: (id: string) => void +} + +function filterByTab(alerts: AlertCardData[], tab: AlertsTab): AlertCardData[] { + if (tab === 'all') return alerts + return alerts.filter((a) => a.severity === tab) +} + +export default function AlertsSection({ + alerts, + isLoading, + onAcknowledge, + onResolve, +}: AlertsSectionProps) { + const [activeTab, setActiveTab] = useState('all') + + const filtered = useMemo( + () => filterByTab(alerts, activeTab), + [alerts, activeTab] + ) + + return ( +
+
+

Alertas del Sistema

+

Notificaciones y advertencias

+
+ + + + {isLoading ? ( +
+ Cargando alertas... +
+ ) : filtered.length === 0 ? ( +
+ +

+ {activeTab === 'all' + ? 'No hay alertas' + : `No hay alertas de tipo ${activeTab === 'CRITICAL' ? 'críticas' : activeTab === 'WARNING' ? 'advertencias' : 'informativas'}`} +

+
+ ) : ( +
+ {filtered.map((alert) => ( + + ))} +
+ )} +
+ ) +} diff --git a/src/components/alerts/AlertsTabs.tsx b/src/components/alerts/AlertsTabs.tsx new file mode 100644 index 0000000..1b8eb96 --- /dev/null +++ b/src/components/alerts/AlertsTabs.tsx @@ -0,0 +1,39 @@ +'use client' + +import { cn } from '@/lib/utils' + +export type AlertsTab = 'all' | 'CRITICAL' | 'WARNING' | 'INFO' + +const TABS: { id: AlertsTab; label: string }[] = [ + { id: 'all', label: 'Todas' }, + { id: 'CRITICAL', label: 'Críticas' }, + { id: 'WARNING', label: 'Advertencias' }, + { id: 'INFO', label: 'Informativas' }, +] + +interface AlertsTabsProps { + active: AlertsTab + onChange: (tab: AlertsTab) => void +} + +export default function AlertsTabs({ active, onChange }: AlertsTabsProps) { + return ( +
+ {TABS.map((tab) => ( + + ))} +
+ ) +} diff --git a/src/components/dashboard/AlertsFeed.tsx b/src/components/dashboard/AlertsFeed.tsx index 4dea834..eb8167b 100644 --- a/src/components/dashboard/AlertsFeed.tsx +++ b/src/components/dashboard/AlertsFeed.tsx @@ -1,5 +1,6 @@ 'use client' +import Link from 'next/link' import { AlertTriangle, CheckCircle, Info, Clock } from 'lucide-react' import { cn, formatRelativeTime } from '@/lib/utils' @@ -42,9 +43,9 @@ export default function AlertsFeed({

Alertas Recientes

- + Ver todas - +