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({