Add ATB tab for antibiotic treatment tracking

This commit is contained in:
2026-04-20 00:36:04 -03:00
parent e2b12a7326
commit 86be190cf0
6 changed files with 430 additions and 181 deletions
+4
View File
@@ -145,6 +145,7 @@ function AppContent() {
cultivos={store.getCultivosByPaciente(paciente.id)}
estudiosComplementarios={store.estudiosComplementarios.filter(e => e.pacienteId === paciente.id)}
interconsultas={(store.interconsultas || []).filter(ic => ic.internacionId === internacion.id)}
atb={(store.atb || []).filter(a => a.internacionId === internacion.id)}
onAgregarEvolucion={store.agregarEvolucion}
onActualizarEvolucion={store.actualizarEvolucion}
onEliminarEvolucion={store.eliminarEvolucion}
@@ -163,6 +164,9 @@ function AppContent() {
onAgregarInterconsulta={(ic) => store.agregarInterconsulta({ ...ic, internacionId: internacion.id })}
onActualizarInterconsulta={store.actualizarInterconsulta}
onEliminarInterconsulta={store.eliminarInterconsulta}
onAgregarATB={(atb) => store.agregarATB({ ...atb, internacionId: internacion.id })}
onActualizarATB={store.actualizarATB}
onEliminarATB={store.eliminarATB}
onActualizarInternacion={store.actualizarInternacion}
onActualizarCama={store.actualizarCama}
onVolver={() => store.setVista('internaciones')}
+29
View File
@@ -10,6 +10,7 @@ import type {
Cultivo,
EstudioComplementario,
Interconsulta,
ATB,
Vista
} from '@/types';
@@ -37,6 +38,7 @@ interface HospitalState {
cultivos: Cultivo[];
estudiosComplementarios: EstudioComplementario[];
interconsultas: Interconsulta[];
atb: ATB[];
vistaActual: Vista;
currentInternacionId?: string | null;
}
@@ -54,6 +56,7 @@ const defaultState = (): HospitalState => ({
cultivos: [],
estudiosComplementarios: [],
interconsultas: [],
atb: [],
vistaActual: 'dashboard',
camas: []
});
@@ -376,6 +379,29 @@ const finalizarInternacion = useCallback((internacionId: string, datos: {
}));
}, []);
const agregarATB = useCallback((atb: Omit<ATB, 'id'>) => {
const nuevoATB: ATB = { ...atb, id: generateUUID() };
setState(prev => ({
...prev,
atb: [...prev.atb, nuevoATB],
}));
return nuevoATB.id;
}, []);
const actualizarATB = useCallback((id: string, datos: Partial<ATB>) => {
setState(prev => ({
...prev,
atb: prev.atb.map(a => a.id === id ? { ...a, ...datos } : a),
}));
}, []);
const eliminarATB = useCallback((id: string) => {
setState(prev => ({
...prev,
atb: prev.atb.filter(a => a.id !== id),
}));
}, []);
// Acciones de areas
const agregarArea = useCallback((area: Omit<Area, 'id'>) => {
const nuevaArea: Area = { ...area, id: generateUUID() };
@@ -502,6 +528,9 @@ const getEstadisticas = useCallback(() => {
agregarInterconsulta,
actualizarInterconsulta,
eliminarInterconsulta,
agregarATB,
actualizarATB,
eliminarATB,
agregarArea,
actualizarArea,
eliminarArea,
+217 -38
View File
@@ -22,7 +22,7 @@ import {
FileText,
ChevronDown,
ChevronUp,
ChevronRight,
List,
FileDown,
Thermometer,
Heart,
@@ -51,7 +51,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import type { Laboratorio, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario, Interconsulta } from '@/types';
import type { Laboratorio, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario, Interconsulta, ATB } from '@/types';
import { formatDateDDMMYYYY } from '@/lib/utils';
interface HistoriaClinicaProps {
@@ -65,6 +65,7 @@ interface HistoriaClinicaProps {
cultivos: Cultivo[];
estudiosComplementarios: EstudioComplementario[];
interconsultas: Interconsulta[];
atb: ATB[];
onAgregarEvolucion: (evolucion: Omit<Evolucion, 'id'>) => void;
onActualizarEvolucion: (id: string, datos: Partial<Evolucion>) => void;
onEliminarEvolucion: (id: string) => void;
@@ -84,6 +85,9 @@ interface HistoriaClinicaProps {
onAgregarInterconsulta: (interconsulta: Omit<Interconsulta, 'id'>) => void;
onActualizarInterconsulta: (id: string, datos: Partial<Interconsulta>) => void;
onEliminarInterconsulta: (id: string) => void;
onAgregarATB: (atb: Omit<ATB, 'id'>) => void;
onActualizarATB: (id: string, datos: Partial<ATB>) => void;
onEliminarATB: (id: string) => void;
onActualizarInternacion: (id: string, datos: Partial<Internacion>) => void;
onActualizarCama: (id: string, datos: Partial<Cama>) => void;
onVolver: () => void;
@@ -192,6 +196,10 @@ export function HistoriaClinica({
onAgregarInterconsulta,
onActualizarInterconsulta,
onEliminarInterconsulta,
atb,
onAgregarATB,
onActualizarATB,
onEliminarATB,
onActualizarInternacion,
onActualizarCama,
onVolver,
@@ -473,6 +481,13 @@ export function HistoriaClinica({
<span className="hidden sm:inline">Evoluciones</span>
({evoluciones.length})
</TabsTrigger>
<TabsTrigger value="indicaciones" className="flex-shrink-0 flex items-center gap-1 text-xs sm:text-sm">
<List className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden sm:inline">Indicaciones</span>
</TabsTrigger>
<TabsTrigger value="laboratorios" className="flex-shrink-0 flex items-center gap-1 text-xs sm:text-sm">
<FlaskConical className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden sm:inline">Laboratorio</span>
@@ -488,6 +503,11 @@ export function HistoriaClinica({
<span className="hidden sm:inline">Cultivos</span>
({cultivos.length})
</TabsTrigger>
<TabsTrigger value="atb" className="flex-shrink-0 flex items-center gap-1 text-xs sm:text-sm">
<Pill className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden sm:inline">ATB</span>
({atb.length})
</TabsTrigger>
<TabsTrigger value="estudios" className="flex-shrink-0 flex items-center gap-1 text-xs sm:text-sm">
<ClipboardList className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden sm:inline">Estudios</span>
@@ -519,10 +539,14 @@ export function HistoriaClinica({
</TabsContent>
<TabsContent value="atb" className="mt-4">
<div className="text-center py-8 text-gray-400">
<Pill className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Sección ATB en desarrollo</p>
</div>
<SeccionATB
atb={atb}
internacionId={internacion.id}
pacienteId={paciente.id}
add={onAgregarATB}
update={onActualizarATB}
del={onEliminarATB}
/>
</TabsContent>
<TabsContent value="estudios" className="mt-4">
@@ -679,10 +703,9 @@ function SeccionLaboratorios({ lab, patientId, add, update, del, addAcidoBase }:
const observacionesExtra: string[] = [];
const mapeoParametros: Record<string, { nombre: string; unidad: string; esPrincipal: boolean }> = {
'hematíes': { nombre: 'Hematíes', unidad: '10⁶/µl', esPrincipal: false },
'hematocrito': { nombre: 'Hematocrito', unidad: '%', esPrincipal: true },
'hemoglobina corpuscular media': { nombre: 'HCM', unidad: 'pg', esPrincipal: false },
'concentración de hemoglobina media': { nombre: 'CHCM', unidad: 'g/dL', esPrincipal: false },
'hemoglobina': { nombre: 'Hemoglobina', unidad: 'g/dL', esPrincipal: true },
'volumen corpuscular medio': { nombre: 'VCM', unidad: 'fL', esPrincipal: false },
'rdw': { nombre: 'RDW', unidad: '%', esPrincipal: false },
@@ -1433,6 +1456,7 @@ function SeccionAcidosBase({ ab, patientId, add, update, del }: {
const [lactato, setLactato] = useState('');
const [fio2, setFio2] = useState('');
const [interpretacion, setInterpretacion] = useState('');
const [eabSortAsc, setEabSortAsc] = useState(false);
const reset = () => {
setFecha(new Date().toISOString().split('T')[0]);
@@ -1555,38 +1579,71 @@ function SeccionAcidosBase({ ab, patientId, add, update, del }: {
</DialogContent>
</Dialog>
{abFiltered.length === 0 ? <div className="text-center py-8 text-gray-400"><Activity className="h-12 w-12 mx-auto mb-2 opacity-50" /><p>No hay gasometrías</p></div> :
abFiltered.map(a => {
const phSt = getEstadoParametro('ph', a.ph);
return (
<Card key={a.id} className="mb-2"><CardContent className="p-4">
<div className="flex flex-col gap-3">
<div className="flex items-start justify-between">
<div><p className="font-medium">{a.fecha} {a.hora}</p></div>
<div className="flex items-end gap-2">
<Badge variant="outline" className="text-xs">{a.fio2 && a.po2 && <span className="mr-1">PaFiO2: {Math.round(a.po2 / a.fio2 * 100) / 100}</span>}</Badge>
<Badge variant="outline" className="text-xs">{a.fio2 && <span className="mr-1">FiO:{a.fio2}</span>}</Badge>
<Badge className={getColorPh(a.ph)}>pH: {a.ph}</Badge>
<Button size="sm" variant="ghost" className="text-blue-600" onClick={() => loadEdit(a)}><Edit /></Button>
<Button size="sm" variant="ghost" className="text-red-600" onClick={() => del(a.id)}><Trash2 /></Button>
</div>
</div>
<div className="grid grid-cols-3 sm:grid-cols-8 gap-2">
<div className="bg-gray-50 p-2 rounded text-center"><p className="text-xs text-gray-500">pH</p><p className={`font-bold ${phSt.color}`}>{a.ph}</p></div>
<div className="bg-gray-50 p-2 rounded text-center"><p className="text-xs text-gray-500">pCO2</p><p className="font-bold">{a.pco2}</p></div>
<div className="bg-gray-50 p-2 rounded text-center"><p className="text-xs text-gray-500">pO2</p><p className="font-bold">{a.po2}</p></div>
<div className="bg-gray-50 p-2 rounded text-center"><p className="text-xs text-gray-500">HCO3</p><p className="font-bold">{a.hco3}</p></div>
<div className="bg-gray-50 p-2 rounded text-center"><p className="text-xs text-gray-500">BE</p><p className="font-bold">{a.be}</p></div>
<div className="bg-gray-50 p-2 rounded text-center"><p className="text-xs text-gray-500">SatO2</p><p className="font-bold">{a.sato2}%</p></div>
{a.lactato && <div className="bg-gray-50 p-2 rounded text-center"><p className="text-xs text-gray-500">Lactato</p><p className="font-bold">{a.lactato}</p></div>}
{a.fio2 && <div className="bg-gray-50 p-2 rounded text-center"><p className="text-xs text-gray-500">FiO2</p><p className="font-bold">{a.fio2}</p></div>}
</div>
{a.interpretacion && <div className="bg-teal-50 p-3 rounded-lg border border-teal-200"><p className="text-sm font-medium text-teal-800">Interpretación: {a.interpretacion}</p></div>}
</div>
</CardContent></Card>
);
})
<div className="grid grid-cols-1 w-full">
<div className="w-full overflow-x-auto rounded-md border pb-2">
<Table className="w-full min-w-max text-sm">
<TableHeader>
<TableRow>
<TableHead className="cursor-pointer hover:bg-muted" onClick={() => setEabSortAsc(!eabSortAsc)}>
Fecha {eabSortAsc ? '↑' : '↓'}
</TableHead>
<TableHead>pH</TableHead>
<TableHead>pCO2</TableHead>
<TableHead>pO2</TableHead>
<TableHead>HCO3</TableHead>
<TableHead>BE</TableHead>
<TableHead>SatO2</TableHead>
<TableHead>Lactato</TableHead>
<TableHead>FiO2</TableHead>
<TableHead>PaFiO2</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{abFiltered.map(a => (
<TableRow key={a.id}>
<TableCell>{a.fecha} {a.hora || ''}</TableCell>
<TableCell>{a.ph}</TableCell>
<TableCell>{a.pco2}</TableCell>
<TableCell>{a.po2}</TableCell>
<TableCell>{a.hco3}</TableCell>
<TableCell>{a.be}</TableCell>
<TableCell>{a.sato2}</TableCell>
<TableCell>{a.lactato}</TableCell>
<TableCell>{a.fio2}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => loadEdit(a)}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => del(a.id)} className="text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Eliminar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
}
</div>
);
@@ -2162,4 +2219,126 @@ function SeccionInterconsultas({ interconsultas, pacienteId, internacionId, add,
)}
</div>
);
}
function SeccionATB({ atb, internacionId, pacienteId, add, update, del }: {
atb: ATB[];
internacionId: string;
pacienteId: string;
add: (a: Omit<ATB, 'id'>) => void;
update: (id: string, data: Partial<ATB>) => void;
del: (id: string) => void;
}) {
const [dialog, setDialog] = useState(false);
const [edit, setEdit] = useState<ATB | null>(null);
const [antibiotico, setAntibiotico] = useState('');
const [fechaInicio, setFechaInicio] = useState(new Date().toISOString().split('T')[0]);
const [fechaFinalizacion, setFechaFinalizacion] = useState('');
const calcularDias = (inicio: string, fin?: string): number => {
const ini = new Date(inicio);
const finDate = fin ? new Date(fin) : new Date();
const diff = finDate.getTime() - ini.getTime();
return Math.ceil(diff / (1000 * 60 * 60 * 24));
};
const reset = () => {
setEdit(null);
setAntibiotico('');
setFechaInicio(new Date().toISOString().split('T')[0]);
setFechaFinalizacion('');
};
const loadEdit = (a: ATB) => {
setEdit(a);
setAntibiotico(a.antibiotico);
setFechaInicio(a.fechaInicio);
setFechaFinalizacion(a.fechaFinalizacion || '');
};
const handleGuardar = () => {
if (!antibiotico.trim() || !fechaInicio) return;
if (edit) {
update(edit.id, { antibiotico, fechaInicio, fechaFinalizacion: fechaFinalizacion || undefined });
} else {
add({ pacienteId, internacionId, antibiotico, fechaInicio, fechaFinalizacion: fechaFinalizacion || undefined });
}
setDialog(false);
reset();
};
const atbOrdenado = atb.sort((a, b) => new Date(b.fechaInicio).getTime() - new Date(a.fechaInicio).getTime());
return (
<div className="space-y-4">
<div className="flex justify-between">
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />Nuevo
</Button>
</div>
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{edit ? 'Editar' : 'Nuevo'} Antibiótico</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Antibiótico</Label>
<Input value={antibiotico} onChange={e => setAntibiotico(e.target.value)} placeholder="Nombre del antibiótico" />
</div>
<div>
<Label>Fecha de Inicio</Label>
<Input type="date" value={fechaInicio} onChange={e => setFechaInicio(e.target.value)} />
</div>
<div>
<Label>Fecha de Finalización (opcional)</Label>
<Input type="date" value={fechaFinalizacion} onChange={e => setFechaFinalizacion(e.target.value)} />
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setDialog(false)}>
<X className="h-4 w-4 mr-2" />Cancelar
</Button>
<Button onClick={handleGuardar}>
<Save className="h-4 w-4 mr-2" />Guardar
</Button>
</div>
</DialogContent>
</Dialog>
{atbOrdenado.length === 0 ? (
<div className="text-center py-8 text-gray-500">No hay antibiotics registrados</div>
) : (
<div className="space-y-2">
{atbOrdenado.map(a => (
<Card key={a.id}>
<CardContent className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex-1">
<div className="font-medium">{a.antibiotico}</div>
<div className="text-sm text-gray-500 flex flex-wrap gap-2 mt-1">
<span>Inicio: {formatDateDDMMYYYY(a.fechaInicio)}</span>
{a.fechaFinalizacion && (
<span>Fin: {formatDateDDMMYYYY(a.fechaFinalizacion)}</span>
)}
<Badge className="bg-green-100 text-green-800">
{calcularDias(a.fechaInicio, a.fechaFinalizacion)} días
</Badge>
</div>
</div>
<div className="flex gap-1">
<Button size="sm" variant="outline" onClick={() => loadEdit(a)}>
<Pencil className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(a.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}
+9
View File
@@ -162,4 +162,13 @@ export interface Interconsulta {
respuestaFecha?: string;
}
export interface ATB {
id: string;
pacienteId: string;
internacionId: string;
antibiotico: string;
fechaInicio: string;
fechaFinalizacion?: string;
}
export type Vista = 'dashboard' | 'camas' | 'pacientes' | 'internaciones' | 'evoluciones' | 'laboratorios' | 'acidobase' | 'cultivos' | 'historiaclinica' | 'nuevoingreso' | 'editaringreso';