Fix guide infographic rendering and add emergency mode
This commit is contained in:
parent
2353297bb7
commit
a4ec6660ee
|
|
@ -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)
|
||||

|
||||
|
||||
**Descripción de la infografía:**
|
||||
<!--
|
||||
Descripción interna (no renderizar en la UI):
|
||||
Esta infografía comunica visualmente el ABCDE como estructura mental de priorización, no como checklist.
|
||||
|
||||
Esta infografía comunica visualmente el ABCDE como estructura mental de priorización, no como checklist. Muestra:
|
||||
|
||||
#### Estructura vertical ordenada:
|
||||
|
||||
- Representación del ABCDE como una estructura vertical clara, con cada letra (A, B, C, D, E) en un nivel jerárquico
|
||||
- Flujo descendente visual que indica orden de prioridad: A en la parte superior (más crítico), E en la parte inferior (menos crítico en ese momento)
|
||||
- Tamaño visual que refuerza la jerarquía: A es más prominente visualmente, disminuyendo hacia E
|
||||
- Color que indica gravedad decreciente: rojo/naranja para A (más crítico), amarillo para B y C (crítico), verde/azul para D y E (importante pero menos crítico en ese momento)
|
||||
|
||||
#### Indicadores visuales de priorización vital:
|
||||
|
||||
- Texto conceptual: "Lo que mata antes va primero"
|
||||
- Iconografía que comunica urgencia: símbolos de alerta más intensos en A, disminuyendo hacia E
|
||||
- Flechas descendentes que indican el flujo lógico del pensamiento: A → B → C → D → E
|
||||
- Visual que muestra que cada nivel debe evaluarse antes de pasar al siguiente
|
||||
|
||||
#### Comparación conceptual:
|
||||
|
||||
**Lado izquierdo — Evaluación desordenada:**
|
||||
- Representación visual de pensamiento caótico o aleatorio
|
||||
- Múltiples elementos compitiendo por atención sin orden claro
|
||||
- Iconografía de confusión o desorganización
|
||||
- Etiqueta: "Sin estructura → Errores de prioridad"
|
||||
|
||||
**Lado derecho — Evaluación estructurada (ABCDE):**
|
||||
- Representación visual del ABCDE como estructura ordenada
|
||||
- Flujo claro y sistemático
|
||||
- Iconografía de organización y claridad
|
||||
- Etiqueta: "Con estructura → Priorización correcta"
|
||||
|
||||
#### Iconografía conceptual (sin detalles técnicos):
|
||||
|
||||
- **A — Aire/Respiración:** Icono estilizado de pulmones o flujo de aire (no anatómico detallado)
|
||||
- **B — Circulación:** Icono estilizado de corazón o flujo sanguíneo (no anatómico detallado)
|
||||
- **C — Cerebro/Consciencia:** Icono estilizado de cerebro o función neurológica (no anatómico detallado)
|
||||
- **D — Exposición/Entorno:** Icono de exposición o factores ambientales
|
||||
- **E — Evaluación continua:** Icono de ciclo o evaluación continua
|
||||
|
||||
#### Badge distintivo:
|
||||
|
||||
- Badge claro en la parte superior: "Modo Formación / Refuerzo"
|
||||
- Badge contrastante: "Modo Operativo"
|
||||
- Separación visual clara entre ambos modos
|
||||
- Texto explicativo breve: "Esta guía explica la estructura mental. Para la acción inmediata, consulta el Protocolo Operativo ABCDE."
|
||||
|
||||
#### Contexto visual:
|
||||
|
||||
- Elementos sutiles que sitúan el ABCDE en el contexto de emergencias prehospitalarias
|
||||
- Iconografía de escena de emergencia (sin detalles técnicos)
|
||||
- Representación conceptual de aplicación universal (no específica de un tipo de emergencia)
|
||||
|
||||
#### Estilo visual:
|
||||
|
||||
- Colores suaves y profesionales (rojos/naranjas para urgencia, azules/verdes para información, grises para texto secundario)
|
||||
- Iconografía clara y reconocible, sin detalles anatómicos complejos
|
||||
- Tipografía legible y jerárquica
|
||||
- Diseño limpio que facilita la comprensión rápida
|
||||
- Elementos visuales que sugieren estructura y orden sin ser rígidos
|
||||
|
||||
#### Lo que NO muestra esta infografía:
|
||||
|
||||
- ❌ No muestra técnicas específicas de evaluación
|
||||
- ❌ No muestra maniobras o procedimientos
|
||||
- ❌ No muestra valores numéricos o escalas
|
||||
- ❌ No muestra pasos operativos numerados
|
||||
- ❌ No muestra checklist o lista de verificación
|
||||
- ❌ No muestra detalles anatómicos complejos
|
||||
- ❌ No desarrolla cada letra del ABCDE (eso viene en secciones siguientes)
|
||||
|
||||
**Propósito pedagógico:** Esta infografía introduce visualmente al usuario en el concepto del ABCDE como estructura mental de priorización, comunicando su universalidad, su propósito de ordenar el pensamiento, y su diferencia fundamental con una evaluación desordenada. Transmite la idea de que el ABCDE es una herramienta mental que organiza el pensamiento bajo estrés, preparando mentalmente para el aprendizaje profundo que seguirá en las siguientes secciones.
|
||||
Contenido visual a alto nivel:
|
||||
- Estructura vertical ABCDE con jerarquía y gradiente de gravedad.
|
||||
- Flujo descendente A → B → C → D → E con iconografía clara.
|
||||
- Comparativa: evaluación desordenada vs estructurada.
|
||||
- Badge "Modo Formación / Refuerzo" vs "Modo Operativo".
|
||||
- Sin pasos operativos ni detalles técnicos.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||

|
||||
|
||||
**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á.
|
||||
<!--
|
||||
Descripción interna (no renderizar en la UI):
|
||||
Infografía conceptual de introducción a RCP Adulto SVB.
|
||||
Elementos: sistema cardiovascular, flujo conceptual PCR → RCP → vida.
|
||||
Badges de protocolo crítico y contexto prehospitalario.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -10,173 +10,12 @@
|
|||
|
||||

|
||||
|
||||
**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.
|
||||
<!--
|
||||
Descripción interna (no renderizar en la UI):
|
||||
Infografía conceptual del algoritmo completo ABCDE como flujo mental estructurado.
|
||||
Incluye jerarquía A→E, gradiente de gravedad, iconografía de control y reevaluación.
|
||||
No muestra pasos operativos ni detalles técnicos.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@
|
|||
|
||||
### Visión Global del Algoritmo de RCP Adulto SVB
|
||||
|
||||
**Tipo de visual:** Infografía conceptual del algoritmo completo
|
||||

|
||||
|
||||
**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:**
|
||||
<!--
|
||||
Descripción interna (no renderizar en la UI):
|
||||
Infografía conceptual del algoritmo completo.
|
||||
Flujo continuo con nodo de decisión y bloque central de circulación,
|
||||
integración de desfibrilación y flechas suaves de continuidad.
|
||||
-->
|
||||
|
||||
1. **Punto de inicio — Reconocimiento:**
|
||||
- Representado visualmente como un nodo inicial destacado
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
<EmergencyModeRoot />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</BrowserRouter>
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ const MarkdownViewer = ({
|
|||
|
||||
// Renderizar Markdown
|
||||
return (
|
||||
<div className={`prose prose-slate dark:prose-invert max-w-none md:max-w-prose lg:max-w-[700px] ${className}`}>
|
||||
<div className={`prose prose-slate dark:prose-invert max-w-none ${className}`}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkFrontmatter]}
|
||||
rehypePlugins={[
|
||||
|
|
|
|||
30
src/components/emergency/EmergencyButton.tsx
Normal file
30
src/components/emergency/EmergencyButton.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
interface EmergencyButtonProps {
|
||||
onClick: () => 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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label="Activar modo emergencia"
|
||||
className={`fixed right-4 bottom-24 md:bottom-6 z-[999] flex items-center gap-2 rounded-full px-4 py-3 shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white ${
|
||||
isActive
|
||||
? 'bg-red-700 text-white'
|
||||
: 'bg-red-600 text-white hover:bg-red-700 active:scale-[0.98]'
|
||||
}`}
|
||||
>
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<span className="text-base font-semibold">Emergencia</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmergencyButton;
|
||||
143
src/components/emergency/EmergencyInstructions.tsx
Normal file
143
src/components/emergency/EmergencyInstructions.tsx
Normal file
|
|
@ -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<EmergencySituation, string> = {
|
||||
medica: 'Emergencia médica',
|
||||
accidente: 'Accidente',
|
||||
tecnico: 'Incidente técnico',
|
||||
otro: 'Otro',
|
||||
};
|
||||
|
||||
const situationSteps: Record<EmergencySituation, string[]> = {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="text-sm uppercase tracking-wide text-white/70">No estás solo. Sigue estos pasos.</div>
|
||||
<h2 className="text-2xl font-bold text-white">{situationLabels[situation]}</h2>
|
||||
<p className="text-sm text-white/80">Una acción clara por paso. Vamos contigo.</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-white/20 bg-white/10 p-5 space-y-4">
|
||||
{steps.map((step) => (
|
||||
<div key={step} className="flex items-start gap-3 text-white text-lg">
|
||||
<span className="mt-1 h-3 w-3 rounded-full bg-white" />
|
||||
<span>{step}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-white/20 bg-white/5 p-4 text-white/80 text-sm">
|
||||
{notificationStatus === 'sent' && 'Coordinación notificada.'}
|
||||
{notificationStatus === 'queued' && 'Aviso guardado para sincronizar.'}
|
||||
{notificationStatus === 'idle' && 'Preparando aviso a coordinación...'}
|
||||
</div>
|
||||
|
||||
{isOnline ? (
|
||||
<div className="grid gap-3">
|
||||
<a
|
||||
href="tel:112"
|
||||
className="w-full rounded-xl bg-white px-4 py-4 text-center text-lg font-semibold text-red-700"
|
||||
>
|
||||
Llamar al 112
|
||||
</a>
|
||||
<a
|
||||
href="sms:112"
|
||||
className="w-full rounded-xl border-2 border-white/40 px-4 py-4 text-center text-lg font-semibold text-white"
|
||||
>
|
||||
Enviar mensaje rápido
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-white/30 bg-white/10 p-4 text-white text-sm">
|
||||
Sin conexión. Seguimos en modo offline y guardamos el evento para sincronizar después.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRestart}
|
||||
className="w-full rounded-xl border-2 border-white/40 px-4 py-3 text-base font-semibold text-white"
|
||||
>
|
||||
Cambiar situación
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateEmergencyEvent(eventId, { status: 'completed' });
|
||||
onClose();
|
||||
}}
|
||||
className="w-full rounded-xl bg-white/20 px-4 py-3 text-base font-semibold text-white"
|
||||
>
|
||||
Cerrar modo emergencia
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmergencyInstructions;
|
||||
118
src/components/emergency/EmergencyModeOverlay.tsx
Normal file
118
src/components/emergency/EmergencyModeOverlay.tsx
Normal file
|
|
@ -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<string | null>(null);
|
||||
const [situation, setSituation] = useState<EmergencySituation | null>(null);
|
||||
const [isOnline, setIsOnline] = useState<boolean>(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 (
|
||||
<div className="fixed inset-0 z-[1000] bg-black/95 text-white">
|
||||
<div className="mx-auto flex min-h-screen w-full max-w-2xl flex-col justify-between px-6 py-8">
|
||||
<div className="space-y-3 text-center">
|
||||
<p className="text-sm uppercase tracking-wide text-white/70">Modo Emergencia</p>
|
||||
<h1 className="text-3xl font-extrabold text-white">{title}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 py-8">
|
||||
{stage === 'select' && (
|
||||
<EmergencySituationSelector
|
||||
onSelect={(value) => {
|
||||
if (eventId) {
|
||||
updateEmergencyEvent(eventId, {
|
||||
situation: value,
|
||||
status: 'situation_selected',
|
||||
});
|
||||
}
|
||||
setSituation(value);
|
||||
setStage('instructions');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{stage === 'instructions' && eventId && situation && (
|
||||
<EmergencyInstructions
|
||||
eventId={eventId}
|
||||
situation={situation}
|
||||
isOnline={isOnline}
|
||||
onRestart={() => {
|
||||
setStage('select');
|
||||
setSituation(null);
|
||||
}}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="w-full rounded-xl border border-white/30 px-4 py-3 text-base font-semibold text-white/80"
|
||||
>
|
||||
Salir del modo emergencia
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmergencyModeOverlay;
|
||||
20
src/components/emergency/EmergencyModeRoot.tsx
Normal file
20
src/components/emergency/EmergencyModeRoot.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<EmergencyButton onClick={() => setIsOpen(true)} isActive={isOpen} />
|
||||
<EmergencyModeOverlay isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmergencyModeRoot;
|
||||
49
src/components/emergency/EmergencySituationSelector.tsx
Normal file
49
src/components/emergency/EmergencySituationSelector.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { EmergencySituation } from '@/services/emergency.service';
|
||||
|
||||
interface EmergencySituationSelectorProps {
|
||||
onSelect: (situation: EmergencySituation) => void;
|
||||
}
|
||||
|
||||
const EmergencySituationSelector = ({ onSelect }: EmergencySituationSelectorProps) => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-2xl font-bold text-white">¿Qué está pasando ahora mismo?</h2>
|
||||
<p className="text-sm text-white/80">Selecciona la opción que mejor encaje.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect('medica')}
|
||||
className="w-full rounded-xl border-2 border-white/20 bg-white/10 px-4 py-5 text-lg font-semibold text-white hover:bg-white/20"
|
||||
>
|
||||
Emergencia médica
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect('accidente')}
|
||||
className="w-full rounded-xl border-2 border-white/20 bg-white/10 px-4 py-5 text-lg font-semibold text-white hover:bg-white/20"
|
||||
>
|
||||
Accidente
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect('tecnico')}
|
||||
className="w-full rounded-xl border-2 border-white/20 bg-white/10 px-4 py-5 text-lg font-semibold text-white hover:bg-white/20"
|
||||
>
|
||||
Incidente técnico
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect('otro')}
|
||||
className="w-full rounded-xl border-2 border-white/20 bg-white/10 px-4 py-5 text-lg font-semibold text-white hover:bg-white/20"
|
||||
>
|
||||
Otro
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmergencySituationSelector;
|
||||
167
src/services/emergency.service.ts
Normal file
167
src/services/emergency.service.ts
Normal file
|
|
@ -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<EmergencyEvent>) => {
|
||||
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<EmergencyLocation | null> => {
|
||||
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';
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue