Changes
This commit is contained in:
parent
bebc3a2029
commit
69dacbe188
28
index.html
28
index.html
|
|
@ -1,22 +1,24 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- TODO: Set the document title to the name of your application -->
|
||||
<title>Lovable App</title>
|
||||
<meta name="description" content="Lovable Generated Project" />
|
||||
<meta name="author" content="Lovable" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<title>EMERGES TES - Guía de Protocolos de Emergencias</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="author" content="EMERGES TES" />
|
||||
<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="Lovable App" />
|
||||
<meta property="og:description" content="Lovable Generated Project" />
|
||||
<meta property="og:title" content="EMERGES TES - Guía de Protocolos" />
|
||||
<meta property="og:description" content="Guía rápida de protocolos médicos de emergencias para TES" />
|
||||
<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:site" content="@Lovable" />
|
||||
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="EMERGES TES" />
|
||||
<meta name="twitter:description" content="Protocolos de emergencias para TES" />
|
||||
|
||||
<link rel="canonical" href="https://emerges-tes.lovable.app/" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
53
src/App.tsx
53
src/App.tsx
|
|
@ -1,27 +1,72 @@
|
|||
import { useState } from 'react';
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
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";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const App = () => (
|
||||
const App = () => {
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header
|
||||
onSearchClick={() => setIsSearchOpen(true)}
|
||||
onMenuClick={() => setIsMenuOpen(true)}
|
||||
/>
|
||||
|
||||
<main className="pt-14 pb-safe">
|
||||
<div className="container max-w-2xl py-4">
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<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;
|
||||
|
|
|
|||
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 utilities;
|
||||
|
||||
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
||||
All colors MUST be HSL.
|
||||
*/
|
||||
/* EMERGES TES - Design System for Emergency Medical Technicians */
|
||||
/* 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 {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
/* Dark theme by default - optimized for night use */
|
||||
--background: 220 20% 12%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--card: 220 18% 16%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--popover: 220 18% 14%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
/* Emergency Red - Primary action color */
|
||||
--primary: 0 84% 50%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
/* Medical Blue - Secondary elements */
|
||||
--secondary: 217 91% 52%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--muted: 220 16% 22%;
|
||||
--muted-foreground: 220 10% 65%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 220 16% 24%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 84% 50%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--border: 220 16% 26%;
|
||||
--input: 220 16% 20%;
|
||||
--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-primary-foreground: 0 0% 98%;
|
||||
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--sidebar-border: 220 13% 91%;
|
||||
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
--sidebar-background: 220 20% 10%;
|
||||
--sidebar-foreground: 0 0% 98%;
|
||||
--sidebar-primary: 0 84% 50%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 220 16% 18%;
|
||||
--sidebar-accent-foreground: 0 0% 98%;
|
||||
--sidebar-border: 220 16% 22%;
|
||||
--sidebar-ring: 0 84% 50%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
.light {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 220 20% 12%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 220 20% 12%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 220 20% 12%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--primary: 0 84% 50%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--secondary: 217 91% 52%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--muted: 220 14% 96%;
|
||||
--muted-foreground: 220 10% 40%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--accent: 220 14% 96%;
|
||||
--accent-foreground: 220 20% 12%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 84% 50%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--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%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 0 84% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +99,134 @@ All colors MUST be HSL.
|
|||
@apply border-border;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1>
|
||||
<p className="text-xl text-muted-foreground">Start building your amazing project here!</p>
|
||||
<div className="space-y-6">
|
||||
{/* Search Bar */}
|
||||
<button
|
||||
onClick={onSearchClick}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
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: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
padding: "1rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
|
|
@ -57,33 +57,58 @@ export default {
|
|||
border: "hsl(var(--sidebar-border))",
|
||||
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: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
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: {
|
||||
"accordion-down": {
|
||||
from: {
|
||||
height: "0",
|
||||
},
|
||||
to: {
|
||||
height: "var(--radix-accordion-content-height)",
|
||||
},
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: {
|
||||
height: "var(--radix-accordion-content-height)",
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
to: {
|
||||
height: "0",
|
||||
pulse: {
|
||||
"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: {
|
||||
"accordion-down": "accordion-down 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