Implement core TES PWA layout
- Set up dark, high-contrast design system - Add Tailwind config and base styles - Create data scaffolding for procedures and drugs (partial) - Establish bottom navigation and home structure (partial) X-Lovable-Edit-ID: edt-14d9f7c0-9c39-4974-b647-0064a1cc00cf
This commit is contained in:
commit
b915867d80
28
index.html
28
index.html
|
|
@ -1,22 +1,24 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<!-- TODO: Set the document title to the name of your application -->
|
<title>EMERGES TES - Guía de Protocolos de Emergencias</title>
|
||||||
<title>Lovable App</title>
|
<meta name="description" content="Guía rápida de protocolos médicos de emergencias para Técnicos de Emergencias Sanitarias (TES). RCP, fármacos, calculadoras y más." />
|
||||||
<meta name="description" content="Lovable Generated Project" />
|
<meta name="author" content="EMERGES TES" />
|
||||||
<meta name="author" content="Lovable" />
|
<meta name="theme-color" content="#1a1f2e" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
|
||||||
<!-- TODO: Update og:title to match your application name -->
|
<meta property="og:title" content="EMERGES TES - Guía de Protocolos" />
|
||||||
<meta property="og:title" content="Lovable App" />
|
<meta property="og:description" content="Guía rápida de protocolos médicos de emergencias para TES" />
|
||||||
<meta property="og:description" content="Lovable Generated Project" />
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary" />
|
||||||
<meta name="twitter:site" content="@Lovable" />
|
<meta name="twitter:title" content="EMERGES TES" />
|
||||||
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
<meta name="twitter:description" content="Protocolos de emergencias para TES" />
|
||||||
|
|
||||||
|
<link rel="canonical" href="https://emerges-tes.lovable.app/" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
77
src/App.tsx
77
src/App.tsx
|
|
@ -1,27 +1,72 @@
|
||||||
|
import { useState } from 'react';
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import Index from "./pages/Index";
|
import Header from "@/components/layout/Header";
|
||||||
|
import BottomNav from "@/components/layout/BottomNav";
|
||||||
|
import SearchModal from "@/components/layout/SearchModal";
|
||||||
|
import MenuSheet from "@/components/layout/MenuSheet";
|
||||||
|
import Home from "./pages/Index";
|
||||||
|
import SoporteVital from "./pages/SoporteVital";
|
||||||
|
import Patologias from "./pages/Patologias";
|
||||||
|
import Escena from "./pages/Escena";
|
||||||
|
import Farmacos from "./pages/Farmacos";
|
||||||
|
import Herramientas from "./pages/Herramientas";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const App = () => (
|
const App = () => {
|
||||||
<QueryClientProvider client={queryClient}>
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
<TooltipProvider>
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
<Toaster />
|
|
||||||
<Sonner />
|
return (
|
||||||
<BrowserRouter>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Routes>
|
<TooltipProvider>
|
||||||
<Route path="/" element={<Index />} />
|
<Toaster />
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
<Sonner />
|
||||||
<Route path="*" element={<NotFound />} />
|
<BrowserRouter>
|
||||||
</Routes>
|
<div className="min-h-screen bg-background">
|
||||||
</BrowserRouter>
|
<Header
|
||||||
</TooltipProvider>
|
onSearchClick={() => setIsSearchOpen(true)}
|
||||||
</QueryClientProvider>
|
onMenuClick={() => setIsMenuOpen(true)}
|
||||||
);
|
/>
|
||||||
|
|
||||||
|
<main className="pt-14 pb-safe">
|
||||||
|
<div className="container max-w-2xl py-4">
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={<Home onSearchClick={() => setIsSearchOpen(true)} />}
|
||||||
|
/>
|
||||||
|
<Route path="/soporte-vital" element={<SoporteVital />} />
|
||||||
|
<Route path="/patologias" element={<Patologias />} />
|
||||||
|
<Route path="/escena" element={<Escena />} />
|
||||||
|
<Route path="/farmacos" element={<Farmacos />} />
|
||||||
|
<Route path="/herramientas" element={<Herramientas />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<BottomNav />
|
||||||
|
|
||||||
|
<SearchModal
|
||||||
|
isOpen={isSearchOpen}
|
||||||
|
onClose={() => setIsSearchOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MenuSheet
|
||||||
|
isOpen={isMenuOpen}
|
||||||
|
onClose={() => setIsMenuOpen(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
|
</TooltipProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
||||||
173
src/components/drugs/DrugCard.tsx
Normal file
173
src/components/drugs/DrugCard.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ChevronDown, ChevronUp, Star, Package, Syringe, User, Baby, AlertCircle } from 'lucide-react';
|
||||||
|
import { Drug } from '@/data/drugs';
|
||||||
|
import Badge from '@/components/shared/Badge';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface DrugCardProps {
|
||||||
|
drug: Drug;
|
||||||
|
defaultExpanded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DrugCard = ({ drug, defaultExpanded = false }: DrugCardProps) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||||
|
const [isFavorite, setIsFavorite] = useState(false);
|
||||||
|
|
||||||
|
const toggleFavorite = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsFavorite(!isFavorite);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-procedure">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="w-full text-left"
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="text-2xl">💊</span>
|
||||||
|
<h3 className="font-bold text-foreground text-lg">
|
||||||
|
{drug.genericName.toUpperCase()}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-sm">({drug.tradeName})</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={toggleFavorite}
|
||||||
|
className={cn(
|
||||||
|
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
|
||||||
|
isFavorite ? 'text-warning' : 'text-muted-foreground hover:text-foreground'
|
||||||
|
)}
|
||||||
|
aria-label={isFavorite ? 'Quitar de favoritos' : 'Añadir a favoritos'}
|
||||||
|
>
|
||||||
|
<Star className={cn('w-5 h-5', isFavorite && 'fill-current')} />
|
||||||
|
</button>
|
||||||
|
<div className="w-10 h-10 flex items-center justify-center">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-5 h-5 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-border space-y-4">
|
||||||
|
{/* Presentation */}
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Package className="w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Presentación</p>
|
||||||
|
<p className="text-foreground font-medium">{drug.presentation}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Adult Dose */}
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<User className="w-5 h-5 text-info flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Dosis Adulto</p>
|
||||||
|
<p className="text-foreground font-medium">{drug.adultDose}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pediatric Dose */}
|
||||||
|
{drug.pediatricDose && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Baby className="w-5 h-5 text-info flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Dosis Pediátrica</p>
|
||||||
|
<p className="text-foreground font-medium">{drug.pediatricDose}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Routes */}
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Syringe className="w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Vías de Administración</p>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{drug.routes.map((route) => (
|
||||||
|
<Badge key={route} variant="info">
|
||||||
|
{route}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dilution */}
|
||||||
|
{drug.dilution && (
|
||||||
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">Dilución</p>
|
||||||
|
<p className="text-foreground">{drug.dilution}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Indications */}
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2 flex items-center gap-1">
|
||||||
|
<span className="text-success">✓</span> Indicaciones
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{drug.indications.map((indication, index) => (
|
||||||
|
<li key={index} className="text-foreground text-sm flex items-start gap-2">
|
||||||
|
<span className="text-success">•</span>
|
||||||
|
<span>{indication}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contraindications */}
|
||||||
|
<div className="warning-box">
|
||||||
|
<p className="text-sm text-warning mb-2 flex items-center gap-1 font-semibold">
|
||||||
|
<AlertCircle className="w-4 h-4" /> Contraindicaciones
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{drug.contraindications.map((contraindication, index) => (
|
||||||
|
<li key={index} className="text-foreground text-sm flex items-start gap-2">
|
||||||
|
<span className="text-warning">•</span>
|
||||||
|
<span>{contraindication}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Antidote */}
|
||||||
|
{drug.antidote && (
|
||||||
|
<div className="p-3 bg-primary/10 border border-primary/30 rounded-lg">
|
||||||
|
<p className="text-sm text-primary font-semibold">
|
||||||
|
Antídoto: {drug.antidote}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
{drug.notes && drug.notes.length > 0 && (
|
||||||
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">Notas</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{drug.notes.map((note, index) => (
|
||||||
|
<li key={index} className="text-foreground text-sm">
|
||||||
|
{note}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DrugCard;
|
||||||
40
src/components/layout/BottomNav.tsx
Normal file
40
src/components/layout/BottomNav.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { Home, AlertTriangle, Stethoscope, Video, Pill, Wrench } from 'lucide-react';
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
path: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navItems: NavItem[] = [
|
||||||
|
{ path: '/', icon: <Home className="w-5 h-5" />, label: 'Home' },
|
||||||
|
{ path: '/soporte-vital', icon: <AlertTriangle className="w-5 h-5" />, label: 'Soporte' },
|
||||||
|
{ path: '/patologias', icon: <Stethoscope className="w-5 h-5" />, label: 'Patologías' },
|
||||||
|
{ path: '/escena', icon: <Video className="w-5 h-5" />, label: 'Escena' },
|
||||||
|
{ path: '/farmacos', icon: <Pill className="w-5 h-5" />, label: 'Fármacos' },
|
||||||
|
{ path: '/herramientas', icon: <Wrench className="w-5 h-5" />, label: 'Herram.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const BottomNav = () => {
|
||||||
|
return (
|
||||||
|
<nav className="bottom-nav">
|
||||||
|
<div className="flex items-stretch justify-around max-w-lg mx-auto">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`bottom-nav-item flex-1 ${isActive ? 'active' : ''}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
<span className="text-2xs mt-1 font-medium">{item.label}</span>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BottomNav;
|
||||||
80
src/components/layout/Header.tsx
Normal file
80
src/components/layout/Header.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { Search, Menu, Wifi, WifiOff, Star } from 'lucide-react';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
onSearchClick: () => void;
|
||||||
|
onMenuClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = ({ onSearchClick, onMenuClick }: HeaderProps) => {
|
||||||
|
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="fixed top-0 left-0 right-0 z-50 bg-card border-b border-border">
|
||||||
|
<div className="flex items-center justify-between h-14 px-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
||||||
|
<span className="text-primary-foreground font-bold text-sm">TES</span>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<h1 className="font-semibold text-foreground text-sm">EMERGES TES</h1>
|
||||||
|
<p className="text-2xs text-muted-foreground">Guía de Protocolos</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1.5 px-2 py-1 rounded-full text-2xs font-medium ${
|
||||||
|
isOnline
|
||||||
|
? 'bg-success/20 text-success'
|
||||||
|
: 'bg-warning/20 text-warning'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isOnline ? (
|
||||||
|
<>
|
||||||
|
<Wifi className="w-3 h-3" />
|
||||||
|
<span className="hidden sm:inline">Online</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<WifiOff className="w-3 h-3" />
|
||||||
|
<span className="hidden sm:inline">Offline</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onSearchClick}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-muted transition-colors"
|
||||||
|
aria-label="Buscar"
|
||||||
|
>
|
||||||
|
<Search className="w-5 h-5 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onMenuClick}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-muted transition-colors"
|
||||||
|
aria-label="Menú"
|
||||||
|
>
|
||||||
|
<Menu className="w-5 h-5 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
63
src/components/layout/MenuSheet.tsx
Normal file
63
src/components/layout/MenuSheet.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { X, Star, History, Settings, Info, Share2 } from 'lucide-react';
|
||||||
|
|
||||||
|
interface MenuSheetProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuSheet = ({ isOpen, onClose }: MenuSheetProps) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ icon: <Star className="w-5 h-5" />, label: 'Favoritos', onClick: () => {} },
|
||||||
|
{ icon: <History className="w-5 h-5" />, label: 'Historial', onClick: () => {} },
|
||||||
|
{ icon: <Share2 className="w-5 h-5" />, label: 'Compartir App', onClick: () => {} },
|
||||||
|
{ icon: <Settings className="w-5 h-5" />, label: 'Ajustes', onClick: () => {} },
|
||||||
|
{ icon: <Info className="w-5 h-5" />, label: 'Acerca de', onClick: () => {} },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[90] bg-background/80 backdrop-blur-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<div className="fixed top-0 right-0 bottom-0 z-[95] w-80 max-w-[85vw] bg-card border-l border-border shadow-xl">
|
||||||
|
<div className="flex items-center justify-between h-14 px-4 border-b border-border">
|
||||||
|
<h2 className="font-semibold text-foreground">Menú</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-muted transition-colors"
|
||||||
|
aria-label="Cerrar menú"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 space-y-2">
|
||||||
|
{menuItems.map((item, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={item.onClick}
|
||||||
|
className="w-full flex items-center gap-4 p-4 rounded-xl hover:bg-muted transition-colors text-left"
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground">{item.icon}</span>
|
||||||
|
<span className="font-medium text-foreground">{item.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-4 border-t border-border">
|
||||||
|
<p className="text-2xs text-muted-foreground text-center">
|
||||||
|
EMERGES TES v1.0.0
|
||||||
|
</p>
|
||||||
|
<p className="text-2xs text-muted-foreground text-center mt-1">
|
||||||
|
Guía de Protocolos para TES
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuSheet;
|
||||||
137
src/components/layout/SearchModal.tsx
Normal file
137
src/components/layout/SearchModal.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Search, X, FileText, Pill, ArrowRight } from 'lucide-react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { searchProcedures, Procedure } from '@/data/procedures';
|
||||||
|
import { searchDrugs, Drug } from '@/data/drugs';
|
||||||
|
|
||||||
|
interface SearchModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchResult = {
|
||||||
|
type: 'procedure' | 'drug';
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchModal = ({ isOpen, onClose }: SearchModalProps) => {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [results, setResults] = useState<SearchResult[]>([]);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query.length < 2) {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const procedures = searchProcedures(query).map((p): SearchResult => ({
|
||||||
|
type: 'procedure',
|
||||||
|
id: p.id,
|
||||||
|
title: p.shortTitle,
|
||||||
|
subtitle: p.category.replace('_', ' '),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const drugs = searchDrugs(query).map((d): SearchResult => ({
|
||||||
|
type: 'drug',
|
||||||
|
id: d.id,
|
||||||
|
title: d.genericName,
|
||||||
|
subtitle: d.tradeName,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setResults([...procedures, ...drugs].slice(0, 8));
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
const handleResultClick = (result: SearchResult) => {
|
||||||
|
if (result.type === 'procedure') {
|
||||||
|
navigate(`/soporte-vital?id=${result.id}`);
|
||||||
|
} else {
|
||||||
|
navigate(`/farmacos?id=${result.id}`);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
setQuery('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[100] bg-background/95 backdrop-blur-sm">
|
||||||
|
<div className="flex flex-col h-full max-w-2xl mx-auto p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Buscar protocolo, fármaco, calculadora..."
|
||||||
|
className="w-full h-14 pl-12 pr-4 bg-card border border-border rounded-xl text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-14 h-14 flex items-center justify-center rounded-xl bg-card border border-border hover:bg-muted transition-colors"
|
||||||
|
aria-label="Cerrar búsqueda"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto scroll-touch">
|
||||||
|
{results.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{results.map((result) => (
|
||||||
|
<button
|
||||||
|
key={`${result.type}-${result.id}`}
|
||||||
|
onClick={() => handleResultClick(result)}
|
||||||
|
className="w-full flex items-center gap-4 p-4 bg-card border border-border rounded-xl hover:border-primary/50 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
|
||||||
|
{result.type === 'procedure' ? (
|
||||||
|
<FileText className="w-5 h-5 text-primary" />
|
||||||
|
) : (
|
||||||
|
<Pill className="w-5 h-5 text-secondary" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-foreground truncate">{result.title}</p>
|
||||||
|
{result.subtitle && (
|
||||||
|
<p className="text-sm text-muted-foreground capitalize truncate">
|
||||||
|
{result.subtitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="w-5 h-5 text-muted-foreground flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : query.length >= 2 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<Search className="w-12 h-12 text-muted-foreground mb-4" />
|
||||||
|
<p className="text-muted-foreground">No se encontraron resultados</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Intenta con otros términos</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<Search className="w-12 h-12 text-muted-foreground mb-4" />
|
||||||
|
<p className="text-muted-foreground">Escribe para buscar</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Protocolos, fármacos, calculadoras...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchModal;
|
||||||
147
src/components/procedures/ProcedureCard.tsx
Normal file
147
src/components/procedures/ProcedureCard.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ChevronDown, ChevronUp, Star, AlertTriangle, User, Baby } from 'lucide-react';
|
||||||
|
import { Procedure, Priority } from '@/data/procedures';
|
||||||
|
import Badge from '@/components/shared/Badge';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface ProcedureCardProps {
|
||||||
|
procedure: Procedure;
|
||||||
|
defaultExpanded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorityToBadgeVariant: Record<Priority, 'critical' | 'high' | 'medium' | 'low'> = {
|
||||||
|
critico: 'critical',
|
||||||
|
alto: 'high',
|
||||||
|
medio: 'medium',
|
||||||
|
bajo: 'low',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProcedureCard = ({ procedure, defaultExpanded = false }: ProcedureCardProps) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||||
|
const [isFavorite, setIsFavorite] = useState(false);
|
||||||
|
|
||||||
|
const toggleFavorite = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsFavorite(!isFavorite);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-procedure">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="w-full text-left"
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||||
|
<Badge variant={priorityToBadgeVariant[procedure.priority]}>
|
||||||
|
{procedure.priority}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="info">
|
||||||
|
{procedure.ageGroup === 'adulto' && <User className="w-3 h-3 mr-1" />}
|
||||||
|
{procedure.ageGroup === 'pediatrico' && <Baby className="w-3 h-3 mr-1" />}
|
||||||
|
{procedure.ageGroup}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-foreground text-lg leading-tight">
|
||||||
|
{procedure.shortTitle}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={toggleFavorite}
|
||||||
|
className={cn(
|
||||||
|
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
|
||||||
|
isFavorite ? 'text-warning' : 'text-muted-foreground hover:text-foreground'
|
||||||
|
)}
|
||||||
|
aria-label={isFavorite ? 'Quitar de favoritos' : 'Añadir a favoritos'}
|
||||||
|
>
|
||||||
|
<Star className={cn('w-5 h-5', isFavorite && 'fill-current')} />
|
||||||
|
</button>
|
||||||
|
<div className="w-10 h-10 flex items-center justify-center">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-5 h-5 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-border">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="font-semibold text-foreground text-sm uppercase tracking-wide">
|
||||||
|
Pasos
|
||||||
|
</h4>
|
||||||
|
<ol className="space-y-3">
|
||||||
|
{procedure.steps.map((step, index) => (
|
||||||
|
<li key={index} className="flex items-start gap-3">
|
||||||
|
<span className="step-number">{index + 1}</span>
|
||||||
|
<span className="text-foreground pt-1 flex-1">{step}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{procedure.warnings.length > 0 && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="warning-box">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<AlertTriangle className="w-4 h-4 text-warning" />
|
||||||
|
<h4 className="font-semibold text-warning text-sm uppercase tracking-wide">
|
||||||
|
Puntos Clave
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{procedure.warnings.map((warning, index) => (
|
||||||
|
<li key={index} className="text-foreground text-sm flex items-start gap-2">
|
||||||
|
<span className="text-warning">⚠️</span>
|
||||||
|
<span>{warning}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{procedure.keyPoints && procedure.keyPoints.length > 0 && (
|
||||||
|
<div className="mt-4 p-3 bg-muted rounded-lg">
|
||||||
|
<h4 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-2">
|
||||||
|
Recuerda
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{procedure.keyPoints.map((point, index) => (
|
||||||
|
<li key={index} className="text-foreground text-sm flex items-start gap-2">
|
||||||
|
<span className="text-info">•</span>
|
||||||
|
<span>{point}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{procedure.equipment && procedure.equipment.length > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<h4 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-2">
|
||||||
|
Material
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{procedure.equipment.map((item, index) => (
|
||||||
|
<Badge key={index} variant="default">
|
||||||
|
{item}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProcedureCard;
|
||||||
34
src/components/shared/Badge.tsx
Normal file
34
src/components/shared/Badge.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type BadgeVariant = 'critical' | 'high' | 'medium' | 'low' | 'info' | 'default';
|
||||||
|
|
||||||
|
interface BadgeProps {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantClasses: Record<BadgeVariant, string> = {
|
||||||
|
critical: 'badge-critical',
|
||||||
|
high: 'badge-high',
|
||||||
|
medium: 'badge-medium',
|
||||||
|
low: 'bg-success/20 text-success border border-success/30',
|
||||||
|
info: 'badge-info',
|
||||||
|
default: 'bg-muted text-muted-foreground border border-border',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Badge = ({ variant = 'default', children, className }: BadgeProps) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center px-2 py-0.5 rounded-md text-2xs font-semibold uppercase tracking-wide',
|
||||||
|
variantClasses[variant],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Badge;
|
||||||
48
src/components/shared/EmergencyButton.tsx
Normal file
48
src/components/shared/EmergencyButton.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type ButtonVariant = 'critical' | 'high' | 'medium';
|
||||||
|
|
||||||
|
interface EmergencyButtonProps {
|
||||||
|
to: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantClasses: Record<ButtonVariant, string> = {
|
||||||
|
critical: 'btn-emergency-critical',
|
||||||
|
high: 'btn-emergency-high',
|
||||||
|
medium: 'btn-emergency-medium',
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmergencyButton = ({
|
||||||
|
to,
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
variant = 'critical',
|
||||||
|
className,
|
||||||
|
}: EmergencyButtonProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={to}
|
||||||
|
className={cn(
|
||||||
|
'btn-emergency flex flex-col items-center justify-center gap-1 p-4',
|
||||||
|
variantClasses[variant],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="w-8 h-8" />
|
||||||
|
<span className="text-sm font-semibold text-center leading-tight">{title}</span>
|
||||||
|
{subtitle && (
|
||||||
|
<span className="text-2xs opacity-80 text-center">{subtitle}</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmergencyButton;
|
||||||
83
src/components/tools/GlasgowCalculator.tsx
Normal file
83
src/components/tools/GlasgowCalculator.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { glasgowScale, getGlasgowInterpretation } from '@/data/calculators';
|
||||||
|
import Badge from '@/components/shared/Badge';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const GlasgowCalculator = () => {
|
||||||
|
const [scores, setScores] = useState<Record<string, number>>({
|
||||||
|
'Apertura Ocular': 4,
|
||||||
|
'Respuesta Verbal': 5,
|
||||||
|
'Respuesta Motora': 6,
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalScore = Object.values(scores).reduce((sum, score) => sum + score, 0);
|
||||||
|
const interpretation = getGlasgowInterpretation(totalScore);
|
||||||
|
|
||||||
|
const handleScoreChange = (category: string, value: number) => {
|
||||||
|
setScores((prev) => ({ ...prev, [category]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-procedure">
|
||||||
|
<h3 className="font-bold text-foreground text-lg mb-4">
|
||||||
|
🧠 Escala de Glasgow (GCS)
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{glasgowScale.map((category) => (
|
||||||
|
<div key={category.name}>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="font-semibold text-foreground">{category.name}</h4>
|
||||||
|
<span className="text-xl font-bold text-primary">
|
||||||
|
{scores[category.name]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{category.options.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleScoreChange(category.name, option.value)}
|
||||||
|
className={cn(
|
||||||
|
'w-full flex items-center justify-between p-3 rounded-lg border transition-colors text-left',
|
||||||
|
scores[category.name] === option.value
|
||||||
|
? 'bg-primary/10 border-primary text-foreground'
|
||||||
|
: 'bg-muted border-border text-muted-foreground hover:border-primary/50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-sm">{option.label}</span>
|
||||||
|
<span className="font-bold">{option.value}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 p-4 bg-card border-2 border-primary rounded-xl text-center">
|
||||||
|
<p className="text-muted-foreground text-sm mb-1">Puntuación Total</p>
|
||||||
|
<p className="text-5xl font-bold text-foreground mb-2">{totalScore}</p>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
interpretation.color === 'critical'
|
||||||
|
? 'critical'
|
||||||
|
: interpretation.color === 'high'
|
||||||
|
? 'high'
|
||||||
|
: 'low'
|
||||||
|
}
|
||||||
|
className="text-sm px-3 py-1"
|
||||||
|
>
|
||||||
|
{interpretation.severity}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center text-sm text-muted-foreground">
|
||||||
|
<p>Rango: 3 (mínimo) - 15 (máximo)</p>
|
||||||
|
<p className="mt-1">
|
||||||
|
≤8: Grave (IOT) | 9-12: Moderado | 13-15: Leve
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GlasgowCalculator;
|
||||||
54
src/components/tools/InfusionTableView.tsx
Normal file
54
src/components/tools/InfusionTableView.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { InfusionTable } from '@/data/calculators';
|
||||||
|
|
||||||
|
interface InfusionTableViewProps {
|
||||||
|
table: InfusionTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InfusionTableView = ({ table }: InfusionTableViewProps) => {
|
||||||
|
return (
|
||||||
|
<div className="card-procedure">
|
||||||
|
<h3 className="font-bold text-foreground text-lg mb-2">{table.name}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">{table.preparation}</p>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto -mx-4 px-4">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-border">
|
||||||
|
<th className="text-left py-3 px-2 font-semibold text-muted-foreground">
|
||||||
|
Peso (kg)
|
||||||
|
</th>
|
||||||
|
{table.columns.map((col) => (
|
||||||
|
<th
|
||||||
|
key={col}
|
||||||
|
className="text-center py-3 px-2 font-semibold text-muted-foreground"
|
||||||
|
>
|
||||||
|
{col}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.rows.map((row) => (
|
||||||
|
<tr key={row.weight} className="border-b border-border/50">
|
||||||
|
<td className="py-3 px-2 font-bold text-foreground">
|
||||||
|
{row.weight}
|
||||||
|
</td>
|
||||||
|
{table.columns.map((col) => (
|
||||||
|
<td key={col} className="text-center py-3 px-2 text-foreground">
|
||||||
|
{row.doses[col]} {table.unit}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-2xs text-muted-foreground mt-4 text-center">
|
||||||
|
Rango de dosis: {table.doseRange}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfusionTableView;
|
||||||
101
src/data/calculators.ts
Normal file
101
src/data/calculators.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
export interface GlasgowOption {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlasgowCategory {
|
||||||
|
name: string;
|
||||||
|
options: GlasgowOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const glasgowScale: GlasgowCategory[] = [
|
||||||
|
{
|
||||||
|
name: 'Apertura Ocular',
|
||||||
|
options: [
|
||||||
|
{ label: 'Espontánea', value: 4 },
|
||||||
|
{ label: 'A la orden verbal', value: 3 },
|
||||||
|
{ label: 'Al dolor', value: 2 },
|
||||||
|
{ label: 'No abre', value: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Respuesta Verbal',
|
||||||
|
options: [
|
||||||
|
{ label: 'Orientado', value: 5 },
|
||||||
|
{ label: 'Confuso', value: 4 },
|
||||||
|
{ label: 'Palabras inapropiadas', value: 3 },
|
||||||
|
{ label: 'Sonidos incomprensibles', value: 2 },
|
||||||
|
{ label: 'Sin respuesta', value: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Respuesta Motora',
|
||||||
|
options: [
|
||||||
|
{ label: 'Obedece órdenes', value: 6 },
|
||||||
|
{ label: 'Localiza el dolor', value: 5 },
|
||||||
|
{ label: 'Retirada al dolor', value: 4 },
|
||||||
|
{ label: 'Flexión anormal (decorticación)', value: 3 },
|
||||||
|
{ label: 'Extensión anormal (descerebración)', value: 2 },
|
||||||
|
{ label: 'Sin respuesta', value: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getGlasgowInterpretation = (score: number): { severity: string; color: string } => {
|
||||||
|
if (score <= 8) return { severity: 'TCE Grave - IOT', color: 'critical' };
|
||||||
|
if (score <= 12) return { severity: 'TCE Moderado', color: 'high' };
|
||||||
|
return { severity: 'TCE Leve', color: 'low' };
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InfusionRow {
|
||||||
|
weight: number;
|
||||||
|
doses: { [key: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InfusionTable {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
drugName: string;
|
||||||
|
preparation: string;
|
||||||
|
unit: string;
|
||||||
|
doseRange: string;
|
||||||
|
columns: string[];
|
||||||
|
rows: InfusionRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const infusionTables: InfusionTable[] = [
|
||||||
|
{
|
||||||
|
id: 'dopamina',
|
||||||
|
name: 'Perfusión Dopamina',
|
||||||
|
drugName: 'Dopamina',
|
||||||
|
preparation: '200mg en 100ml SG5% = 2000 mcg/ml',
|
||||||
|
unit: 'ml/h',
|
||||||
|
doseRange: '2-20 mcg/kg/min',
|
||||||
|
columns: ['5 mcg/kg/min', '10 mcg/kg/min', '15 mcg/kg/min', '20 mcg/kg/min'],
|
||||||
|
rows: [
|
||||||
|
{ weight: 50, doses: { '5 mcg/kg/min': '7.5', '10 mcg/kg/min': '15', '15 mcg/kg/min': '22.5', '20 mcg/kg/min': '30' } },
|
||||||
|
{ weight: 60, doses: { '5 mcg/kg/min': '9', '10 mcg/kg/min': '18', '15 mcg/kg/min': '27', '20 mcg/kg/min': '36' } },
|
||||||
|
{ weight: 70, doses: { '5 mcg/kg/min': '10.5', '10 mcg/kg/min': '21', '15 mcg/kg/min': '31.5', '20 mcg/kg/min': '42' } },
|
||||||
|
{ weight: 80, doses: { '5 mcg/kg/min': '12', '10 mcg/kg/min': '24', '15 mcg/kg/min': '36', '20 mcg/kg/min': '48' } },
|
||||||
|
{ weight: 90, doses: { '5 mcg/kg/min': '13.5', '10 mcg/kg/min': '27', '15 mcg/kg/min': '40.5', '20 mcg/kg/min': '54' } },
|
||||||
|
{ weight: 100, doses: { '5 mcg/kg/min': '15', '10 mcg/kg/min': '30', '15 mcg/kg/min': '45', '20 mcg/kg/min': '60' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'noradrenalina',
|
||||||
|
name: 'Perfusión Noradrenalina',
|
||||||
|
drugName: 'Noradrenalina',
|
||||||
|
preparation: '8mg en 100ml SG5% = 80 mcg/ml',
|
||||||
|
unit: 'ml/h',
|
||||||
|
doseRange: '0.05-1 mcg/kg/min',
|
||||||
|
columns: ['0.1 mcg/kg/min', '0.2 mcg/kg/min', '0.3 mcg/kg/min', '0.5 mcg/kg/min'],
|
||||||
|
rows: [
|
||||||
|
{ weight: 50, doses: { '0.1 mcg/kg/min': '3.75', '0.2 mcg/kg/min': '7.5', '0.3 mcg/kg/min': '11.25', '0.5 mcg/kg/min': '18.75' } },
|
||||||
|
{ weight: 60, doses: { '0.1 mcg/kg/min': '4.5', '0.2 mcg/kg/min': '9', '0.3 mcg/kg/min': '13.5', '0.5 mcg/kg/min': '22.5' } },
|
||||||
|
{ weight: 70, doses: { '0.1 mcg/kg/min': '5.25', '0.2 mcg/kg/min': '10.5', '0.3 mcg/kg/min': '15.75', '0.5 mcg/kg/min': '26.25' } },
|
||||||
|
{ weight: 80, doses: { '0.1 mcg/kg/min': '6', '0.2 mcg/kg/min': '12', '0.3 mcg/kg/min': '18', '0.5 mcg/kg/min': '30' } },
|
||||||
|
{ weight: 90, doses: { '0.1 mcg/kg/min': '6.75', '0.2 mcg/kg/min': '13.5', '0.3 mcg/kg/min': '20.25', '0.5 mcg/kg/min': '33.75' } },
|
||||||
|
{ weight: 100, doses: { '0.1 mcg/kg/min': '7.5', '0.2 mcg/kg/min': '15', '0.3 mcg/kg/min': '22.5', '0.5 mcg/kg/min': '37.5' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
174
src/data/drugs.ts
Normal file
174
src/data/drugs.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
export type DrugCategory = 'cardiovascular' | 'respiratorio' | 'neurologico' | 'analgesia' | 'otros';
|
||||||
|
export type AdministrationRoute = 'IV' | 'IM' | 'SC' | 'IO' | 'Nebulizado' | 'SL' | 'Rectal' | 'Nasal';
|
||||||
|
|
||||||
|
export interface Drug {
|
||||||
|
id: string;
|
||||||
|
genericName: string;
|
||||||
|
tradeName: string;
|
||||||
|
category: DrugCategory;
|
||||||
|
presentation: string;
|
||||||
|
adultDose: string;
|
||||||
|
pediatricDose?: string;
|
||||||
|
routes: AdministrationRoute[];
|
||||||
|
dilution?: string;
|
||||||
|
indications: string[];
|
||||||
|
contraindications: string[];
|
||||||
|
sideEffects?: string[];
|
||||||
|
antidote?: string;
|
||||||
|
notes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const drugs: Drug[] = [
|
||||||
|
{
|
||||||
|
id: 'adrenalina',
|
||||||
|
genericName: 'Adrenalina',
|
||||||
|
tradeName: 'Adrenalina Braun®',
|
||||||
|
category: 'cardiovascular',
|
||||||
|
presentation: '1 mg/1 ml (ampolla)',
|
||||||
|
adultDose: 'PCR: 1mg IV/IO cada 3-5 min | Anafilaxia: 0.3-0.5mg IM',
|
||||||
|
pediatricDose: 'PCR: 0.01 mg/kg IV/IO | Anafilaxia: 0.01 mg/kg IM (máx 0.3mg)',
|
||||||
|
routes: ['IV', 'IO', 'IM'],
|
||||||
|
dilution: 'PCR: sin diluir | Perfusión: 1mg en 100ml SSF (10 mcg/ml)',
|
||||||
|
indications: [
|
||||||
|
'Parada cardiorrespiratoria',
|
||||||
|
'Anafilaxia',
|
||||||
|
'Shock séptico refractario',
|
||||||
|
'Bradicardia sintomática',
|
||||||
|
'Crisis asmática severa',
|
||||||
|
],
|
||||||
|
contraindications: [
|
||||||
|
'No hay contraindicaciones absolutas en emergencias vitales',
|
||||||
|
],
|
||||||
|
sideEffects: ['Taquicardia', 'HTA', 'Arritmias', 'Ansiedad'],
|
||||||
|
notes: [
|
||||||
|
'En anafilaxia: vía IM en cara anterolateral del muslo',
|
||||||
|
'Repetir cada 5-15 min si persisten síntomas',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'amiodarona',
|
||||||
|
genericName: 'Amiodarona',
|
||||||
|
tradeName: 'Trangorex®',
|
||||||
|
category: 'cardiovascular',
|
||||||
|
presentation: '150 mg/3 ml (ampolla)',
|
||||||
|
adultDose: 'FV/TVSP: 300mg IV en bolo | Mantenimiento: 900mg/24h',
|
||||||
|
pediatricDose: '5 mg/kg IV/IO (máx 300mg)',
|
||||||
|
routes: ['IV', 'IO'],
|
||||||
|
dilution: 'Diluir en SG5% (precipita con SSF)',
|
||||||
|
indications: [
|
||||||
|
'FV/TVSP refractaria a descargas',
|
||||||
|
'Taquicardia ventricular estable',
|
||||||
|
'Control de frecuencia en FA',
|
||||||
|
],
|
||||||
|
contraindications: [
|
||||||
|
'Bradicardia sinusal',
|
||||||
|
'Bloqueo AV 2º-3º grado sin marcapasos',
|
||||||
|
'Disfunción tiroidea severa',
|
||||||
|
'Hipersensibilidad al yodo',
|
||||||
|
],
|
||||||
|
sideEffects: ['Hipotensión', 'Bradicardia', 'Flebitis'],
|
||||||
|
notes: [
|
||||||
|
'Segunda dosis en PCR: 150mg si persiste FV/TVSP',
|
||||||
|
'Usar vía central si es posible',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'atropina',
|
||||||
|
genericName: 'Atropina',
|
||||||
|
tradeName: 'Atropina Braun®',
|
||||||
|
category: 'cardiovascular',
|
||||||
|
presentation: '1 mg/1 ml (ampolla)',
|
||||||
|
adultDose: 'Bradicardia: 0.5mg IV cada 3-5 min (máx 3mg)',
|
||||||
|
pediatricDose: '0.02 mg/kg IV (mín 0.1mg, máx 0.5mg)',
|
||||||
|
routes: ['IV', 'IO', 'IM'],
|
||||||
|
indications: [
|
||||||
|
'Bradicardia sintomática',
|
||||||
|
'Intoxicación por organofosforados',
|
||||||
|
'Premedicación antes de IOT',
|
||||||
|
],
|
||||||
|
contraindications: [
|
||||||
|
'Glaucoma de ángulo estrecho',
|
||||||
|
'Retención urinaria',
|
||||||
|
'Íleo paralítico',
|
||||||
|
],
|
||||||
|
sideEffects: ['Taquicardia', 'Sequedad de boca', 'Retención urinaria', 'Midriasis'],
|
||||||
|
antidote: 'Fisostigmina',
|
||||||
|
notes: [
|
||||||
|
'Dosis <0.5mg pueden causar bradicardia paradójica',
|
||||||
|
'Ya NO se usa en PCR según guías actuales',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'midazolam',
|
||||||
|
genericName: 'Midazolam',
|
||||||
|
tradeName: 'Dormicum®',
|
||||||
|
category: 'neurologico',
|
||||||
|
presentation: '15 mg/3 ml o 5 mg/5 ml (ampolla)',
|
||||||
|
adultDose: 'Sedación: 1-2.5mg IV titulando | Crisis: 10mg IM/Intranasal',
|
||||||
|
pediatricDose: 'Crisis: 0.2-0.3 mg/kg intranasal/bucal (máx 10mg)',
|
||||||
|
routes: ['IV', 'IM', 'Nasal', 'Rectal'],
|
||||||
|
dilution: 'Puede administrarse sin diluir IV lento',
|
||||||
|
indications: [
|
||||||
|
'Status epiléptico',
|
||||||
|
'Sedación para procedimientos',
|
||||||
|
'Premedicación anestésica',
|
||||||
|
'Agitación severa',
|
||||||
|
],
|
||||||
|
contraindications: [
|
||||||
|
'Miastenia gravis',
|
||||||
|
'Insuficiencia respiratoria severa',
|
||||||
|
'Shock',
|
||||||
|
],
|
||||||
|
sideEffects: ['Depresión respiratoria', 'Hipotensión', 'Amnesia'],
|
||||||
|
antidote: 'Flumazenilo',
|
||||||
|
notes: [
|
||||||
|
'En crisis: vía intranasal con atomizador (MAD)',
|
||||||
|
'Inicio de acción IV: 1-2 min, IM/IN: 5-10 min',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'salbutamol',
|
||||||
|
genericName: 'Salbutamol',
|
||||||
|
tradeName: 'Ventolin®',
|
||||||
|
category: 'respiratorio',
|
||||||
|
presentation: 'Nebulización: 5 mg/ml | MDI: 100 mcg/puff',
|
||||||
|
adultDose: 'Nebulización: 2.5-5mg | MDI: 4-8 puffs con cámara',
|
||||||
|
pediatricDose: 'Nebulización: 2.5mg (<20kg) o 5mg (>20kg) | MDI: 4-6 puffs',
|
||||||
|
routes: ['Nebulizado'],
|
||||||
|
dilution: 'Diluir en 3ml de SSF para nebulizar',
|
||||||
|
indications: [
|
||||||
|
'Crisis asmática',
|
||||||
|
'EPOC reagudizado',
|
||||||
|
'Broncoespasmo',
|
||||||
|
'Hiperpotasemia (coadyuvante)',
|
||||||
|
],
|
||||||
|
contraindications: [
|
||||||
|
'Hipersensibilidad conocida',
|
||||||
|
'Usar con precaución en cardiopatía isquémica',
|
||||||
|
],
|
||||||
|
sideEffects: ['Taquicardia', 'Temblor', 'Hipopotasemia', 'Nerviosismo'],
|
||||||
|
notes: [
|
||||||
|
'En crisis grave: nebulización continua',
|
||||||
|
'Puede repetirse cada 20 min las primeras horas',
|
||||||
|
'Asociar ipratropio en crisis moderada-grave',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getDrugsByCategory = (category: DrugCategory): Drug[] => {
|
||||||
|
return drugs.filter((d) => d.category === category);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDrugById = (id: string): Drug | undefined => {
|
||||||
|
return drugs.find((d) => d.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchDrugs = (query: string): Drug[] => {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
return drugs.filter(
|
||||||
|
(d) =>
|
||||||
|
d.genericName.toLowerCase().includes(lowerQuery) ||
|
||||||
|
d.tradeName.toLowerCase().includes(lowerQuery) ||
|
||||||
|
d.indications.some((i) => i.toLowerCase().includes(lowerQuery))
|
||||||
|
);
|
||||||
|
};
|
||||||
200
src/data/procedures.ts
Normal file
200
src/data/procedures.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
export type Priority = 'critico' | 'alto' | 'medio' | 'bajo';
|
||||||
|
export type AgeGroup = 'adulto' | 'pediatrico' | 'neonatal' | 'todos';
|
||||||
|
export type Category = 'soporte_vital' | 'patologias' | 'escena';
|
||||||
|
|
||||||
|
export interface Procedure {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
shortTitle: string;
|
||||||
|
category: Category;
|
||||||
|
subcategory?: string;
|
||||||
|
priority: Priority;
|
||||||
|
ageGroup: AgeGroup;
|
||||||
|
steps: string[];
|
||||||
|
warnings: string[];
|
||||||
|
keyPoints?: string[];
|
||||||
|
equipment?: string[];
|
||||||
|
drugs?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const procedures: Procedure[] = [
|
||||||
|
{
|
||||||
|
id: 'rcp-adulto-svb',
|
||||||
|
title: 'RCP Adulto - Soporte Vital Básico',
|
||||||
|
shortTitle: 'RCP Adulto SVB',
|
||||||
|
category: 'soporte_vital',
|
||||||
|
subcategory: 'rcp',
|
||||||
|
priority: 'critico',
|
||||||
|
ageGroup: 'adulto',
|
||||||
|
steps: [
|
||||||
|
'Garantizar seguridad de la escena',
|
||||||
|
'Comprobar consciencia: estimular y preguntar "¿Se encuentra bien?"',
|
||||||
|
'Si no responde, gritar pidiendo ayuda',
|
||||||
|
'Abrir vía aérea: maniobra frente-mentón',
|
||||||
|
'Comprobar respiración: VER-OÍR-SENTIR (máx. 10 segundos)',
|
||||||
|
'Si no respira normal: llamar 112 y pedir DEA',
|
||||||
|
'Iniciar compresiones torácicas: 30 compresiones',
|
||||||
|
'Dar 2 ventilaciones de rescate',
|
||||||
|
'Continuar ciclos 30:2 sin interrupción',
|
||||||
|
'Cuando llegue DEA: encenderlo y seguir instrucciones',
|
||||||
|
],
|
||||||
|
warnings: [
|
||||||
|
'Profundidad compresiones: 5-6 cm',
|
||||||
|
'Frecuencia: 100-120 compresiones/min',
|
||||||
|
'Permitir descompresión completa',
|
||||||
|
'Minimizar interrupciones (<10 seg)',
|
||||||
|
'Cambiar reanimador cada 2 min',
|
||||||
|
],
|
||||||
|
keyPoints: [
|
||||||
|
'Compresiones de calidad salvan vidas',
|
||||||
|
'No interrumpir para pulso hasta que haya signos de vida',
|
||||||
|
'La desfibrilación precoz aumenta supervivencia',
|
||||||
|
],
|
||||||
|
equipment: ['DEA', 'Bolsa-mascarilla', 'Cánula orofaríngea'],
|
||||||
|
drugs: ['Adrenalina 1mg'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rcp-adulto-sva',
|
||||||
|
title: 'RCP Adulto - Soporte Vital Avanzado',
|
||||||
|
shortTitle: 'RCP Adulto SVA',
|
||||||
|
category: 'soporte_vital',
|
||||||
|
subcategory: 'rcp',
|
||||||
|
priority: 'critico',
|
||||||
|
ageGroup: 'adulto',
|
||||||
|
steps: [
|
||||||
|
'Continuar RCP 30:2 mientras se prepara monitorización',
|
||||||
|
'Colocar monitor/desfibrilador y analizar ritmo',
|
||||||
|
'Ritmo desfibrilable (FV/TVSP): descarga 150-200J bifásico',
|
||||||
|
'Reiniciar RCP inmediatamente 2 minutos',
|
||||||
|
'Obtener acceso IV/IO',
|
||||||
|
'Administrar adrenalina 1mg IV cada 3-5 min (tras 3ª descarga si DF)',
|
||||||
|
'Considerar amiodarona 300mg IV si FV/TVSP refractaria',
|
||||||
|
'Asegurar vía aérea avanzada cuando sea posible',
|
||||||
|
'Buscar y tratar causas reversibles (4H y 4T)',
|
||||||
|
'Si ROSC: cuidados post-parada',
|
||||||
|
],
|
||||||
|
warnings: [
|
||||||
|
'Minimizar interrupciones de compresiones',
|
||||||
|
'Adrenalina en ritmos no DF: lo antes posible',
|
||||||
|
'Amiodarona: 150mg adicionales si persiste FV/TVSP',
|
||||||
|
'Capnografía: objetivo ETCO2 >10 mmHg',
|
||||||
|
],
|
||||||
|
keyPoints: [
|
||||||
|
'4H: Hipoxia, Hipovolemia, Hipo/Hiperpotasemia, Hipotermia',
|
||||||
|
'4T: Neumotórax a Tensión, Taponamiento, Tóxicos, TEP',
|
||||||
|
],
|
||||||
|
equipment: ['Monitor/Desfibrilador', 'Material IOT', 'Acceso venoso'],
|
||||||
|
drugs: ['Adrenalina', 'Amiodarona', 'Atropina'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rcp-pediatrico',
|
||||||
|
title: 'RCP Pediátrico - SVB',
|
||||||
|
shortTitle: 'RCP Pediátrico',
|
||||||
|
category: 'soporte_vital',
|
||||||
|
subcategory: 'rcp',
|
||||||
|
priority: 'critico',
|
||||||
|
ageGroup: 'pediatrico',
|
||||||
|
steps: [
|
||||||
|
'Garantizar seguridad',
|
||||||
|
'Comprobar consciencia',
|
||||||
|
'Abrir vía aérea: maniobra frente-mentón',
|
||||||
|
'Comprobar respiración (máx. 10 segundos)',
|
||||||
|
'Dar 5 ventilaciones de rescate iniciales',
|
||||||
|
'Comprobar signos de vida/pulso (máx. 10 seg)',
|
||||||
|
'Si no hay signos de vida: 15 compresiones torácicas',
|
||||||
|
'Continuar con ciclos 15:2',
|
||||||
|
'Si está solo: RCP 1 minuto antes de llamar 112',
|
||||||
|
],
|
||||||
|
warnings: [
|
||||||
|
'Lactante (<1 año): compresiones con 2 dedos',
|
||||||
|
'Niño (1-8 años): talón de una mano',
|
||||||
|
'Profundidad: 1/3 del tórax (4cm lactante, 5cm niño)',
|
||||||
|
'Frecuencia: 100-120/min',
|
||||||
|
],
|
||||||
|
keyPoints: [
|
||||||
|
'La causa más frecuente es respiratoria',
|
||||||
|
'Las 5 ventilaciones iniciales son cruciales',
|
||||||
|
'Ratio 15:2 para profesionales',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'obstruccion-via-aerea',
|
||||||
|
title: 'Obstrucción de Vía Aérea - OVACE',
|
||||||
|
shortTitle: 'OVACE',
|
||||||
|
category: 'soporte_vital',
|
||||||
|
subcategory: 'via_aerea',
|
||||||
|
priority: 'critico',
|
||||||
|
ageGroup: 'todos',
|
||||||
|
steps: [
|
||||||
|
'Valorar gravedad: ¿Puede toser, hablar, respirar?',
|
||||||
|
'OBSTRUCCIÓN LEVE: animar a toser, vigilar',
|
||||||
|
'OBSTRUCCIÓN GRAVE consciente: 5 golpes interescapulares',
|
||||||
|
'Si no se resuelve: 5 compresiones abdominales (Heimlich)',
|
||||||
|
'Alternar 5 golpes + 5 compresiones hasta resolución',
|
||||||
|
'Si pierde consciencia: iniciar RCP',
|
||||||
|
'Antes de ventilar: revisar boca y extraer objeto visible',
|
||||||
|
],
|
||||||
|
warnings: [
|
||||||
|
'En embarazadas y obesos: compresiones torácicas',
|
||||||
|
'Lactantes: 5 golpes en espalda + 5 compresiones torácicas',
|
||||||
|
'NO hacer barrido digital a ciegas',
|
||||||
|
'Derivar siempre tras maniobras de Heimlich',
|
||||||
|
],
|
||||||
|
keyPoints: [
|
||||||
|
'La tos es el mecanismo más efectivo',
|
||||||
|
'No interferir si la tos es efectiva',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shock-hemorragico',
|
||||||
|
title: 'Shock Hemorrágico',
|
||||||
|
shortTitle: 'Shock Hemorrágico',
|
||||||
|
category: 'soporte_vital',
|
||||||
|
subcategory: 'shock',
|
||||||
|
priority: 'critico',
|
||||||
|
ageGroup: 'adulto',
|
||||||
|
steps: [
|
||||||
|
'Control de hemorragia externa: presión directa',
|
||||||
|
'Torniquete si hemorragia en extremidad no controlable',
|
||||||
|
'Oxigenoterapia alto flujo',
|
||||||
|
'Canalizar 2 vías IV gruesas (14-16G)',
|
||||||
|
'Fluidos: cristaloides tibios (objetivo TAS 80-90 mmHg)',
|
||||||
|
'Posición Trendelenburg si no hay TCE',
|
||||||
|
'Evitar hipotermia: mantas térmicas',
|
||||||
|
'Traslado urgente a hospital útil',
|
||||||
|
'Considerar ácido tranexámico 1g IV',
|
||||||
|
],
|
||||||
|
warnings: [
|
||||||
|
'Hipotensión permisiva: TAS 80-90 mmHg',
|
||||||
|
'Excepto en TCE: mantener TAS >90 mmHg',
|
||||||
|
'Evitar sobrecarga de fluidos',
|
||||||
|
'Torniquete: anotar hora de colocación',
|
||||||
|
],
|
||||||
|
keyPoints: [
|
||||||
|
'Clase I: <15% pérdida, FC normal, TA normal',
|
||||||
|
'Clase II: 15-30%, taquicardia, TA normal',
|
||||||
|
'Clase III: 30-40%, taquicardia, hipotensión',
|
||||||
|
'Clase IV: >40%, bradicardia, shock severo',
|
||||||
|
],
|
||||||
|
equipment: ['Torniquete', 'Agentes hemostáticos', 'Mantas térmicas'],
|
||||||
|
drugs: ['Ácido tranexámico', 'Cristaloides'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getProceduresByCategory = (category: Category): Procedure[] => {
|
||||||
|
return procedures.filter((p) => p.category === category);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProcedureById = (id: string): Procedure | undefined => {
|
||||||
|
return procedures.find((p) => p.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchProcedures = (query: string): Procedure[] => {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
return procedures.filter(
|
||||||
|
(p) =>
|
||||||
|
p.title.toLowerCase().includes(lowerQuery) ||
|
||||||
|
p.shortTitle.toLowerCase().includes(lowerQuery) ||
|
||||||
|
p.steps.some((s) => s.toLowerCase().includes(lowerQuery))
|
||||||
|
);
|
||||||
|
};
|
||||||
257
src/index.css
257
src/index.css
|
|
@ -2,95 +2,95 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
/* EMERGES TES - Design System for Emergency Medical Technicians */
|
||||||
All colors MUST be HSL.
|
/* Dark theme optimized for ambulance night use */
|
||||||
*/
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap');
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
/* Dark theme by default - optimized for night use */
|
||||||
--foreground: 222.2 84% 4.9%;
|
--background: 220 20% 12%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 220 18% 16%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 220 18% 14%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
/* Emergency Red - Primary action color */
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary: 0 84% 50%;
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
/* Medical Blue - Secondary elements */
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary: 217 91% 52%;
|
||||||
|
--secondary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 220 16% 22%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 220 10% 65%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 220 16% 24%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84% 50%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 220 16% 26%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 220 16% 20%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 0 84% 50%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.75rem;
|
||||||
|
|
||||||
--sidebar-background: 0 0% 98%;
|
/* Custom emergency colors */
|
||||||
|
--emergency-critical: 0 84% 50%;
|
||||||
|
--emergency-high: 25 95% 53%;
|
||||||
|
--emergency-medium: 45 93% 47%;
|
||||||
|
--emergency-low: 142 71% 45%;
|
||||||
|
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
/* Functional colors */
|
||||||
|
--success: 142 71% 45%;
|
||||||
|
--warning: 45 93% 47%;
|
||||||
|
--info: 217 91% 52%;
|
||||||
|
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--sidebar-background: 220 20% 10%;
|
||||||
|
--sidebar-foreground: 0 0% 98%;
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--sidebar-primary: 0 84% 50%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
--sidebar-accent: 220 16% 18%;
|
||||||
|
--sidebar-accent-foreground: 0 0% 98%;
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--sidebar-border: 220 16% 22%;
|
||||||
|
--sidebar-ring: 0 84% 50%;
|
||||||
--sidebar-border: 220 13% 91%;
|
|
||||||
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.light {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 220 20% 12%;
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: 220 20% 12%;
|
||||||
|
|
||||||
--popover: 222.2 84% 4.9%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 220 20% 12%;
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--primary: 0 84% 50%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 217 91% 52%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: 220 14% 96%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: 220 10% 40%;
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: 220 14% 96%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 220 20% 12%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 84% 50%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: 220 13% 91%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 220 13% 91%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 0 84% 50%;
|
||||||
--sidebar-background: 240 5.9% 10%;
|
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,7 +99,134 @@ All colors MUST be HSL.
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground antialiased;
|
||||||
|
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch-friendly for gloved hands */
|
||||||
|
button, a, input, select, textarea {
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High contrast focus states */
|
||||||
|
*:focus-visible {
|
||||||
|
@apply outline-none ring-2 ring-primary ring-offset-2 ring-offset-background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* Emergency button variants */
|
||||||
|
.btn-emergency {
|
||||||
|
@apply min-h-[60px] font-semibold text-lg rounded-lg transition-all active:scale-95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-emergency-critical {
|
||||||
|
@apply bg-[hsl(var(--emergency-critical))] text-white hover:bg-[hsl(var(--emergency-critical))]/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-emergency-high {
|
||||||
|
@apply bg-[hsl(var(--emergency-high))] text-white hover:bg-[hsl(var(--emergency-high))]/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-emergency-medium {
|
||||||
|
@apply bg-[hsl(var(--emergency-medium))] text-black hover:bg-[hsl(var(--emergency-medium))]/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge priorities */
|
||||||
|
.badge-critical {
|
||||||
|
@apply bg-[hsl(var(--emergency-critical))]/20 text-[hsl(var(--emergency-critical))] border border-[hsl(var(--emergency-critical))]/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-high {
|
||||||
|
@apply bg-[hsl(var(--emergency-high))]/20 text-[hsl(var(--emergency-high))] border border-[hsl(var(--emergency-high))]/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-medium {
|
||||||
|
@apply bg-[hsl(var(--emergency-medium))]/20 text-[hsl(var(--emergency-medium))] border border-[hsl(var(--emergency-medium))]/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-info {
|
||||||
|
@apply bg-[hsl(var(--info))]/20 text-[hsl(var(--info))] border border-[hsl(var(--info))]/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card styles */
|
||||||
|
.card-procedure {
|
||||||
|
@apply bg-card border border-border rounded-lg p-4 transition-colors hover:border-primary/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step numbers */
|
||||||
|
.step-number {
|
||||||
|
@apply w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold text-sm flex-shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning box */
|
||||||
|
.warning-box {
|
||||||
|
@apply bg-[hsl(var(--emergency-medium))]/10 border-l-4 border-[hsl(var(--emergency-medium))] p-3 rounded-r-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bottom navigation */
|
||||||
|
.bottom-nav {
|
||||||
|
@apply fixed bottom-0 left-0 right-0 bg-card border-t border-border z-50;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item {
|
||||||
|
@apply flex flex-col items-center justify-center py-2 px-1 min-h-[64px] text-muted-foreground transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item.active {
|
||||||
|
@apply text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item:hover {
|
||||||
|
@apply text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
/* Safe area padding for mobile */
|
||||||
|
.pb-safe {
|
||||||
|
padding-bottom: calc(80px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch-friendly scrolling */
|
||||||
|
.scroll-touch {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar but keep functionality */
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles for protocols */
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background: white !important;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav,
|
||||||
|
header,
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-procedure {
|
||||||
|
break-inside: avoid;
|
||||||
|
border: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
290
src/pages/Escena.tsx
Normal file
290
src/pages/Escena.tsx
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Shield, Activity, Users, Move, Truck, Check, Square } from 'lucide-react';
|
||||||
|
import Badge from '@/components/shared/Badge';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'seguridad', label: 'Seguridad', icon: Shield },
|
||||||
|
{ id: 'abcde', label: 'ABCDE', icon: Activity },
|
||||||
|
{ id: 'triage', label: 'Triage', icon: Users },
|
||||||
|
{ id: 'inmovilizacion', label: 'Inmovil.', icon: Move },
|
||||||
|
{ id: 'extricacion', label: 'Extric.', icon: Truck },
|
||||||
|
];
|
||||||
|
|
||||||
|
const seguridadChecklist = [
|
||||||
|
'Valorar mecanismo lesional',
|
||||||
|
'Identificar riesgos: tráfico, fuego, químicos, electricidad',
|
||||||
|
'Usar EPI adecuado',
|
||||||
|
'Señalizar/balizar la zona',
|
||||||
|
'Solicitar recursos si es necesario',
|
||||||
|
'Establecer zona de seguridad',
|
||||||
|
'Acceso seguro a la víctima',
|
||||||
|
];
|
||||||
|
|
||||||
|
const abcdeContent = [
|
||||||
|
{
|
||||||
|
letter: 'A',
|
||||||
|
title: 'Airway - Vía Aérea',
|
||||||
|
points: [
|
||||||
|
'Permeabilidad vía aérea',
|
||||||
|
'Control cervical si trauma',
|
||||||
|
'Aspirar secreciones',
|
||||||
|
'Cánula orofaríngea si inconsciente',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
letter: 'B',
|
||||||
|
title: 'Breathing - Respiración',
|
||||||
|
points: [
|
||||||
|
'FR, profundidad, simetría',
|
||||||
|
'SpO2',
|
||||||
|
'Auscultación',
|
||||||
|
'Oxigenoterapia si precisa',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
letter: 'C',
|
||||||
|
title: 'Circulation - Circulación',
|
||||||
|
points: [
|
||||||
|
'FC, TA, relleno capilar',
|
||||||
|
'Control de hemorragias',
|
||||||
|
'Acceso venoso',
|
||||||
|
'Fluidoterapia si shock',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
letter: 'D',
|
||||||
|
title: 'Disability - Neurológico',
|
||||||
|
points: [
|
||||||
|
'Nivel consciencia (AVDN/Glasgow)',
|
||||||
|
'Pupilas',
|
||||||
|
'Glucemia',
|
||||||
|
'Movilidad extremidades',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
letter: 'E',
|
||||||
|
title: 'Exposure - Exposición',
|
||||||
|
points: [
|
||||||
|
'Desvestir para explorar',
|
||||||
|
'Prevenir hipotermia',
|
||||||
|
'Inspección completa',
|
||||||
|
'Buscar lesiones ocultas',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const triageStart = [
|
||||||
|
{ color: 'Negro', criteria: 'No respira tras apertura vía aérea', action: 'Fallecido / Expectante', colorClass: 'bg-foreground text-background' },
|
||||||
|
{ color: 'Rojo', criteria: 'FR >30 o <10, TRC >2s, no obedece órdenes', action: 'Prioridad 1 - Inmediato', colorClass: 'bg-primary text-primary-foreground' },
|
||||||
|
{ color: 'Amarillo', criteria: 'No puede caminar, pero estable', action: 'Prioridad 2 - Urgente', colorClass: 'bg-warning text-background' },
|
||||||
|
{ color: 'Verde', criteria: 'Puede caminar', action: 'Prioridad 3 - Demorado', colorClass: 'bg-success text-background' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const Escena = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('seguridad');
|
||||||
|
const [checkedItems, setCheckedItems] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
|
const toggleCheck = (index: number) => {
|
||||||
|
const newChecked = new Set(checkedItems);
|
||||||
|
if (newChecked.has(index)) {
|
||||||
|
newChecked.delete(index);
|
||||||
|
} else {
|
||||||
|
newChecked.add(index);
|
||||||
|
}
|
||||||
|
setCheckedItems(newChecked);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-foreground mb-1">
|
||||||
|
Actuación en Escena
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Seguridad, valoración y triage
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'bg-secondary text-secondary-foreground'
|
||||||
|
: 'bg-muted text-muted-foreground hover:bg-accent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{activeTab === 'seguridad' && (
|
||||||
|
<div className="card-procedure">
|
||||||
|
<h3 className="font-bold text-foreground text-lg mb-4">
|
||||||
|
🛡️ Checklist Seguridad Escena
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{seguridadChecklist.map((item, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => toggleCheck(index)}
|
||||||
|
className="w-full flex items-center gap-3 p-3 rounded-lg bg-muted hover:bg-accent transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-6 h-6 rounded border-2 flex items-center justify-center transition-colors ${
|
||||||
|
checkedItems.has(index)
|
||||||
|
? 'bg-success border-success'
|
||||||
|
: 'border-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{checkedItems.has(index) && (
|
||||||
|
<Check className="w-4 h-4 text-background" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-foreground ${
|
||||||
|
checkedItems.has(index) ? 'line-through opacity-60' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCheckedItems(new Set())}
|
||||||
|
className="mt-4 text-sm text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
Reiniciar checklist
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'abcde' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{abcdeContent.map((section) => (
|
||||||
|
<div key={section.letter} className="card-procedure">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<span className="step-number text-lg">{section.letter}</span>
|
||||||
|
<h3 className="font-bold text-foreground">{section.title}</h3>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2 ml-11">
|
||||||
|
{section.points.map((point, i) => (
|
||||||
|
<li key={i} className="text-foreground flex items-start gap-2">
|
||||||
|
<span className="text-primary">•</span>
|
||||||
|
<span>{point}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'triage' && (
|
||||||
|
<div className="card-procedure">
|
||||||
|
<h3 className="font-bold text-foreground text-lg mb-4">
|
||||||
|
🏥 Triage START (Adultos)
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{triageStart.map((level) => (
|
||||||
|
<div
|
||||||
|
key={level.color}
|
||||||
|
className="flex items-stretch gap-3 rounded-lg overflow-hidden border border-border"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-20 flex items-center justify-center font-bold text-sm ${level.colorClass}`}
|
||||||
|
>
|
||||||
|
{level.color}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-3">
|
||||||
|
<p className="text-foreground text-sm font-medium">
|
||||||
|
{level.criteria}
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-2xs mt-1">
|
||||||
|
{level.action}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-2xs text-muted-foreground mt-4">
|
||||||
|
JumpSTART para pediátricos: ajustar FR (15-45 normal en lactantes)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'inmovilizacion' && (
|
||||||
|
<div className="card-procedure">
|
||||||
|
<h3 className="font-bold text-foreground text-lg mb-4">
|
||||||
|
🦴 Inmovilización
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-foreground mb-2">
|
||||||
|
Indicaciones de inmovilización espinal
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-1 text-foreground text-sm">
|
||||||
|
<li>• Mecanismo lesional de riesgo</li>
|
||||||
|
<li>• Dolor cervical o dorsolumbar</li>
|
||||||
|
<li>• Déficit neurológico</li>
|
||||||
|
<li>• Alteración nivel consciencia</li>
|
||||||
|
<li>• Intoxicación que impide valoración</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-foreground mb-2">Material</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="default">Collarín cervical</Badge>
|
||||||
|
<Badge variant="default">Tabla espinal</Badge>
|
||||||
|
<Badge variant="default">Dama de Elche</Badge>
|
||||||
|
<Badge variant="default">Férula de tracción</Badge>
|
||||||
|
<Badge variant="default">Férulas de vacío</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'extricacion' && (
|
||||||
|
<div className="card-procedure">
|
||||||
|
<h3 className="font-bold text-foreground text-lg mb-4">
|
||||||
|
🚗 Extricación Vehicular
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-2">
|
||||||
|
Maniobra de Rautek
|
||||||
|
</h4>
|
||||||
|
<ol className="space-y-2">
|
||||||
|
{[
|
||||||
|
'Aproximarse por detrás de la víctima',
|
||||||
|
'Pasar brazos bajo axilas',
|
||||||
|
'Sujetar antebrazo de víctima contra su pecho',
|
||||||
|
'Extraer arrastrando hacia atrás',
|
||||||
|
'Mantener alineación espinal',
|
||||||
|
].map((step, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-3">
|
||||||
|
<span className="step-number">{i + 1}</span>
|
||||||
|
<span className="text-foreground pt-1">{step}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Escena;
|
||||||
114
src/pages/Farmacos.tsx
Normal file
114
src/pages/Farmacos.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
import { drugs, Drug, DrugCategory } from '@/data/drugs';
|
||||||
|
import DrugCard from '@/components/drugs/DrugCard';
|
||||||
|
|
||||||
|
const categories: { id: DrugCategory | 'todos'; label: string }[] = [
|
||||||
|
{ id: 'todos', label: 'Todos' },
|
||||||
|
{ id: 'cardiovascular', label: 'Cardiovascular' },
|
||||||
|
{ id: 'respiratorio', label: 'Respiratorio' },
|
||||||
|
{ id: 'neurologico', label: 'Neurológico' },
|
||||||
|
{ id: 'analgesia', label: 'Analgesia' },
|
||||||
|
{ id: 'otros', label: 'Otros' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const Farmacos = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const highlightId = searchParams.get('id');
|
||||||
|
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [activeCategory, setActiveCategory] = useState<DrugCategory | 'todos'>('todos');
|
||||||
|
|
||||||
|
const filteredDrugs = useMemo(() => {
|
||||||
|
let result = [...drugs];
|
||||||
|
|
||||||
|
// Filter by category
|
||||||
|
if (activeCategory !== 'todos') {
|
||||||
|
result = result.filter((d) => d.category === activeCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by search
|
||||||
|
if (searchQuery.length >= 2) {
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
result = result.filter(
|
||||||
|
(d) =>
|
||||||
|
d.genericName.toLowerCase().includes(query) ||
|
||||||
|
d.tradeName.toLowerCase().includes(query) ||
|
||||||
|
d.indications.some((i) => i.toLowerCase().includes(query))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort highlighted to top
|
||||||
|
if (highlightId) {
|
||||||
|
result.sort((a, b) => {
|
||||||
|
if (a.id === highlightId) return -1;
|
||||||
|
if (b.id === highlightId) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [activeCategory, searchQuery, highlightId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-foreground mb-1">Fármacos</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Vademécum de emergencias
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Buscar fármaco..."
|
||||||
|
className="w-full h-12 pl-12 pr-4 bg-card border border-border rounded-xl text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Tabs */}
|
||||||
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => setActiveCategory(cat.id)}
|
||||||
|
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
|
||||||
|
activeCategory === cat.id
|
||||||
|
? 'bg-secondary text-secondary-foreground'
|
||||||
|
: 'bg-muted text-muted-foreground hover:bg-accent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Drugs List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredDrugs.map((drug) => (
|
||||||
|
<DrugCard
|
||||||
|
key={drug.id}
|
||||||
|
drug={drug}
|
||||||
|
defaultExpanded={drug.id === highlightId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredDrugs.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
No se encontraron fármacos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Farmacos;
|
||||||
137
src/pages/Herramientas.tsx
Normal file
137
src/pages/Herramientas.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Calculator, Table, AlertCircle } from 'lucide-react';
|
||||||
|
import GlasgowCalculator from '@/components/tools/GlasgowCalculator';
|
||||||
|
import InfusionTableView from '@/components/tools/InfusionTableView';
|
||||||
|
import { infusionTables } from '@/data/calculators';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'calculadoras', label: 'Calculadoras', icon: Calculator },
|
||||||
|
{ id: 'perfusiones', label: 'Perfusiones', icon: Table },
|
||||||
|
{ id: 'codigos', label: 'Códigos', icon: AlertCircle },
|
||||||
|
];
|
||||||
|
|
||||||
|
const codigosProtocolo = [
|
||||||
|
{
|
||||||
|
name: 'Código Ictus',
|
||||||
|
description: 'Activación ante sospecha de ictus agudo',
|
||||||
|
path: '/patologias?tab=neurologicas',
|
||||||
|
color: 'bg-secondary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Código IAM',
|
||||||
|
description: 'SCACEST - Infarto con elevación ST',
|
||||||
|
path: '/patologias?tab=circulatorias',
|
||||||
|
color: 'bg-primary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Código Sepsis',
|
||||||
|
description: 'Sospecha de sepsis severa / shock séptico',
|
||||||
|
path: '/soporte-vital',
|
||||||
|
color: 'bg-emergency-high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Código Parada',
|
||||||
|
description: 'PCR - Parada cardiorrespiratoria',
|
||||||
|
path: '/soporte-vital?id=rcp-adulto-svb',
|
||||||
|
color: 'bg-primary',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Herramientas = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('calculadoras');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-foreground mb-1">Herramientas</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Calculadoras, tablas y códigos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'bg-secondary text-secondary-foreground'
|
||||||
|
: 'bg-muted text-muted-foreground hover:bg-accent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{activeTab === 'calculadoras' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<GlasgowCalculator />
|
||||||
|
|
||||||
|
{/* Placeholder for more calculators */}
|
||||||
|
<div className="card-procedure opacity-60">
|
||||||
|
<h3 className="font-bold text-foreground text-lg mb-2">
|
||||||
|
🔥 Fórmula de Parkland (Quemados)
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Próximamente disponible
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-procedure opacity-60">
|
||||||
|
<h3 className="font-bold text-foreground text-lg mb-2">
|
||||||
|
⚖️ Dosis Pediátricas por Peso
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Próximamente disponible
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'perfusiones' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{infusionTables.map((table) => (
|
||||||
|
<InfusionTableView key={table.id} table={table} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'codigos' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{codigosProtocolo.map((codigo) => (
|
||||||
|
<Link
|
||||||
|
key={codigo.name}
|
||||||
|
to={codigo.path}
|
||||||
|
className="block card-procedure hover:border-primary/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
className={`w-12 h-12 rounded-lg ${codigo.color} flex items-center justify-center`}
|
||||||
|
>
|
||||||
|
<AlertCircle className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-foreground">{codigo.name}</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{codigo.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Herramientas;
|
||||||
|
|
@ -1,14 +1,140 @@
|
||||||
// Update this page (the content is just a fallback if you fail to update the page)
|
import { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Heart,
|
||||||
|
Brain,
|
||||||
|
Zap,
|
||||||
|
Wind,
|
||||||
|
Clock,
|
||||||
|
ChevronRight,
|
||||||
|
AlertTriangle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import EmergencyButton from '@/components/shared/EmergencyButton';
|
||||||
|
|
||||||
const Index = () => {
|
const recentSearches = [
|
||||||
|
{ id: 'rcp-adulto-svb', title: 'RCP Adulto SVB', type: 'procedure' },
|
||||||
|
{ id: 'adrenalina', title: 'Adrenalina', type: 'drug' },
|
||||||
|
{ id: 'shock-hemorragico', title: 'Shock Hemorrágico', type: 'procedure' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const quickAccess = [
|
||||||
|
{ label: 'OVACE', path: '/soporte-vital?id=obstruccion-via-aerea' },
|
||||||
|
{ label: 'Glasgow', path: '/herramientas' },
|
||||||
|
{ label: 'Triage', path: '/escena' },
|
||||||
|
{ label: 'Código Ictus', path: '/patologias' },
|
||||||
|
{ label: 'Dopamina', path: '/herramientas' },
|
||||||
|
{ label: 'Politrauma', path: '/soporte-vital' },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface HomeProps {
|
||||||
|
onSearchClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Home = ({ onSearchClick }: HomeProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
<div className="space-y-6">
|
||||||
<div className="text-center">
|
{/* Search Bar */}
|
||||||
<h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1>
|
<button
|
||||||
<p className="text-xl text-muted-foreground">Start building your amazing project here!</p>
|
onClick={onSearchClick}
|
||||||
</div>
|
className="w-full flex items-center gap-4 p-4 bg-card border border-border rounded-xl hover:border-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
<Search className="w-6 h-6 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Buscar protocolo, fármaco, calculadora...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Emergency Buttons Grid */}
|
||||||
|
<section>
|
||||||
|
<h2 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-3">
|
||||||
|
Emergencias Críticas
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<EmergencyButton
|
||||||
|
to="/soporte-vital?id=rcp-adulto-svb"
|
||||||
|
icon={Heart}
|
||||||
|
title="RCP / Parada"
|
||||||
|
subtitle="Adulto y Pediátrico"
|
||||||
|
variant="critical"
|
||||||
|
/>
|
||||||
|
<EmergencyButton
|
||||||
|
to="/patologias?tab=neurologicas"
|
||||||
|
icon={Brain}
|
||||||
|
title="Código Ictus"
|
||||||
|
variant="high"
|
||||||
|
/>
|
||||||
|
<EmergencyButton
|
||||||
|
to="/soporte-vital?id=shock-hemorragico"
|
||||||
|
icon={Zap}
|
||||||
|
title="Shock"
|
||||||
|
subtitle="Hemorrágico"
|
||||||
|
variant="medium"
|
||||||
|
/>
|
||||||
|
<EmergencyButton
|
||||||
|
to="/soporte-vital?id=obstruccion-via-aerea"
|
||||||
|
icon={Wind}
|
||||||
|
title="Vía Aérea"
|
||||||
|
subtitle="OVACE / IOT"
|
||||||
|
variant="critical"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Quick Access Chips */}
|
||||||
|
<section>
|
||||||
|
<h2 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-3">
|
||||||
|
Accesos Rápidos
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{quickAccess.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.label}
|
||||||
|
to={item.path}
|
||||||
|
className="px-4 py-2 bg-muted hover:bg-accent text-foreground rounded-full text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Recent Searches */}
|
||||||
|
<section>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h2 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide">
|
||||||
|
Últimas Consultas
|
||||||
|
</h2>
|
||||||
|
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{recentSearches.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
to={
|
||||||
|
item.type === 'procedure'
|
||||||
|
? `/soporte-vital?id=${item.id}`
|
||||||
|
: `/farmacos?id=${item.id}`
|
||||||
|
}
|
||||||
|
className="flex items-center justify-between p-3 bg-card border border-border rounded-lg hover:border-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-foreground">{item.title}</span>
|
||||||
|
<ChevronRight className="w-5 h-5 text-muted-foreground" />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Floating Emergency Button */}
|
||||||
|
<Link
|
||||||
|
to="/soporte-vital?id=rcp-adulto-svb"
|
||||||
|
className="fixed bottom-24 right-4 z-40 w-16 h-16 rounded-full bg-primary flex items-center justify-center shadow-lg animate-pulse-ring"
|
||||||
|
aria-label="Emergencia - RCP"
|
||||||
|
>
|
||||||
|
<AlertTriangle className="w-8 h-8 text-primary-foreground" />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Index;
|
export default Home;
|
||||||
|
|
|
||||||
147
src/pages/Patologias.tsx
Normal file
147
src/pages/Patologias.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Wind, Heart, Brain, FlaskConical, Skull } from 'lucide-react';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'respiratorias', label: 'Respiratorias', icon: Wind },
|
||||||
|
{ id: 'circulatorias', label: 'Circulatorias', icon: Heart },
|
||||||
|
{ id: 'neurologicas', label: 'Neurológicas', icon: Brain },
|
||||||
|
{ id: 'endocrinas', label: 'Endocrinas', icon: FlaskConical },
|
||||||
|
{ id: 'intoxicaciones', label: 'Intoxicaciones', icon: Skull },
|
||||||
|
];
|
||||||
|
|
||||||
|
const patologias = {
|
||||||
|
respiratorias: [
|
||||||
|
{
|
||||||
|
title: 'Crisis Asmática',
|
||||||
|
clinica: 'Disnea, sibilancias, uso de musculatura accesoria, habla entrecortada',
|
||||||
|
actuacion: ['O2 alto flujo', 'Salbutamol nebulizado', 'Corticoides IV', 'Valorar adrenalina si severa'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'EPOC Reagudizado',
|
||||||
|
clinica: 'Aumento de disnea, cambio en esputo, aumento de tos',
|
||||||
|
actuacion: ['O2 controlado (SpO2 88-92%)', 'Broncodilatadores', 'Corticoides', 'ATB si sospecha infección'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
circulatorias: [
|
||||||
|
{
|
||||||
|
title: 'SCA / IAM',
|
||||||
|
clinica: 'Dolor torácico opresivo, irradiado a brazo/mandíbula, sudoración, náuseas',
|
||||||
|
actuacion: ['ECG 12 derivaciones', 'O2 si SpO2 <94%', 'AAS 300mg', 'Nitroglicerina SL', 'Morfina si dolor intenso'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Edema Agudo de Pulmón',
|
||||||
|
clinica: 'Disnea súbita, ortopnea, esputo rosado, crepitantes',
|
||||||
|
actuacion: ['Posición semisentado', 'O2 alto flujo / CPAP', 'Furosemida IV', 'Nitroglicerina', 'Morfina'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
neurologicas: [
|
||||||
|
{
|
||||||
|
title: 'Ictus - Código Ictus',
|
||||||
|
clinica: 'Déficit neurológico súbito: paresia facial, debilidad brazo, alteración habla (FAST)',
|
||||||
|
actuacion: ['Hora de inicio síntomas', 'Glucemia', 'TA (no bajar si <220/120)', 'Código Ictus', 'Traslado urgente'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Crisis Convulsiva',
|
||||||
|
clinica: 'Movimientos tónico-clónicos, pérdida consciencia, relajación esfínteres',
|
||||||
|
actuacion: ['Proteger de traumatismos', 'Posición lateral si cede', 'Midazolam si >5min', 'O2', 'Glucemia'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
endocrinas: [
|
||||||
|
{
|
||||||
|
title: 'Hipoglucemia',
|
||||||
|
clinica: 'Glucemia <70 mg/dl, sudoración, temblor, confusión, taquicardia',
|
||||||
|
actuacion: ['Si consciente: glucosa oral', 'Si inconsciente: Glucosmon IV', 'Glucagón IM si no vía', 'Buscar causa'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Cetoacidosis Diabética',
|
||||||
|
clinica: 'Hiperglucemia >250, aliento cetósico, náuseas, dolor abdominal',
|
||||||
|
actuacion: ['Fluidoterapia SSF', 'Insulina rápida', 'Monitorización K+', 'Buscar desencadenante'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
intoxicaciones: [
|
||||||
|
{
|
||||||
|
title: 'Intoxicación por Opioides',
|
||||||
|
clinica: 'Miosis puntiforme, depresión respiratoria, bajo nivel consciencia',
|
||||||
|
actuacion: ['Vía aérea', 'Ventilación', 'Naloxona 0.4-2mg IV', 'Repetir cada 2-3 min si precisa'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Intoxicación por Benzodiacepinas',
|
||||||
|
clinica: 'Somnolencia, ataxia, habla farfullante, depresión respiratoria',
|
||||||
|
actuacion: ['Vía aérea', 'Flumazenilo 0.2mg IV', 'Repetir hasta 1mg', 'Monitorización prolongada'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Patologias = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('respiratorias');
|
||||||
|
|
||||||
|
const currentPatologias = patologias[activeTab as keyof typeof patologias] || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-foreground mb-1">Patologías</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Clínica y actuación por sistemas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* System Tabs */}
|
||||||
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'bg-secondary text-secondary-foreground'
|
||||||
|
: 'bg-muted text-muted-foreground hover:bg-accent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Patologies List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{currentPatologias.map((patologia, index) => (
|
||||||
|
<div key={index} className="card-procedure">
|
||||||
|
<h3 className="font-bold text-foreground text-lg mb-3">
|
||||||
|
{patologia.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||||
|
Clínica
|
||||||
|
</h4>
|
||||||
|
<p className="text-foreground">{patologia.clinica}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||||
|
Actuación
|
||||||
|
</h4>
|
||||||
|
<ol className="space-y-2">
|
||||||
|
{patologia.actuacion.map((paso, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-3">
|
||||||
|
<span className="step-number">{i + 1}</span>
|
||||||
|
<span className="text-foreground pt-1">{paso}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Patologias;
|
||||||
80
src/pages/SoporteVital.tsx
Normal file
80
src/pages/SoporteVital.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { procedures, getProceduresByCategory, Procedure } from '@/data/procedures';
|
||||||
|
import ProcedureCard from '@/components/procedures/ProcedureCard';
|
||||||
|
|
||||||
|
const subcategories = [
|
||||||
|
{ id: 'todos', label: 'Todos' },
|
||||||
|
{ id: 'rcp', label: 'RCP' },
|
||||||
|
{ id: 'via_aerea', label: 'Vía Aérea' },
|
||||||
|
{ id: 'shock', label: 'Shock' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SoporteVital = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const highlightId = searchParams.get('id');
|
||||||
|
const [activeTab, setActiveTab] = useState('todos');
|
||||||
|
|
||||||
|
const soporteVitalProcedures = getProceduresByCategory('soporte_vital');
|
||||||
|
|
||||||
|
const filteredProcedures =
|
||||||
|
activeTab === 'todos'
|
||||||
|
? soporteVitalProcedures
|
||||||
|
: soporteVitalProcedures.filter((p) => p.subcategory === activeTab);
|
||||||
|
|
||||||
|
// Sort to put highlighted procedure first
|
||||||
|
const sortedProcedures = [...filteredProcedures].sort((a, b) => {
|
||||||
|
if (a.id === highlightId) return -1;
|
||||||
|
if (b.id === highlightId) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-foreground mb-1">Soporte Vital</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Protocolos de emergencia y reanimación
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subcategory Tabs */}
|
||||||
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
|
||||||
|
{subcategories.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-muted text-muted-foreground hover:bg-accent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Procedures List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{sortedProcedures.map((procedure) => (
|
||||||
|
<ProcedureCard
|
||||||
|
key={procedure.id}
|
||||||
|
procedure={procedure}
|
||||||
|
defaultExpanded={procedure.id === highlightId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sortedProcedures.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
No hay protocolos en esta categoría
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SoporteVital;
|
||||||
|
|
@ -7,7 +7,7 @@ export default {
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
padding: "2rem",
|
padding: "1rem",
|
||||||
screens: {
|
screens: {
|
||||||
"2xl": "1400px",
|
"2xl": "1400px",
|
||||||
},
|
},
|
||||||
|
|
@ -57,33 +57,58 @@ export default {
|
||||||
border: "hsl(var(--sidebar-border))",
|
border: "hsl(var(--sidebar-border))",
|
||||||
ring: "hsl(var(--sidebar-ring))",
|
ring: "hsl(var(--sidebar-ring))",
|
||||||
},
|
},
|
||||||
|
emergency: {
|
||||||
|
critical: "hsl(var(--emergency-critical))",
|
||||||
|
high: "hsl(var(--emergency-high))",
|
||||||
|
medium: "hsl(var(--emergency-medium))",
|
||||||
|
low: "hsl(var(--emergency-low))",
|
||||||
|
},
|
||||||
|
success: "hsl(var(--success))",
|
||||||
|
warning: "hsl(var(--warning))",
|
||||||
|
info: "hsl(var(--info))",
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: "var(--radius)",
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: "calc(var(--radius) - 2px)",
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: "calc(var(--radius) - 4px)",
|
||||||
},
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["IBM Plex Sans", "system-ui", "-apple-system", "sans-serif"],
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
"2xs": ["0.625rem", { lineHeight: "0.875rem" }],
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
touch: "48px",
|
||||||
|
"touch-lg": "60px",
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
safe: "env(safe-area-inset-bottom)",
|
||||||
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
"accordion-down": {
|
"accordion-down": {
|
||||||
from: {
|
from: { height: "0" },
|
||||||
height: "0",
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
},
|
|
||||||
to: {
|
|
||||||
height: "var(--radix-accordion-content-height)",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"accordion-up": {
|
"accordion-up": {
|
||||||
from: {
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
height: "var(--radix-accordion-content-height)",
|
to: { height: "0" },
|
||||||
},
|
},
|
||||||
to: {
|
pulse: {
|
||||||
height: "0",
|
"0%, 100%": { opacity: "1" },
|
||||||
},
|
"50%": { opacity: "0.5" },
|
||||||
|
},
|
||||||
|
"pulse-ring": {
|
||||||
|
"0%": { transform: "scale(0.95)", boxShadow: "0 0 0 0 hsl(var(--emergency-critical) / 0.7)" },
|
||||||
|
"70%": { transform: "scale(1)", boxShadow: "0 0 0 10px hsl(var(--emergency-critical) / 0)" },
|
||||||
|
"100%": { transform: "scale(0.95)", boxShadow: "0 0 0 0 hsl(var(--emergency-critical) / 0)" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
pulse: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
||||||
|
"pulse-ring": "pulse-ring 2s infinite",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue