- ✅ Herramientas nuevas: * Temporizador de RCP con alertas cada 2 minutos * Calculadora de Duración de Botella de Oxígeno * Calculadora de Goteo (gotas/min y ml/h) * Tabla de perfusión Adrenalina agregada - ✅ Actualización Protocolo RCP: * Orden actualizado: Comprobar consciencia → Llamar 112 → Iniciar RCP * Aplicado a RCP Adulto SVB y Pediátrico - ✅ Cambios UI: * Botones de emergencias críticas con fondo negro y texto blanco * Enlaces de códigos corregidos - ✅ Medicación TES: * Nueva sección separada para medicación autorizada bajo prescripción * Aviso legal prominente * Sin dosis ni decisiones clínicas - ✅ Correcciones: * Errores de sintaxis JSX corregidos (símbolos < y >) * Favicon SVG actualizado * GitHub Pages configurado correctamente
199 lines
6.6 KiB
TypeScript
199 lines
6.6 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
||
import { Play, Pause, RotateCcw, AlertTriangle, Clock, Info } from 'lucide-react';
|
||
import { Button } from '@/components/ui/button';
|
||
import Badge from '@/components/shared/Badge';
|
||
|
||
const RCPTimer = () => {
|
||
const [isRunning, setIsRunning] = useState(false);
|
||
const [elapsedTime, setElapsedTime] = useState(0); // en segundos
|
||
const [cycles, setCycles] = useState(0);
|
||
const [lastCycleTime, setLastCycleTime] = useState(0);
|
||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||
|
||
// Ciclo de RCP: 2 minutos = 120 segundos
|
||
const CYCLE_DURATION = 120;
|
||
|
||
useEffect(() => {
|
||
if (isRunning) {
|
||
intervalRef.current = setInterval(() => {
|
||
setElapsedTime((prev) => {
|
||
const newTime = prev + 1;
|
||
const cycleTime = newTime - lastCycleTime;
|
||
|
||
// Alerta cada 2 minutos (cambio de reanimador)
|
||
if (cycleTime >= CYCLE_DURATION) {
|
||
setCycles((prev) => prev + 1);
|
||
setLastCycleTime(newTime);
|
||
playAlert();
|
||
}
|
||
|
||
return newTime;
|
||
});
|
||
}, 1000);
|
||
} else {
|
||
if (intervalRef.current) {
|
||
clearInterval(intervalRef.current);
|
||
}
|
||
}
|
||
|
||
return () => {
|
||
if (intervalRef.current) {
|
||
clearInterval(intervalRef.current);
|
||
}
|
||
};
|
||
}, [isRunning, lastCycleTime]);
|
||
|
||
const playAlert = () => {
|
||
// Crear audio para alerta (usando Web Audio API)
|
||
if (typeof Audio !== 'undefined') {
|
||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||
const oscillator = audioContext.createOscillator();
|
||
const gainNode = audioContext.createGain();
|
||
|
||
oscillator.connect(gainNode);
|
||
gainNode.connect(audioContext.destination);
|
||
|
||
oscillator.frequency.value = 800;
|
||
oscillator.type = 'sine';
|
||
|
||
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
|
||
|
||
oscillator.start(audioContext.currentTime);
|
||
oscillator.stop(audioContext.currentTime + 0.5);
|
||
}
|
||
};
|
||
|
||
const handleStart = () => {
|
||
setIsRunning(true);
|
||
if (elapsedTime === 0) {
|
||
setLastCycleTime(0);
|
||
}
|
||
};
|
||
|
||
const handlePause = () => {
|
||
setIsRunning(false);
|
||
};
|
||
|
||
const handleReset = () => {
|
||
setIsRunning(false);
|
||
setElapsedTime(0);
|
||
setCycles(0);
|
||
setLastCycleTime(0);
|
||
};
|
||
|
||
const formatTime = (seconds: number): string => {
|
||
const mins = Math.floor(seconds / 60);
|
||
const secs = seconds % 60;
|
||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||
};
|
||
|
||
const cycleTime = elapsedTime - lastCycleTime;
|
||
const timeUntilNextCycle = CYCLE_DURATION - cycleTime;
|
||
const progress = (cycleTime / CYCLE_DURATION) * 100;
|
||
|
||
return (
|
||
<div className="card-procedure">
|
||
<h3 className="font-bold text-foreground text-lg mb-4">
|
||
⏱️ Temporizador de RCP
|
||
</h3>
|
||
|
||
<div className="space-y-4">
|
||
{/* Información */}
|
||
<div className="p-3 bg-muted/50 rounded-lg border border-border">
|
||
<div className="flex items-start gap-2">
|
||
<Info className="w-5 h-5 text-info mt-0.5 flex-shrink-0" />
|
||
<div className="text-sm text-muted-foreground">
|
||
<p className="font-semibold text-foreground mb-1">Ciclos de RCP:</p>
|
||
<p>Cada 2 minutos (120 segundos) se debe cambiar de reanimador para mantener calidad de compresiones.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tiempo principal */}
|
||
<div className="p-6 bg-card border-2 border-primary rounded-xl text-center">
|
||
<div className="flex items-center justify-center gap-2 mb-2">
|
||
<Clock className="w-6 h-6 text-primary" />
|
||
<p className="text-muted-foreground text-sm">Tiempo Total</p>
|
||
</div>
|
||
<p className="text-5xl font-bold text-foreground mb-2">
|
||
{formatTime(elapsedTime)}
|
||
</p>
|
||
<Badge variant="info" className="text-sm px-3 py-1">
|
||
Ciclos completados: {cycles}
|
||
</Badge>
|
||
</div>
|
||
|
||
{/* Progreso del ciclo actual */}
|
||
{isRunning && (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span className="text-muted-foreground">Tiempo hasta cambio de reanimador</span>
|
||
<span className="font-bold text-foreground">
|
||
{formatTime(timeUntilNextCycle)}
|
||
</span>
|
||
</div>
|
||
<div className="w-full bg-muted rounded-full h-3 overflow-hidden">
|
||
<div
|
||
className="h-full bg-primary transition-all duration-1000"
|
||
style={{ width: `${progress}%` }}
|
||
/>
|
||
</div>
|
||
{timeUntilNextCycle <= 10 && timeUntilNextCycle > 0 && (
|
||
<div className="p-3 bg-[hsl(var(--emergency-high))]/10 border border-[hsl(var(--emergency-high))]/30 rounded-lg">
|
||
<div className="flex items-center gap-2">
|
||
<AlertTriangle className="w-5 h5 text-[hsl(var(--emergency-high))]" />
|
||
<p className="text-sm text-[hsl(var(--emergency-high))] font-semibold">
|
||
¡Cambio de reanimador en {timeUntilNextCycle} segundos!
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Controles */}
|
||
<div className="flex gap-2">
|
||
{!isRunning ? (
|
||
<Button
|
||
onClick={handleStart}
|
||
className="flex-1 bg-primary text-primary-foreground"
|
||
>
|
||
<Play className="w-4 h-4 mr-2" />
|
||
Iniciar
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
onClick={handlePause}
|
||
variant="outline"
|
||
className="flex-1"
|
||
>
|
||
<Pause className="w-4 h-4 mr-2" />
|
||
Pausar
|
||
</Button>
|
||
)}
|
||
<Button
|
||
onClick={handleReset}
|
||
variant="outline"
|
||
className="flex-1"
|
||
>
|
||
<RotateCcw className="w-4 h-4 mr-2" />
|
||
Reiniciar
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Instrucciones */}
|
||
<div className="p-3 bg-muted/50 rounded-lg border border-border">
|
||
<p className="text-xs text-muted-foreground">
|
||
<strong>Uso:</strong> Iniciar cuando comience RCP. El temporizador alertará cada 2 minutos para cambio de reanimador.
|
||
Pausar durante desfibrilación si es necesario.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default RCPTimer;
|