From a4ec6660eec04ad1100a9b1ec766837fd00f831e Mon Sep 17 00:00:00 2001 From: planetazuzu Date: Mon, 19 Jan 2026 20:32:53 +0100 Subject: [PATCH] Fix guide infographic rendering and add emergency mode --- .../consolidado/SECCION_01_ABCDE_OPERATIVO.md | 83 ++------- docs/consolidado/SECCION_01_RCP_ADULTO_SVB.md | 24 +-- .../consolidado/SECCION_03_ABCDE_OPERATIVO.md | 173 +----------------- docs/consolidado/SECCION_03_RCP_ADULTO_SVB.md | 13 +- package.json | 4 +- src/App.tsx | 4 + src/components/content/MarkdownViewer.tsx | 2 +- src/components/emergency/EmergencyButton.tsx | 30 +++ .../emergency/EmergencyInstructions.tsx | 143 +++++++++++++++ .../emergency/EmergencyModeOverlay.tsx | 118 ++++++++++++ .../emergency/EmergencyModeRoot.tsx | 20 ++ .../emergency/EmergencySituationSelector.tsx | 49 +++++ src/services/emergency.service.ts | 167 +++++++++++++++++ 13 files changed, 565 insertions(+), 265 deletions(-) create mode 100644 src/components/emergency/EmergencyButton.tsx create mode 100644 src/components/emergency/EmergencyInstructions.tsx create mode 100644 src/components/emergency/EmergencyModeOverlay.tsx create mode 100644 src/components/emergency/EmergencyModeRoot.tsx create mode 100644 src/components/emergency/EmergencySituationSelector.tsx create mode 100644 src/services/emergency.service.ts diff --git a/docs/consolidado/SECCION_01_ABCDE_OPERATIVO.md b/docs/consolidado/SECCION_01_ABCDE_OPERATIVO.md index 4829d258..b141bff0 100644 --- a/docs/consolidado/SECCION_01_ABCDE_OPERATIVO.md +++ b/docs/consolidado/SECCION_01_ABCDE_OPERATIVO.md @@ -58,80 +58,19 @@ Recuerda que esta guía es para formación, repaso y comprensión profunda. Cuan ### Infografía: ABCDE como Estructura Mental de Priorización -![Infografía ABCDE - Estructura Mental de Priorización]/assets/infografias/bloque-0-fundamentos/ABCDE_INTRODUCCION_ESTRUCTURA_MENTAL.svg) +![Infografía ABCDE - Estructura Mental de Priorización](/assets/infografias/bloque-0-fundamentos/abcde_introduccion_estructura_mental.svg) -**Descripción de la infografía:** + --- diff --git a/docs/consolidado/SECCION_01_RCP_ADULTO_SVB.md b/docs/consolidado/SECCION_01_RCP_ADULTO_SVB.md index 97eebbbf..17688181 100644 --- a/docs/consolidado/SECCION_01_RCP_ADULTO_SVB.md +++ b/docs/consolidado/SECCION_01_RCP_ADULTO_SVB.md @@ -38,24 +38,14 @@ Recuerda que esta guía es para formación, repaso y comprensión profunda. Cuan ### Infografía de Introducción -![Infografía de Introducción RCP Adulto SVB]/assets/infografias/bloque-4-rcp/introduccion_rcp_adulto_svb.png) +![Infografía de Introducción RCP Adulto SVB](/assets/infografias/bloque-4-rcp/introduccion_rcp_adulto_svb.png) -**Descripción de la Infografía:** - -Esta infografía conceptual comunica visualmente la importancia y el contexto de la RCP Adulto SVB: - -- **Elemento central:** Diagrama esquemático del sistema cardiovascular humano (corazón y vasos sanguíneos principales), transmitiendo que la RCP actúa sobre el sistema circulatorio. - -- **Flujo conceptual:** Flechas que conectan tres conceptos clave: - - **Parada Cardiorrespiratoria** (sistema detenido) - - **RCP Básica** (acción/intervención con iconos de compresión) - - **Mantenimiento de la Vida** (flujo continuo) - -- **Iconografía de importancia:** Badges destacando "Protocolo Crítico", "Acción Inmediata", "Formación Continua". - -- **Contexto visual:** Elementos sutiles de emergencias prehospitalarias (ambulancia, escena de emergencia) sin distraer del mensaje principal. - -**Propósito pedagógico:** Introduce visualmente al usuario en el contexto de la RCP, comunicando su importancia crítica y preparando para el aprendizaje profundo que seguirá. + --- diff --git a/docs/consolidado/SECCION_03_ABCDE_OPERATIVO.md b/docs/consolidado/SECCION_03_ABCDE_OPERATIVO.md index 7f7e430e..2422b326 100644 --- a/docs/consolidado/SECCION_03_ABCDE_OPERATIVO.md +++ b/docs/consolidado/SECCION_03_ABCDE_OPERATIVO.md @@ -10,173 +10,12 @@ ![Infografía: Algoritmo ABCDE Completo](/assets/infografias/bloque-0-fundamentos/abcde_introduccion_estructura_mental.svg) -**Tipo de visual:** Infografía conceptual del algoritmo completo - -**Descripción de la infografía:** - -La infografía presenta el algoritmo ABCDE como un flujo mental estructurado y vertical, organizado visualmente para transmitir la estructura de priorización vital más que una lista de verificación. El diseño comunica que el ABCDE es un proceso mental de evaluación y control sistemático, donde cada nivel debe estar controlado antes de avanzar al siguiente. - -**Estructura visual principal:** - -1. **Estructura vertical clara:** - - Representación del ABCDE como una columna o estructura vertical - - Cinco niveles claramente diferenciados, uno debajo del otro - - Flujo descendente visual que indica orden de evaluación: de arriba hacia abajo - - Cada nivel ocupa un espacio visual definido, con separación clara entre niveles - -2. **Nivel A — Vía Aérea y Respiración (Prioridad Vital Máxima):** - - Ubicado en la parte superior de la estructura - - Tamaño visual mayor o más prominente - - Color rojo intenso o naranja que indica máxima urgencia - - Iconografía de aire/respiración: pulmones estilizados, flujo de aire, vía aérea abierta - - Etiqueta conceptual: "Vía Aérea y Respiración" - - Indicador visual: "Prioridad Vital Máxima" - - Flujo visual: "Evaluar → Identificar amenaza → Controlar → Continuar" - - Bloque visual que indica: "No avanzar si no está controlado" - -3. **Nivel B — Respiración Efectiva:** - - Ubicado inmediatamente debajo del nivel A - - Tamaño visual medio-grande - - Color naranja o rojo suave que indica urgencia alta - - Iconografía de respiración efectiva: pulmones expandiéndose, intercambio de gases - - Etiqueta conceptual: "Respiración Efectiva" - - Indicador visual: "Crítico si falla" - - Flujo visual: "Evaluar → Identificar amenaza → Controlar → Continuar" - - Bloque visual que indica: "No avanzar si no está controlado" - - Conexión visual clara con el nivel A (dependencia) - -4. **Nivel C — Circulación:** - - Ubicado debajo del nivel B - - Tamaño visual medio - - Color amarillo o naranja suave que indica importancia alta - - Iconografía de circulación: corazón estilizado, flujo sanguíneo, vasos - - Etiqueta conceptual: "Circulación" - - Indicador visual: "Vital para órganos" - - Flujo visual: "Evaluar → Identificar amenaza → Controlar → Continuar" - - Bloque visual que indica: "No avanzar si no está controlado" - - Conexión visual clara con los niveles A y B (dependencia) - -5. **Nivel D — Función Neurológica:** - - Ubicado debajo del nivel C - - Tamaño visual medio-pequeño - - Color amarillo suave que indica importancia moderada - - Iconografía de función neurológica: cerebro estilizado, consciencia, respuesta - - Etiqueta conceptual: "Función Neurológica" - - Indicador visual: "Control del organismo" - - Flujo visual: "Evaluar → Identificar amenaza → Controlar → Continuar" - - Bloque visual que indica: "No avanzar si no está controlado" - - Conexión visual clara con los niveles anteriores (dependencia) - -6. **Nivel E — Exposición y Evaluación Continua:** - - Ubicado en la parte inferior de la estructura - - Tamaño visual menor - - Color verde o azul suave que indica importancia pero menor urgencia inmediata - - Iconografía de exposición: cuerpo completo, entorno, temperatura, evaluación global - - Etiqueta conceptual: "Exposición y Evaluación Continua" - - Indicador visual: "Factores que pueden empeorar" - - Flujo visual: "Evaluar → Identificar amenaza → Controlar → Reevaluar" - - Bloque visual que indica: "Reevaluación constante" - -7. **Flujo de evaluación y control:** - - Flechas descendentes que conectan cada nivel con el siguiente - - Flechas que indican el flujo lógico: A → B → C → D → E - - Indicadores visuales en cada nivel que muestran el ciclo: "Evaluar → Identificar → Controlar → Continuar" - - Flechas de retorno o ciclo que indican reevaluación constante - - Visual que muestra que si un nivel no está controlado, no se avanza al siguiente - -8. **Indicadores de "No avanzar si no está controlado":** - - Bloque visual distintivo en cada nivel (excepto E) - - Iconografía de "pausa" o "control" antes de avanzar - - Color que indica que el avance depende del control del nivel actual - - Texto conceptual: "Controlar antes de continuar" - - Visual que muestra cómo el flujo se detiene si un nivel no está controlado - -9. **Indicadores de reevaluación constante:** - - Flechas circulares o de retorno que conectan E con A - - Visual que muestra que después de evaluar E, se vuelve a evaluar A - - Iconografía de ciclo continuo - - Texto conceptual: "Reevaluación constante" - - Color que indica continuidad del proceso - -10. **Gradiente de gravedad decreciente:** - - Color que va de rojo intenso (arriba) a verde/azul suave (abajo) - - Tamaño visual que disminuye de arriba hacia abajo - - Intensidad visual que refuerza la jerarquía - - Mensaje visual claro: "Lo más crítico arriba, lo menos crítico abajo" - -**Jerarquía visual:** - -- **Elemento superior (más prominente):** Nivel A (Vía Aérea y Respiración) - - Tamaño visual mayor - - Color más saturado o destacado - - Posición superior en la composición - - Indicadores visuales más intensos - -- **Elementos medios (importantes pero secundarios):** Niveles B, C, D - - Tamaño visual medio, disminuyendo gradualmente - - Colores complementarios pero menos intensos - - Posición intermedia en la composición - - Indicadores visuales moderados - -- **Elemento inferior (importante pero menos crítico en ese momento):** Nivel E - - Tamaño visual menor - - Color más suave - - Posición inferior en la composición - - Indicadores visuales sutiles pero presentes - -**Iconografía utilizada:** - -- **Aire/Respiración:** Símbolo estilizado de pulmones, flujo de aire, vía aérea abierta -- **Circulación:** Símbolo estilizado de corazón, flujo sanguíneo, vasos -- **Cerebro:** Símbolo estilizado de cerebro o función neurológica -- **Cuerpo/Entorno:** Símbolo de cuerpo completo, entorno, temperatura -- **Control:** Icono de verificación, control, o pausa antes de avanzar -- **Ciclo:** Símbolo de ciclo, reevaluación, continuidad - -**Indicadores visuales de prioridad:** - -- **Prioridad máxima:** Nivel A (tamaño, color, posición) -- **Prioridad alta:** Niveles B y C (tamaño medio, color moderado) -- **Prioridad media:** Nivel D (tamaño menor, color suave) -- **Prioridad baja (en ese momento):** Nivel E (tamaño menor, color más suave) - -**Indicadores visuales de flujo lógico:** - -- Numeración visual sutil (no numérica, sino posicional): elementos ordenados de arriba hacia abajo -- Flechas que guían la lectura visual en el orden lógico: A → B → C → D → E -- Agrupación visual que muestra qué elementos pertenecen a la misma estructura -- Separación visual clara entre niveles - -**Indicadores visuales de control y avance:** - -- Bloque visual distintivo en cada nivel que indica "Controlar antes de continuar" -- Visual que muestra cómo el flujo se detiene si un nivel no está controlado -- Flechas de avance que solo aparecen cuando el nivel está controlado -- Iconografía de "pausa" o "verificación" antes de avanzar - -**Lo que NO muestra esta infografía:** - -- Pasos numerados (1, 2, 3...) -- Técnicas específicas de evaluación o intervención -- Maniobras o procedimientos -- Valores numéricos o escalas -- Tiempos exactos o duraciones -- Secuencias clínicas operativas -- Checklist o lista de verificación -- Desarrollo técnico de cada letra (A=..., B=...) -- Instrucciones de ejecución - -**Objetivo pedagógico del visual:** - -Esta infografía permite al TES "ver" el algoritmo ABCDE como una estructura mental vertical y ordenada, no como una lista de verificación. Al visualizar el flujo, la jerarquía, y la necesidad de controlar cada nivel antes de avanzar, el TES puede internalizar la lógica del algoritmo, lo que facilita: - -- La comprensión del propósito de cada nivel y su posición en la jerarquía -- El reconocimiento del orden lógico cuando se consulta el protocolo operativo -- La identificación de errores (si se avanza sin controlar, si se salta un nivel, si se invierte el orden) -- La preparación mental para seguir el flujo durante la ejecución real -- La comprensión de por qué no se puede "saltar letras" o avanzar sin controlar - -La infografía actúa como un "mapa mental" del algoritmo, preparando al TES para navegar el protocolo operativo con mayor comprensión y confianza, especialmente en los momentos críticos donde mantener el orden puede marcar la diferencia. + --- diff --git a/docs/consolidado/SECCION_03_RCP_ADULTO_SVB.md b/docs/consolidado/SECCION_03_RCP_ADULTO_SVB.md index fcbcddb0..676d929b 100644 --- a/docs/consolidado/SECCION_03_RCP_ADULTO_SVB.md +++ b/docs/consolidado/SECCION_03_RCP_ADULTO_SVB.md @@ -8,13 +8,14 @@ ### Visión Global del Algoritmo de RCP Adulto SVB -**Tipo de visual:** Infografía conceptual del algoritmo completo +![Infografía: Algoritmo RCP Adulto SVB](/assets/infografias/bloque-4-rcp/algoritmo_rcp_comentado.svg) -**Descripción de la infografía:** - -La infografía presenta el algoritmo de RCP Adulto SVB como un flujo continuo y lógico, organizado visualmente para transmitir la estructura mental del protocolo más que una lista de acciones. - -**Estructura visual principal:** + 1. **Punto de inicio — Reconocimiento:** - Representado visualmente como un nodo inicial destacado diff --git a/package.json b/package.json index f5b392b2..9cc61558 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", "sonner": "^1.7.4", - "tailwind-merge": "^2.6.0" + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7" }, "overrides": { "react": "^19.2.3", @@ -80,7 +81,6 @@ "remark": "^15.0.1", "remark-rehype": "^11.1.2", "tailwindcss": "^3.4.17", - "tailwindcss-animate": "^1.0.7", "tsx": "^4.21.0", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", diff --git a/src/App.tsx b/src/App.tsx index 4fcc2fb1..b5898af5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import ErrorBoundary from "@/components/ErrorBoundary"; import PageLoader from "@/components/ui/PageLoader"; import { featureFlags } from "@/config/featureFlags"; import ClinicalSuggestions from "@/components/ClinicalSuggestions"; +import EmergencyModeRoot from "@/components/emergency/EmergencyModeRoot"; // Página principal - cargar inmediatamente (crítica) import Home from "./pages/Index"; @@ -244,6 +245,9 @@ const App = () => { isOpen={isMenuOpen} onClose={() => setIsMenuOpen(false)} /> + + {/* Módulo complementario y desacoplado: Modo Emergencia */} + diff --git a/src/components/content/MarkdownViewer.tsx b/src/components/content/MarkdownViewer.tsx index 05ce6d59..e6ea563a 100644 --- a/src/components/content/MarkdownViewer.tsx +++ b/src/components/content/MarkdownViewer.tsx @@ -115,7 +115,7 @@ const MarkdownViewer = ({ // Renderizar Markdown return ( -
+
void; + isActive?: boolean; +} + +/** + * Botón flotante de emergencia. + * UX: siempre visible, alto contraste y tamaño amplio para uso con una mano. + */ +const EmergencyButton = ({ onClick, isActive }: EmergencyButtonProps) => { + return ( + + ); +}; + +export default EmergencyButton; diff --git a/src/components/emergency/EmergencyInstructions.tsx b/src/components/emergency/EmergencyInstructions.tsx new file mode 100644 index 00000000..7db99fd3 --- /dev/null +++ b/src/components/emergency/EmergencyInstructions.tsx @@ -0,0 +1,143 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { EmergencySituation } from '@/services/emergency.service'; +import { notifyCoordinator, updateEmergencyEvent } from '@/services/emergency.service'; + +interface EmergencyInstructionsProps { + eventId: string; + situation: EmergencySituation; + onClose: () => void; + onRestart: () => void; + isOnline: boolean; +} + +const situationLabels: Record = { + medica: 'Emergencia médica', + accidente: 'Accidente', + tecnico: 'Incidente técnico', + otro: 'Otro', +}; + +const situationSteps: Record = { + medica: [ + 'Respira hondo y habla en voz calmada.', + 'Asegura la zona si es posible.', + 'Comprueba conciencia y respiración.', + 'Sigue las instrucciones en pantalla.', + ], + accidente: [ + 'Respira hondo y mantén la calma.', + 'Asegura la zona y evita nuevos riesgos.', + 'Cuenta rápidamente cuántas personas necesitan ayuda.', + 'Sigue las instrucciones en pantalla.', + ], + tecnico: [ + 'Respira hondo y reduce la velocidad.', + 'Detén el uso del equipo si es seguro hacerlo.', + 'Aísla la zona para evitar más daños.', + 'Sigue las instrucciones en pantalla.', + ], + otro: [ + 'Respira hondo y observa el entorno.', + 'Asegura la zona si es posible.', + 'Identifica la prioridad más urgente.', + 'Sigue las instrucciones en pantalla.', + ], +}; + +const EmergencyInstructions = ({ + eventId, + situation, + onClose, + onRestart, + isOnline, +}: EmergencyInstructionsProps) => { + const [notificationStatus, setNotificationStatus] = useState<'idle' | 'sent' | 'queued'>('idle'); + + useEffect(() => { + updateEmergencyEvent(eventId, { status: 'instructions' }); + }, [eventId]); + + useEffect(() => { + let isMounted = true; + const sendNotification = async () => { + const result = await notifyCoordinator(eventId); + if (isMounted) { + setNotificationStatus(result); + } + }; + sendNotification(); + return () => { + isMounted = false; + }; + }, [eventId]); + + const steps = useMemo(() => situationSteps[situation], [situation]); + + return ( +
+
+
No estás solo. Sigue estos pasos.
+

{situationLabels[situation]}

+

Una acción clara por paso. Vamos contigo.

+
+ +
+ {steps.map((step) => ( +
+ + {step} +
+ ))} +
+ +
+ {notificationStatus === 'sent' && 'Coordinación notificada.'} + {notificationStatus === 'queued' && 'Aviso guardado para sincronizar.'} + {notificationStatus === 'idle' && 'Preparando aviso a coordinación...'} +
+ + {isOnline ? ( + + ) : ( +
+ Sin conexión. Seguimos en modo offline y guardamos el evento para sincronizar después. +
+ )} + +
+ + +
+
+ ); +}; + +export default EmergencyInstructions; diff --git a/src/components/emergency/EmergencyModeOverlay.tsx b/src/components/emergency/EmergencyModeOverlay.tsx new file mode 100644 index 00000000..7dece435 --- /dev/null +++ b/src/components/emergency/EmergencyModeOverlay.tsx @@ -0,0 +1,118 @@ +import { useEffect, useMemo, useState } from 'react'; +import EmergencySituationSelector from './EmergencySituationSelector'; +import EmergencyInstructions from './EmergencyInstructions'; +import type { EmergencySituation } from '@/services/emergency.service'; +import { + createEmergencyEvent, + getLocationSnapshot, + updateEmergencyEvent, +} from '@/services/emergency.service'; + +interface EmergencyModeOverlayProps { + isOpen: boolean; + onClose: () => void; +} + +/** + * Overlay fullscreen autocontenido. + * UX: bloquea navegación y centra la atención en un flujo lineal. + */ +const EmergencyModeOverlay = ({ isOpen, onClose }: EmergencyModeOverlayProps) => { + const [stage, setStage] = useState<'select' | 'instructions'>('select'); + const [eventId, setEventId] = useState(null); + const [situation, setSituation] = useState(null); + const [isOnline, setIsOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true); + + useEffect(() => { + if (!isOpen) return; + const event = createEmergencyEvent(); + setEventId(event.id); + setStage('select'); + setSituation(null); + + getLocationSnapshot().then((location) => { + if (location) { + updateEmergencyEvent(event.id, { location }); + } + }); + }, [isOpen]); + + useEffect(() => { + if (!isOpen) return; + const handleOnline = () => setIsOnline(true); + const handleOffline = () => setIsOnline(false); + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + return () => { + window.removeEventListener('online', handleOnline); + window.removeEventListener('offline', handleOffline); + }; + }, [isOpen]); + + useEffect(() => { + if (!isOpen) return; + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = originalOverflow; + }; + }, [isOpen]); + + const title = useMemo(() => { + if (stage === 'select') return 'Estás en una situación crítica. Estamos contigo.'; + return 'No estás solo. Sigue estos pasos.'; + }, [stage]); + + if (!isOpen) return null; + + return ( +
+
+
+

Modo Emergencia

+

{title}

+
+ +
+ {stage === 'select' && ( + { + if (eventId) { + updateEmergencyEvent(eventId, { + situation: value, + status: 'situation_selected', + }); + } + setSituation(value); + setStage('instructions'); + }} + /> + )} + + {stage === 'instructions' && eventId && situation && ( + { + setStage('select'); + setSituation(null); + }} + onClose={onClose} + /> + )} +
+ + +
+
+ ); +}; + +export default EmergencyModeOverlay; diff --git a/src/components/emergency/EmergencyModeRoot.tsx b/src/components/emergency/EmergencyModeRoot.tsx new file mode 100644 index 00000000..9ef90464 --- /dev/null +++ b/src/components/emergency/EmergencyModeRoot.tsx @@ -0,0 +1,20 @@ +import { useState } from 'react'; +import EmergencyButton from './EmergencyButton'; +import EmergencyModeOverlay from './EmergencyModeOverlay'; + +/** + * Punto de integración aislado del módulo de emergencia. + * Mantiene estado local y no altera flujos existentes. + */ +const EmergencyModeRoot = () => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + setIsOpen(true)} isActive={isOpen} /> + setIsOpen(false)} /> + + ); +}; + +export default EmergencyModeRoot; diff --git a/src/components/emergency/EmergencySituationSelector.tsx b/src/components/emergency/EmergencySituationSelector.tsx new file mode 100644 index 00000000..e530c002 --- /dev/null +++ b/src/components/emergency/EmergencySituationSelector.tsx @@ -0,0 +1,49 @@ +import type { EmergencySituation } from '@/services/emergency.service'; + +interface EmergencySituationSelectorProps { + onSelect: (situation: EmergencySituation) => void; +} + +const EmergencySituationSelector = ({ onSelect }: EmergencySituationSelectorProps) => { + return ( +
+
+

¿Qué está pasando ahora mismo?

+

Selecciona la opción que mejor encaje.

+
+ +
+ + + + +
+
+ ); +}; + +export default EmergencySituationSelector; diff --git a/src/services/emergency.service.ts b/src/services/emergency.service.ts new file mode 100644 index 00000000..d7a20460 --- /dev/null +++ b/src/services/emergency.service.ts @@ -0,0 +1,167 @@ +export type EmergencySituation = 'medica' | 'accidente' | 'tecnico' | 'otro'; + +export interface EmergencyLocation { + lat: number; + lng: number; + accuracy?: number; +} + +export interface EmergencyEvent { + id: string; + createdAt: string; + updatedAt: string; + situation?: EmergencySituation; + status: 'active' | 'situation_selected' | 'instructions' | 'completed' | 'pending_sync'; + user?: string | null; + location?: EmergencyLocation | null; + connectivity: 'online' | 'offline'; +} + +const EVENTS_KEY = 'emergency_events_v1'; +const PENDING_NOTIFICATIONS_KEY = 'emergency_pending_notifications_v1'; + +const getStoredEvents = (): EmergencyEvent[] => { + if (typeof window === 'undefined') return []; + try { + const raw = window.localStorage.getItem(EVENTS_KEY); + if (!raw) return []; + return JSON.parse(raw) as EmergencyEvent[]; + } catch { + return []; + } +}; + +const saveEvents = (events: EmergencyEvent[]) => { + if (typeof window === 'undefined') return; + try { + window.localStorage.setItem(EVENTS_KEY, JSON.stringify(events)); + } catch { + // Evitar romper el flujo si el storage falla + } +}; + +const resolveCurrentUser = (): string | null => { + if (typeof window === 'undefined') return null; + const candidates = ['user', 'currentUser', 'auth_user', 'username']; + for (const key of candidates) { + const raw = window.localStorage.getItem(key); + if (!raw) continue; + try { + const parsed = JSON.parse(raw); + if (typeof parsed === 'string') return parsed; + if (parsed && typeof parsed.name === 'string') return parsed.name; + if (parsed && typeof parsed.username === 'string') return parsed.username; + } catch { + if (raw) return raw; + } + } + return null; +}; + +export const createEmergencyEvent = (): EmergencyEvent => { + const now = new Date().toISOString(); + const event: EmergencyEvent = { + id: `emergency_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + createdAt: now, + updatedAt: now, + status: navigator.onLine ? 'active' : 'pending_sync', + user: resolveCurrentUser(), + connectivity: navigator.onLine ? 'online' : 'offline', + }; + + const events = getStoredEvents(); + events.unshift(event); + saveEvents(events); + return event; +}; + +export const updateEmergencyEvent = (id: string, updates: Partial) => { + const events = getStoredEvents(); + const next = events.map((event) => + event.id === id + ? { ...event, ...updates, updatedAt: new Date().toISOString() } + : event + ); + saveEvents(next); +}; + +export const getEmergencyEvent = (id: string): EmergencyEvent | undefined => { + return getStoredEvents().find((event) => event.id === id); +}; + +export const getLocationSnapshot = (): Promise => { + if (typeof window === 'undefined' || !navigator.geolocation) { + return Promise.resolve(null); + } + return new Promise((resolve) => { + navigator.geolocation.getCurrentPosition( + (position) => { + resolve({ + lat: position.coords.latitude, + lng: position.coords.longitude, + accuracy: position.coords.accuracy, + }); + }, + () => resolve(null), + { enableHighAccuracy: true, timeout: 4000 } + ); + }); +}; + +const getPendingNotifications = (): string[] => { + if (typeof window === 'undefined') return []; + try { + const raw = window.localStorage.getItem(PENDING_NOTIFICATIONS_KEY); + if (!raw) return []; + return JSON.parse(raw) as string[]; + } catch { + return []; + } +}; + +const savePendingNotifications = (ids: string[]) => { + if (typeof window === 'undefined') return; + try { + window.localStorage.setItem(PENDING_NOTIFICATIONS_KEY, JSON.stringify(ids)); + } catch { + // Silencioso + } +}; + +export const queueCoordinatorNotification = (eventId: string) => { + const pending = getPendingNotifications(); + if (!pending.includes(eventId)) { + pending.push(eventId); + savePendingNotifications(pending); + } +}; + +export const notifyCoordinator = async (eventId: string): Promise<'sent' | 'queued'> => { + const event = getEmergencyEvent(eventId); + if (!event || typeof window === 'undefined') { + return 'queued'; + } + + // Endpoint configurable sin tocar configuración global de la app + const notifyUrl = window.localStorage.getItem('emergency_notify_url'); + + if (!navigator.onLine || !notifyUrl) { + queueCoordinatorNotification(eventId); + return 'queued'; + } + + try { + const response = await fetch(notifyUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(event), + }); + if (!response.ok) { + throw new Error('notification_failed'); + } + return 'sent'; + } catch { + queueCoordinatorNotification(eventId); + return 'queued'; + } +};