Actualizar control de glucemias con eje Y secundario para correcciones e integracion de tienda

This commit is contained in:
2026-08-09 14:54:22 +00:00
parent 113bfe50ac
commit 3dae81645f
31 changed files with 4080 additions and 627 deletions
+12 -28
View File
@@ -113,41 +113,21 @@ function AppContent() {
getPacienteById={store.getPacienteById}
/>
);
case 'cultivos': {
const internacionId = store.currentInternacionId || '';
console.log('Cultivos - currentInternacionId:', internacionId);
const internacion = store.getInternacionById(internacionId);
console.log('Cultivos - internacion:', internacion);
const paciente = internacion ? store.getPacienteById(internacion.pacienteId) : undefined;
console.log('Cultivos - paciente:', paciente);
if (!internacion) {
return (
<div className="p-4 text-center">
<p className="text-gray-500 dark:text-gray-400">Internación no encontrada (id: {internacionId})</p>
<Button onClick={() => store.setVista('internaciones')}>Volver a Internaciones</Button>
</div>
);
}
if (!paciente) {
return (
<div className="p-4 text-center">
<p className="text-gray-500 dark:text-gray-400">Paciente no encontrado para internación</p>
<Button onClick={() => store.setVista('internaciones')}>Volver a Internaciones</Button>
</div>
);
}
case 'cultivos':
return (
<Cultivos
cultivos={store.cultivos.filter(c => c.pacienteId === paciente.id)}
patient={paciente}
internacionId={internacion.id}
cultivos={store.cultivos}
pacientes={store.pacientes}
internaciones={store.internaciones}
camas={store.camas}
onAgregarCultivo={store.agregarCultivo}
onActualizarCultivo={store.actualizarCultivo}
onEliminarCultivo={store.eliminarCultivo}
canEdit={store.canEditInternacion(internacion.id)}
getPacienteById={store.getPacienteById}
getInternacionActivaByPaciente={store.getInternacionActivaByPaciente}
canEdit={true}
/>
);
}
case 'historiaclinica': {
const internacionId = store.currentInternacionId || '';
const internacion = store.getInternacionById(internacionId);
@@ -169,6 +149,7 @@ function AppContent() {
allCamas={store.camas}
evoluciones={store.getEvolucionesByInternacion(internacion.id)}
laboratorios={store.getLaboratoriosByPaciente(paciente.id)}
glucemias={store.getGlucemiasByPaciente(paciente.id)}
acidosBase={store.getAcidosBaseByPaciente(paciente.id)}
cultivos={store.getCultivosByPaciente(paciente.id)}
estudiosComplementarios={store.estudiosComplementarios.filter(e => e.pacienteId === paciente.id)}
@@ -180,6 +161,9 @@ function AppContent() {
onAgregarLaboratorio={(l) => store.agregarLaboratorio({ ...l, internacionId: internacion.id })}
onActualizarLaboratorio={store.actualizarLaboratorio}
onEliminarLaboratorio={store.eliminarLaboratorio}
onAgregarGlucemia={(g) => store.agregarGlucemia({ ...g, internacionId: internacion.id })}
onActualizarGlucemia={store.actualizarGlucemia}
onEliminarGlucemia={store.eliminarGlucemia}
onAgregarAcidoBase={(a) => store.agregarAcidoBase({ ...a, internacionId: internacion.id })}
onActualizarAcidoBase={store.actualizarAcidoBase}
onEliminarAcidoBase={store.eliminarAcidoBase}
+4 -4
View File
@@ -42,14 +42,14 @@ function CommandDialog({
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
+30 -5
View File
@@ -1,3 +1,4 @@
import { useEffect, useState } from "react"
import {
CircleCheckIcon,
InfoIcon,
@@ -10,17 +11,41 @@ import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 640)
}
checkMobile()
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [])
const effectivePosition = isMobile ? "bottom-center" : (props.position || "top-right")
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
position={effectivePosition}
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-white dark:group-[.toaster]:bg-slate-900 group-[.toaster]:text-slate-900 dark:group-[.toaster]:text-slate-100 group-[.toaster]:border-slate-300 dark:group-[.toaster]:border-slate-700 group-[.toaster]:shadow-2xl opacity-100 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-slate-300 dark:border-slate-700 shadow-2xl font-medium rounded-lg p-4",
description: "group-[.toast]:text-slate-600 dark:group-[.toast]:text-slate-400",
actionButton:
"group-[.toast]:bg-blue-600 group-[.toast]:text-white font-semibold",
cancelButton:
"group-[.toast]:bg-slate-200 group-[.toast]:text-slate-800 dark:group-[.toast]:bg-slate-800 dark:group-[.toast]:text-slate-200",
},
}}
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
success: <CircleCheckIcon className="size-5 text-emerald-600 dark:text-emerald-400" />,
info: <InfoIcon className="size-5 text-blue-600 dark:text-blue-400" />,
warning: <TriangleAlertIcon className="size-5 text-amber-600 dark:text-amber-400" />,
error: <OctagonXIcon className="size-5 text-red-600 dark:text-red-400" />,
loading: <Loader2Icon className="size-5 animate-spin text-blue-600 dark:text-blue-400" />,
}}
style={
{
+116 -33
View File
@@ -6,6 +6,7 @@ import type {
Internacion,
Evolucion,
Laboratorio,
Glucemia,
AcidoBase,
Cultivo,
EstudioComplementario,
@@ -37,6 +38,7 @@ interface HospitalState {
internaciones: Internacion[];
evoluciones: Evolucion[];
laboratorios: Laboratorio[];
glucemias: Glucemia[];
acidosBase: AcidoBase[];
cultivos: Cultivo[];
estudiosComplementarios: EstudioComplementario[];
@@ -52,7 +54,7 @@ interface HospitalState {
}
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:4000/api';
const API_BASE = import.meta.env.VITE_API_URL || '/api';
const defaultState = (): HospitalState => ({
pacientes: [],
@@ -60,6 +62,7 @@ const defaultState = (): HospitalState => ({
internaciones: [],
evoluciones: [],
laboratorios: [],
glucemias: [],
acidosBase: [],
cultivos: [],
estudiosComplementarios: [],
@@ -96,6 +99,7 @@ export function useHospitalStore() {
estudiosComplementarios: body.estudiosComplementarios || [],
interconsultas: body.interconsultas || [],
atb: body.atb || [],
glucemias: body.glucemias || [],
movimientosIndicaciones: body.movimientosIndicaciones || [],
currentUser,
isAuthenticated: !!currentUser,
@@ -140,8 +144,9 @@ export function useHospitalStore() {
const getInternacionAreaId = useCallback((internacionId: string): string | null => {
const internacion = state.internaciones.find(i => i.id === internacionId);
return internacion?.areaId || null;
}, [state.internaciones]);
if (!internacion) return null;
return internacion.areaId || getCamaAreaId(internacion.camaId) || null;
}, [state.internaciones, getCamaAreaId]);
const getPacienteAreaId = useCallback((pacienteId: string): string | null => {
const internacion = state.internaciones.find(i => i.pacienteId === pacienteId && i.activa);
@@ -150,7 +155,7 @@ export function useHospitalStore() {
if (anyInternacion) return getCamaAreaId(anyInternacion.camaId);
return null;
}
return getCamaAreaId(internacion.camaId);
return internacion.areaId || getCamaAreaId(internacion.camaId) || null;
}, [state.internaciones, getCamaAreaId]);
// Permission functions
@@ -158,7 +163,8 @@ export function useHospitalStore() {
const user = state.currentUser;
if (!user) return false;
if (user.rol === 'admin') return true;
if (!areaId) return false;
if (!areaId) return true;
if (!user.areaId) return true;
return user.areaId === areaId;
}, [state.currentUser]);
@@ -269,7 +275,7 @@ export function useHospitalStore() {
console.error('Error al actualizar cama:', err);
throw err;
}
}, [state.camas, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const agregarCama = useCallback(async (cama: Omit<Cama, 'id'>) => {
const nuevaCama: Cama = {
@@ -390,7 +396,7 @@ const finalizarInternacion = useCallback(async (internacionId: string, datos: {
console.error('Error al finalizar internación:', err);
throw err;
}
}, [state.areas, apiCall]);
}, [state, apiCall]);
const actualizarInternacion = useCallback(async (internacionId: string, datos: Partial<Internacion>) => {
const internacion = state.internaciones.find(i => i.id === internacionId);
@@ -449,7 +455,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al actualizar internación:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
// Acciones de evoluciones
const agregarEvolucion = useCallback(async (evolucion: Omit<Evolucion, 'id'>) => {
@@ -475,7 +481,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al agregar evolución:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const eliminarEvolucion = useCallback(async (id: string) => {
const evolucion = state.evoluciones.find(e => e.id === id);
@@ -497,7 +503,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al eliminar evolución:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const actualizarEvolucion = useCallback(async (id: string, datos: Partial<Evolucion>) => {
const evolucion = state.evoluciones.find(e => e.id === id);
@@ -519,7 +525,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al actualizar evolución:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
// Acciones de laboratorios
const agregarLaboratorio = useCallback(async (laboratorio: Omit<Laboratorio, 'id'>) => {
@@ -544,7 +550,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al agregar laboratorio:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const eliminarLaboratorio = useCallback(async (id: string) => {
const laboratorio = state.laboratorios.find(l => l.id === id);
@@ -565,7 +571,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al eliminar laboratorio:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const actualizarLaboratorio = useCallback(async (id: string, datos: Partial<Laboratorio>) => {
const laboratorio = state.laboratorios.find(l => l.id === id);
@@ -586,7 +592,74 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al actualizar laboratorio:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
// Acciones de glucemias
const agregarGlucemia = useCallback(async (glucemia: Omit<Glucemia, 'id'>) => {
const internacion = state.internaciones.find(i => i.id === glucemia.internacionId);
const areaId = internacion?.areaId || null;
if (!canAccessArea(areaId)) {
console.warn('No tiene permisos para agregar glucemia en esta área');
return null;
}
const nuevaGlucemia: Glucemia = {
...glucemia,
id: generateUUID(),
};
try {
await apiCall('POST', '/glucemias', nuevaGlucemia);
setState(prev => ({
...prev,
glucemias: [...prev.glucemias, nuevaGlucemia],
}));
return nuevaGlucemia.id;
} catch (err) {
console.error('Error al agregar glucemia:', err);
throw err;
}
}, [state, canAccessArea, apiCall]);
const eliminarGlucemia = useCallback(async (id: string) => {
const glucemia = state.glucemias.find(g => g.id === id);
if (!glucemia) return;
const internacion = state.internaciones.find(i => i.id === glucemia.internacionId);
const areaId = internacion?.areaId || null;
if (!canAccessArea(areaId)) {
console.warn('No tiene permisos para eliminar glucemia en esta área');
return;
}
try {
await apiCall('DELETE', `/glucemias/${id}`);
setState(prev => ({
...prev,
glucemias: prev.glucemias.filter(g => g.id !== id),
}));
} catch (err) {
console.error('Error al eliminar glucemia:', err);
throw err;
}
}, [state, canAccessArea, apiCall]);
const actualizarGlucemia = useCallback(async (id: string, datos: Partial<Glucemia>) => {
const glucemia = state.glucemias.find(g => g.id === id);
if (!glucemia) return;
const internacion = state.internaciones.find(i => i.id === glucemia.internacionId);
const areaId = internacion?.areaId || null;
if (!canAccessArea(areaId)) {
console.warn('No tiene permisos para actualizar glucemia en esta área');
return;
}
try {
await apiCall('PUT', `/glucemias/${id}`, datos);
setState(prev => ({
...prev,
glucemias: prev.glucemias.map(g => g.id === id ? { ...g, ...datos } : g),
}));
} catch (err) {
console.error('Error al actualizar glucemia:', err);
throw err;
}
}, [state, canAccessArea, apiCall]);
// Acciones de ácido-base
const agregarAcidoBase = useCallback(async (acidoBase: Omit<AcidoBase, 'id'>) => {
@@ -611,7 +684,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al agregar ácido-base:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const actualizarAcidoBase = useCallback(async (id: string, datos: Partial<AcidoBase>) => {
const acidoBase = state.acidosBase.find(a => a.id === id);
@@ -632,7 +705,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al actualizar ácido-base:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const eliminarAcidoBase = useCallback(async (id: string) => {
const acidoBase = state.acidosBase.find(a => a.id === id);
@@ -653,7 +726,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al eliminar ácido-base:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
// Acciones de cultivos
const agregarCultivo = useCallback(async (cultivo: Omit<Cultivo, 'id'>) => {
@@ -678,7 +751,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al agregar cultivo:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const actualizarCultivo = useCallback(async (id: string, datos: Partial<Cultivo>) => {
const cultivo = state.cultivos.find(c => c.id === id);
@@ -699,7 +772,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al actualizar cultivo:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const eliminarCultivo = useCallback(async (id: string) => {
const cultivo = state.cultivos.find(c => c.id === id);
@@ -720,7 +793,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al eliminar cultivo:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
// Acciones de estudios complementarios
const agregarEstudioComplementario = useCallback(async (estudio: Omit<EstudioComplementario, 'id'>) => {
@@ -745,7 +818,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al agregar estudio:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const actualizarEstudioComplementario = useCallback(async (id: string, datos: Partial<EstudioComplementario>) => {
const estudio = state.estudiosComplementarios.find(e => e.id === id);
@@ -766,7 +839,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al actualizar estudio:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const eliminarEstudioComplementario = useCallback(async (id: string) => {
const estudio = state.estudiosComplementarios.find(e => e.id === id);
@@ -787,7 +860,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al eliminar estudio:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
// Acciones de interconsultas
const agregarInterconsulta = useCallback(async (interconsulta: Omit<Interconsulta, 'id'>) => {
@@ -809,7 +882,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al agregar interconsulta:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const actualizarInterconsulta = useCallback(async (id: string, datos: Partial<Interconsulta>) => {
const interconsulta = (state.interconsultas || []).find(ic => ic.id === id);
@@ -830,7 +903,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al actualizar interconsulta:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const eliminarInterconsulta = useCallback(async (id: string) => {
const interconsulta = (state.interconsultas || []).find(ic => ic.id === id);
@@ -851,7 +924,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al eliminar interconsulta:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const agregarATB = useCallback(async (atb: Omit<ATB, 'id'>) => {
const internacion = state.internaciones.find(i => i.id === atb.internacionId);
@@ -872,7 +945,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al agregar ATB:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const actualizarATB = useCallback(async (id: string, datos: Partial<ATB>) => {
const atb = state.atb.find(a => a.id === id);
@@ -893,7 +966,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al actualizar ATB:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const eliminarATB = useCallback(async (id: string) => {
const atb = state.atb.find(a => a.id === id);
@@ -914,7 +987,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al eliminar ATB:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const agregarIndicacion = useCallback(async (indicacion: Omit<Indicacion, 'id'>) => {
const internacion = state.internaciones.find(i => i.id === indicacion.internacionId);
@@ -935,7 +1008,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al agregar indicación:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state.internaciones, canAccessArea, apiCall]);
const actualizarIndicacion = useCallback(async (id: string, datos: Partial<Indicacion>) => {
const indicacion = state.indicaciones.find(i => i.id === id);
@@ -956,7 +1029,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al actualizar indicación:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state.indicaciones, state.internaciones, canAccessArea, apiCall]);
const eliminarIndicacion = useCallback(async (id: string) => {
const indicacion = state.indicaciones.find(i => i.id === id);
@@ -977,7 +1050,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al eliminar indicación:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state.indicaciones, state.internaciones, canAccessArea, apiCall]);
const agregarMovimientoIndicacion = useCallback(async (movimiento: any) => {
const internacion = state.internaciones.find(i => i.id === movimiento.internacionId);
@@ -997,7 +1070,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
console.error('Error al agregar movimiento:', err);
throw err;
}
}, [state.internaciones, state.currentUser, apiCall]);
}, [state, canAccessArea, apiCall]);
const getMovimientosByInternacion = useCallback((internacionId: string) => {
return (state.movimientosIndicaciones || [])
@@ -1077,6 +1150,12 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
.sort((a, b) => new Date(b.fecha).getTime() - new Date(a.fecha).getTime());
}, [state.laboratorios]);
const getGlucemiasByPaciente = useCallback((pacienteId: string) => {
return state.glucemias
.filter(g => g.pacienteId === pacienteId)
.sort((a, b) => new Date(b.fecha + 'T' + (b.hora || '00:00')).getTime() - new Date(a.fecha + 'T' + (a.hora || '00:00')).getTime());
}, [state.glucemias]);
const getAcidosBaseByPaciente = useCallback((pacienteId: string) => {
return state.acidosBase
.filter(a => a.pacienteId === pacienteId)
@@ -1194,6 +1273,9 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
agregarLaboratorio,
actualizarLaboratorio,
eliminarLaboratorio,
agregarGlucemia,
actualizarGlucemia,
eliminarGlucemia,
agregarAcidoBase,
actualizarAcidoBase,
eliminarAcidoBase,
@@ -1223,6 +1305,7 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
getInternacionActivaByPaciente,
getEvolucionesByInternacion,
getLaboratoriosByPaciente,
getGlucemiasByPaciente,
getAcidosBaseByPaciente,
getCultivosByPaciente,
setCurrentInternacion,
+44 -11
View File
@@ -35,13 +35,13 @@
}
.dark {
--background: 222.2 84% 4.9%;
--background: 224 71% 4%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card: 222 47% 10%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover: 222 47% 10%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
@@ -51,16 +51,16 @@
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--sidebar-background: 222.2 84% 4.9%;
--border: 217.2 32.6% 20%;
--input: 217.2 32.6% 20%;
--ring: 217.2 91.2% 59.8%;
--sidebar-background: 224 71% 4%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 210 40% 98%;
--sidebar-primary: 217.2 91.2% 59.8%;
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-border: 217.2 32.6% 20%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@@ -70,6 +70,39 @@
@apply border-border;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground transition-colors duration-200;
}
textarea, input, select {
@apply bg-background text-foreground border-input placeholder:text-muted-foreground;
}
}
/* Toast styling for mobile margin and 100% opacity */
[data-sonner-toaster] {
z-index: 99999 !important;
}
[data-sonner-toast] {
background-color: #ffffff !important;
color: #0f172a !important;
opacity: 1 !important;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25), 0 8px 10px -6px rgba(0, 0, 0, 0.25) !important;
border: 1px solid #cbd5e1 !important;
}
.dark [data-sonner-toast] {
background-color: #0f172a !important;
color: #f8fafc !important;
border-color: #334155 !important;
}
@media (max-width: 639px) {
[data-sonner-toaster] {
bottom: 20px !important;
top: auto !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: calc(100% - 32px) !important;
max-width: 420px !important;
}
}
+119 -48
View File
@@ -1,31 +1,41 @@
import { useState } from 'react';
import { Microscope, Plus, Search, Calendar, AlertCircle, Trash2, CheckCircle2, Save, X, Pencil } from 'lucide-react';
import { Microscope, Plus, Search, Calendar, AlertCircle, Trash2, CheckCircle2, Pencil } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { Cultivo, Paciente, Cama, Internacion } from '@/types';
interface CultivosProps {
cultivos: Cultivo[];
pacientes?: Paciente[];
internaciones?: Internacion[];
camas?: Cama[];
patient?: Paciente;
internacionId: string;
internacionId?: string;
onAgregarCultivo: (cultivo: Omit<Cultivo, 'id'>) => void;
onActualizarCultivo?: (id: string, datos: Partial<Cultivo>) => void;
onEliminarCultivo: (id: string) => void;
getPacienteById?: (id: string) => Paciente | undefined;
getInternacionActivaByPaciente?: (pacienteId: string) => Internacion | undefined;
canEdit: boolean;
}
export function Cultivos({
cultivos,
pacientes = [],
internaciones = [],
camas = [],
patient,
internacionId,
onAgregarCultivo,
onActualizarCultivo,
onEliminarCultivo,
getPacienteById,
getInternacionActivaByPaciente,
canEdit,
}: CultivosProps) {
const [busqueda, setBusqueda] = useState('');
@@ -35,6 +45,8 @@ export function Cultivos({
const [dialogoDefinitivoAbierto, setDialogoDefinitivoAbierto] = useState(false);
const [cultivoSeleccionado, setCultivoSeleccionado] = useState<Cultivo | null>(null);
const [pacienteSeleccionado, setPacienteSeleccionado] = useState<string>(patient?.id || '');
const [busquedaPaciente, setBusquedaPaciente] = useState('');
const [fechaToma, setFechaToma] = useState(new Date().toISOString().split('T')[0]);
const [protocolo, setProtocolo] = useState('');
const [tipoMuestra, setTipoMuestra] = useState<Cultivo['tipoMuestra']>('HMCx2');
@@ -46,14 +58,30 @@ export function Cultivos({
const [sensible, setSensible] = useState('');
const [resistente, setResistente] = useState('');
const findPaciente = (pacienteId: string): Paciente | undefined => {
if (patient && patient.id === pacienteId) return patient;
if (getPacienteById) return getPacienteById(pacienteId);
return pacientes.find(p => p.id === pacienteId);
};
const getCamaNombreForPaciente = (pacienteId: string) => {
const inter = internaciones.find(i => i.pacienteId === pacienteId && i.activa);
if (!inter) return '';
const cama = camas.find(c => c.id === inter.camaId);
return cama ? `Cama ${cama.numero}` : '';
};
const cultivosFiltrados = cultivos.filter(c => {
if (filtroEstado !== 'todos' && c.estado !== filtroEstado) return false;
if (busqueda) {
const term = busqueda.toLowerCase();
const pac = findPaciente(c.pacienteId);
const nombrePac = pac ? `${pac.apellido} ${pac.nombre} ${pac.dni}`.toLowerCase() : '';
return (
c.protocolo?.toLowerCase().includes(term) ||
c.germen?.toLowerCase().includes(term) ||
c.tipoMuestra.toLowerCase().includes(term)
c.tipoMuestra.toLowerCase().includes(term) ||
nombrePac.includes(term)
);
}
return true;
@@ -61,11 +89,11 @@ export function Cultivos({
const getEstadoColor = (estado: string) => {
switch (estado) {
case 'NAF/Pendiente': return 'bg-amber-100 text-amber-800';
case 'Parcial': return 'bg-orange-100 text-orange-800';
case 'Positivo': return 'bg-red-100 text-red-800';
case 'Negativo': return 'bg-green-100 text-green-800';
default: return 'bg-gray-100 text-gray-800';
case 'NAF/Pendiente': return 'bg-amber-100 text-amber-800 dark:bg-amber-950/80 dark:text-amber-300 dark:border dark:border-amber-800';
case 'Parcial': return 'bg-orange-100 text-orange-800 dark:bg-orange-950/80 dark:text-orange-300 dark:border dark:border-orange-800';
case 'Positivo': return 'bg-red-100 text-red-800 dark:bg-red-950/80 dark:text-red-300 dark:border dark:border-red-800';
case 'Negativo': return 'bg-green-100 text-green-800 dark:bg-green-950/80 dark:text-green-300 dark:border dark:border-green-800';
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300';
}
};
@@ -81,6 +109,8 @@ export function Cultivos({
const abrirNuevo = () => {
setCultivoSeleccionado(null);
setPacienteSeleccionado(patient?.id || '');
setBusquedaPaciente('');
setFechaToma(new Date().toISOString().split('T')[0]);
setProtocolo('');
setTipoMuestra('HMCx2');
@@ -107,14 +137,18 @@ export function Cultivos({
};
const guardarNuevo = () => {
if (!protocolo.trim() || !patient) return;
const targetPac = patient || findPaciente(pacienteSeleccionado);
if (!protocolo.trim() || !targetPac) return;
const activeInter = getInternacionActivaByPaciente ? getInternacionActivaByPaciente(targetPac.id) : undefined;
const targetInternacionId = internacionId || activeInter?.id || '';
onAgregarCultivo({
pacienteId: patient.id,
internacionId,
pacienteId: targetPac.id,
internacionId: targetInternacionId,
fechaToma,
protocolo,
tipoMuestra,
observaciones: observaciones || undefined,
observaciones: observaciones,
estado: 'NAF/Pendiente',
});
setDialogoNuevoAbierto(false);
@@ -126,8 +160,8 @@ export function Cultivos({
onActualizarCultivo(cultivoSeleccionado.id, {
estado: 'Parcial',
germen,
sensible: sensible || undefined,
resistente: resistente || undefined,
sensible: sensible,
resistente: resistente,
});
}
setDialogoParcialAbierto(false);
@@ -186,39 +220,50 @@ export function Cultivos({
</div>
<div className="space-y-3">
{cultivosFiltrados.map((cultivo) => (
<Card key={cultivo.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="flex flex-col gap-3">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2">
<div className="flex items-center gap-3">
<div className={`h-10 w-10 rounded-full flex items-center justify-center shrink-0 ${
cultivo.estado === 'NAF/Pendiente' ? 'bg-amber-100' :
cultivo.estado === 'Parcial' ? 'bg-orange-100' :
cultivo.estado === 'Positivo' ? 'bg-red-100' : 'bg-green-100'
}`}>
<Microscope className={`h-5 w-5 ${
cultivo.estado === 'NAF/Pendiente' ? 'text-amber-600' :
cultivo.estado === 'Parcial' ? 'text-orange-600' :
cultivo.estado === 'Positivo' ? 'text-red-600' : 'text-green-600'
}`} />
</div>
<div className="min-w-0">
<h3 className="font-bold text-sm sm:text-base truncate">
{patient ? `${patient.apellido}, ${patient.nombre}` : 'Paciente no encontrado'}
</h3>
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm text-gray-500">
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{cultivo.fechaToma}
</span>
<Badge variant="outline" className="text-xs">{cultivo.tipoMuestra}</Badge>
<Badge className={`text-xs ${getEstadoColor(cultivo.estado)}`}>
{getEstadoLabel(cultivo.estado)}
</Badge>
{cultivosFiltrados.map((cultivo) => {
const pac = findPaciente(cultivo.pacienteId);
const camaNombre = getCamaNombreForPaciente(cultivo.pacienteId);
return (
<Card key={cultivo.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="flex flex-col gap-3">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2">
<div className="flex items-center gap-3">
<div className={`h-10 w-10 rounded-full flex items-center justify-center shrink-0 ${
cultivo.estado === 'NAF/Pendiente' ? 'bg-amber-100 dark:bg-amber-950/80' :
cultivo.estado === 'Parcial' ? 'bg-orange-100 dark:bg-orange-950/80' :
cultivo.estado === 'Positivo' ? 'bg-red-100 dark:bg-red-950/80' : 'bg-green-100 dark:bg-green-950/80'
}`}>
<Microscope className={`h-5 w-5 ${
cultivo.estado === 'NAF/Pendiente' ? 'text-amber-600 dark:text-amber-400' :
cultivo.estado === 'Parcial' ? 'text-orange-600 dark:text-orange-400' :
cultivo.estado === 'Positivo' ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'
}`} />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<h3 className="font-bold text-sm sm:text-base truncate">
{pac ? `${pac.apellido}, ${pac.nombre}` : 'Paciente no encontrado'}
</h3>
{camaNombre && (
<Badge variant="outline" className="text-xs shrink-0">
{camaNombre}
</Badge>
)}
</div>
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm text-gray-500">
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{cultivo.fechaToma}
</span>
<Badge variant="outline" className="text-xs">{cultivo.tipoMuestra}</Badge>
<Badge className={`text-xs ${getEstadoColor(cultivo.estado)}`}>
{getEstadoLabel(cultivo.estado)}
</Badge>
</div>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-1 sm:gap-2 ml-auto sm:ml-0">
{canEdit && onActualizarCultivo && cultivo.estado === 'NAF/Pendiente' && (
<Button size="sm" variant="outline" className="text-xs px-2 py-1" onClick={() => abrirDefinitivo(cultivo)}>
@@ -251,7 +296,7 @@ export function Cultivos({
</p>
)}
{cultivo.observaciones && (
<p className="text-sm text-gray-600 bg-gray-50 p-2 rounded">
<p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 p-2 rounded">
{cultivo.observaciones}
</p>
)}
@@ -316,7 +361,8 @@ export function Cultivos({
</div>
</CardContent>
</Card>
))}
);
})}
{cultivosFiltrados.length === 0 && (
<p className="text-center text-gray-500 py-8">
@@ -332,6 +378,31 @@ export function Cultivos({
<DialogTitle>Nuevo Cultivo</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{!patient && (
<div className="space-y-2">
<Label>Paciente</Label>
<Input
placeholder="Buscar paciente por nombre o DNI..."
value={busquedaPaciente}
onChange={(e) => setBusquedaPaciente(e.target.value)}
className="mb-2 text-xs"
/>
<Select value={pacienteSeleccionado} onValueChange={setPacienteSeleccionado}>
<SelectTrigger>
<SelectValue placeholder="Seleccionar paciente" />
</SelectTrigger>
<SelectContent className="max-h-60 overflow-auto">
{pacientes
.filter(p => !busquedaPaciente || `${p.apellido} ${p.nombre} ${p.dni}`.toLowerCase().includes(busquedaPaciente.toLowerCase()))
.map(p => (
<SelectItem key={p.id} value={p.id}>
{p.apellido}, {p.nombre} (DNI: {p.dni})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Fecha de Toma</Label>
+34 -34
View File
@@ -67,7 +67,7 @@ export function Dashboard({
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="border-l-4 border-l-blue-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-500 flex items-center gap-2">
<CardTitle className="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2">
<Bed className="h-4 w-4" />
Ocupación de Camas
</CardTitle>
@@ -75,9 +75,9 @@ export function Dashboard({
<CardContent>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold">{estadisticas.porcentajeOcupacion}%</span>
<span className="text-sm text-gray-500">{estadisticas.camasOcupadas}/{estadisticas.totalCamas}</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{estadisticas.camasOcupadas}/{estadisticas.totalCamas}</span>
</div>
<p className="text-xs text-gray-400 mt-1">
<p className="text-xs text-gray-400 dark:text-gray-400 mt-1">
{estadisticas.camasDisponibles} camas disponibles
</p>
</CardContent>
@@ -85,7 +85,7 @@ export function Dashboard({
<Card className="border-l-4 border-l-green-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-500 flex items-center gap-2">
<CardTitle className="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2">
<Users className="h-4 w-4" />
Camas Fuera de Área
</CardTitle>
@@ -94,7 +94,7 @@ export function Dashboard({
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold">{estadisticas.camasFueraDeArea}</span>
</div>
<p className="text-xs text-gray-400 mt-1">
<p className="text-xs text-gray-400 dark:text-gray-400 mt-1">
Camas Fuera de Area
</p>
</CardContent>
@@ -102,7 +102,7 @@ export function Dashboard({
<Card className="border-l-4 border-l-purple-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-500 flex items-center gap-2">
<CardTitle className="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2">
<ClipboardList className="h-4 w-4" />
Internaciones Activas
</CardTitle>
@@ -111,7 +111,7 @@ export function Dashboard({
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold">{estadisticas.internacionesActivas}</span>
</div>
<p className="text-xs text-gray-400 mt-1">
<p className="text-xs text-gray-400 dark:text-gray-400 mt-1">
En curso actualmente
</p>
</CardContent>
@@ -119,7 +119,7 @@ export function Dashboard({
<Card className="border-l-4 border-l-amber-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-500 flex items-center gap-2">
<CardTitle className="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2">
<Microscope className="h-4 w-4" />
Cultivos Pendientes
</CardTitle>
@@ -128,7 +128,7 @@ export function Dashboard({
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold">{estadisticas.cultivosPendientes}</span>
</div>
<p className="text-xs text-gray-400 mt-1">
<p className="text-xs text-gray-400 dark:text-gray-400 mt-1">
Esperando resultados
</p>
</CardContent>
@@ -141,7 +141,7 @@ export function Dashboard({
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-lg flex items-center gap-2">
<Bed className="h-5 w-5 text-blue-600" />
<Bed className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Estado de Camas
</CardTitle>
<Button variant="ghost" size="sm" onClick={() => onCambiarVista('camas')}>
@@ -150,33 +150,33 @@ export function Dashboard({
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-3">
<div className="bg-green-50 p-3 rounded-lg">
<div className="bg-green-50 dark:bg-green-950/60 border border-transparent dark:border-green-800/50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-600" />
<span className="text-sm font-medium text-green-800">Disponibles</span>
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
<span className="text-sm font-medium text-green-800 dark:text-green-300">Disponibles</span>
</div>
<p className="text-2xl font-bold text-green-700 mt-1">{estadisticas.camasDisponibles}</p>
<p className="text-2xl font-bold text-green-700 dark:text-green-200 mt-1">{estadisticas.camasDisponibles}</p>
</div>
<div className="bg-red-50 p-3 rounded-lg">
<div className="bg-red-50 dark:bg-red-950/60 border border-transparent dark:border-red-800/50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-600" />
<span className="text-sm font-medium text-red-800">Ocupadas</span>
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
<span className="text-sm font-medium text-red-800 dark:text-red-300">Ocupadas</span>
</div>
<p className="text-2xl font-bold text-red-700 mt-1">{estadisticas.camasOcupadas}</p>
<p className="text-2xl font-bold text-red-700 dark:text-red-200 mt-1">{estadisticas.camasOcupadas}</p>
</div>
<div className="bg-amber-50 p-3 rounded-lg">
<div className="bg-amber-50 dark:bg-amber-950/60 border border-transparent dark:border-amber-800/50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-amber-600" />
<span className="text-sm font-medium text-amber-800">Mantenimiento</span>
<Clock className="h-4 w-4 text-amber-600 dark:text-amber-400" />
<span className="text-sm font-medium text-amber-800 dark:text-amber-300">Mantenimiento</span>
</div>
<p className="text-2xl font-bold text-amber-700 mt-1">{estadisticas.camasMantenimiento}</p>
<p className="text-2xl font-bold text-amber-700 dark:text-amber-200 mt-1">{estadisticas.camasMantenimiento}</p>
</div>
<div className="bg-blue-50 p-3 rounded-lg">
<div className="bg-blue-50 dark:bg-blue-950/60 border border-transparent dark:border-blue-800/50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium text-blue-800">Ocupación</span>
<TrendingUp className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<span className="text-sm font-medium text-blue-800 dark:text-blue-300">Ocupación</span>
</div>
<p className="text-2xl font-bold text-blue-700 mt-1">{estadisticas.porcentajeOcupacion}%</p>
<p className="text-2xl font-bold text-blue-700 dark:text-blue-200 mt-1">{estadisticas.porcentajeOcupacion}%</p>
</div>
</div>
</CardContent>
@@ -204,16 +204,16 @@ export function Dashboard({
{internacionesActivas.map((internacion) => {
const paciente = getPacienteById(internacion.pacienteId);
return (
<div key={internacion.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div key={internacion.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/80 border border-transparent dark:border-gray-700/50 rounded-lg">
<div>
<p className="font-medium text-gray-900">
<p className="font-medium text-gray-900 dark:text-gray-100">
{paciente ? `${paciente.apellido}, ${paciente.nombre}` : 'Paciente no encontrado'}
</p>
<p className="text-sm text-gray-500">
<p className="text-sm text-gray-500 dark:text-gray-400">
Ingreso: {internacion.fechaIngresoClinica ? formatDateDDMMYYYY(internacion.fechaIngresoClinica) : 'N/A'}
</p>
</div>
<Badge variant="outline" className="bg-purple-50 text-purple-700 dark:bg-purple-900 dark:text-purple-300 dark:border-purple-700">
<Badge variant="outline" className="bg-purple-50 text-purple-700 dark:bg-purple-950/60 dark:text-purple-300 dark:border-purple-800">
Activa
</Badge>
</div>
@@ -292,16 +292,16 @@ export function Dashboard({
{cultivosRecientes.map((cultivo) => {
const paciente = getPacienteById(cultivo.pacienteId);
return (
<div key={cultivo.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div key={cultivo.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/80 border border-transparent dark:border-gray-700/50 rounded-lg">
<div>
<p className="font-medium text-gray-900">
<p className="font-medium text-gray-900 dark:text-gray-100">
{paciente ? `${paciente.apellido}, ${paciente.nombre}` : 'Paciente no encontrado'}
</p>
<p className="text-sm text-gray-500">
<p className="text-sm text-gray-500 dark:text-gray-400">
{cultivo.tipoMuestra} - {cultivo.fechaToma}
</p>
</div>
<Badge className="bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300">
<Badge className="bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 dark:border dark:border-amber-800">
<Clock className="h-3 w-3 mr-1" />
Pendiente
</Badge>
+107 -104
View File
@@ -1,72 +1,56 @@
import { useState } from 'react';
import { ArrowLeft, Save, X, Bed, Calendar, Stethoscope, User, ClipboardList } from 'lucide-react';
import { Save, X, Bed, Stethoscope, User, Loader2 } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import type { Paciente, Cama, Area, Internacion } from '@/types';
interface EditIngresoProps {
internacion: Internacion;
paciente: Paciente;
cama: Cama | undefined;
paciente?: Paciente;
cama?: Cama;
pacientes: Paciente[];
camas: Cama[];
areas: Area[];
onActualizarInternacion: (id: string, datos: Partial<Internacion>) => void;
onAgregarCama?: (cama: Omit<Cama, 'id'>) => any;
onActualizarInternacion: (id: string, datos: Partial<Internacion>) => Promise<void> | void;
onAgregarCama?: (cama: Omit<Cama, 'id'>) => Promise<string>;
onVolver: () => void;
getAreaName: (areaId: string | undefined) => string;
}
export function EditIngreso({
internacion,
paciente: pacienteOriginal,
cama: camaOriginal,
pacientes,
camas,
areas,
onActualizarInternacion,
onAgregarCama,
onVolver,
getAreaName
}: EditIngresoProps) {
const [pacienteSeleccionado, setPacienteSeleccionado] = useState(internacion.pacienteId);
const [busqueda, setBusqueda] = useState('');
const [camaSeleccionada, setCamaSeleccionada] = useState(internacion.camaId);
const [areaSeleccionada, setAreaSeleccionada] = useState(internacion.areaId);
const [pacienteSeleccionado] = useState(internacion.pacienteId);
const [camaSeleccionada] = useState(internacion.camaId);
const [areaSeleccionada] = useState(internacion.areaId);
const [camaInput, setCamaInput] = useState('');
const [modoCama, setModoCama] = useState<'seleccionar' | 'escribir'>('seleccionar');
const [medico, setMedico] = useState(internacion.medicoIngresante);
const [medico, setMedico] = useState(internacion.medicoIngresante || '');
const [fechaIngresoHospital, setFechaIngresoHospital] = useState(internacion.fechaIngresoHospital || '');
const [fechaIngresoClinica, setFechaIngresoClinica] = useState(internacion.fechaIngresoClinica || '');
const [motivoConsulta, setMotivoConsulta] = useState(internacion.motivoConsulta || '');
const [diagnosticoIngreso, setDiagnosticoIngreso] = useState(internacion.diagnosticoIngreso);
const [enfermedadActual, setEnfermedadActual] = useState(internacion.enfermedadActual);
const [diagnosticoIngreso, setDiagnosticoIngreso] = useState(internacion.diagnosticoIngreso || '');
const [enfermedadActual, setEnfermedadActual] = useState(internacion.enfermedadActual || '');
const [antecedentesEnfermedadActual, setAntecedentesEnfermedadActual] = useState(internacion.antecedentesEnfermedadActual || '');
const [apache, setApache] = useState(internacion.apache || '');
const [derivacion, setDerivacion] = useState(internacion.derivacion || '');
const calcularEdad = (fechaNacimiento: string) => {
const hoy = new Date();
const nacimiento = new Date(fechaNacimiento);
let edad = hoy.getFullYear() - nacimiento.getFullYear();
const mes = hoy.getMonth() - nacimiento.getMonth();
if (mes < 0 || (mes === 0 && hoy.getDate() < nacimiento.getDate())) {
edad--;
}
return edad;
};
const [isSubmitting, setIsSubmitting] = useState(false);
const selectedPaciente = pacientes.find(p => p.id === pacienteSeleccionado);
const areasDisponibles = areas.filter(a => {
const nombre = (a.nombre || '').trim().toLowerCase();
return nombre !== 'fuera de area';
});
const areaFueraDeArea = areas.find(a => (a.nombre || '').trim().toLowerCase() === 'fuera de area');
const areaFueraDeAreaId = areaFueraDeArea?.id || '';
const normalizeStr = (str?: string) => (str || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim().toLowerCase();
const areaFueraDeArea = areas.find(a => normalizeStr(a.nombre) === 'fuera de area');
const areaFueraDeAreaId = areaFueraDeArea?.id || areas[0]?.id || 'fuera-de-area';
const parseBedNumber = (numero: string) => {
const match = numero.match(/^(\d{3})-(\d+)$/);
@@ -112,76 +96,95 @@ export function EditIngreso({
});
const handleSubmit = async () => {
if (!pacienteSeleccionado || !medico || !diagnosticoIngreso || !enfermedadActual) {
alert('Por favor complete los campos obligatorios');
if (!pacienteSeleccionado) {
toast.error('Debe seleccionar un paciente');
return;
}
let camaId = '';
let newAreaId = '';
if (modoCama === 'escribir') {
if (!camaInput.trim()) {
alert('Ingrese el número de cama');
return;
}
const numeroCama = camaInput.trim();
const camaExistente = camas.find(c => c.numero === numeroCama);
if (camaExistente) {
camaId = camaExistente.id;
newAreaId = camaExistente.areaId || '';
} else if (onAgregarCama) {
const nuevaCamaId = onAgregarCama({
numero: numeroCama,
areaId: areaFueraDeAreaId,
tipo: 'General',
estado: 'Ocupada'
});
camaId = nuevaCamaId;
newAreaId = areaFueraDeAreaId;
} else {
alert('No se puede agregar una nueva cama');
return;
}
} else {
if (camaSeleccionada && camaSeleccionada !== internacion.camaId) {
const camaElegida = sortedCamas.find(c => c.id === camaSeleccionada);
if (!camaElegida) {
alert('Cama no encontrada');
return;
}
newAreaId = camaElegida.areaId || areaFueraDeAreaId;
if (!newAreaId) {
alert('La cama no tiene un área asignada');
return;
}
camaId = camaSeleccionada;
} else {
newAreaId = areaSeleccionada || internacion.areaId;
camaId = internacion.camaId;
}
if (!newAreaId) {
alert('Seleccione un área de trabajo');
return;
}
if (!diagnosticoIngreso.trim()) {
toast.error('Debe ingresar el diagnóstico de ingreso');
return;
}
onActualizarInternacion(internacion.id, {
pacienteId: pacienteSeleccionado,
camaId: camaId,
areaId: newAreaId,
medicoIngresante: medico,
diagnosticoIngreso,
motivoConsulta: motivoConsulta || undefined,
enfermedadActual,
antecedentesEnfermedadActual: antecedentesEnfermedadActual || undefined,
fechaIngresoHospital: fechaIngresoHospital || undefined,
fechaIngresoClinica: fechaIngresoClinica || undefined,
apache: apache || undefined,
derivacion: derivacion || undefined,
});
if (!enfermedadActual.trim()) {
toast.error('Debe ingresar la enfermedad actual');
return;
}
onVolver();
if (!medico.trim()) {
toast.error('Debe ingresar el médico ingresante');
return;
}
setIsSubmitting(true);
try {
let camaId = '';
let newAreaId = '';
if (modoCama === 'escribir') {
if (!camaInput.trim()) {
toast.error('Ingrese el número de cama');
setIsSubmitting(false);
return;
}
const numeroCama = camaInput.trim();
const camaExistente = camas.find(c => c.numero === numeroCama);
if (camaExistente) {
camaId = camaExistente.id;
newAreaId = camaExistente.areaId || areaFueraDeAreaId;
} else if (onAgregarCama) {
camaId = await onAgregarCama({
numero: numeroCama,
areaId: areaFueraDeAreaId,
tipo: 'General',
estado: 'Ocupada'
});
newAreaId = areaFueraDeAreaId;
} else {
toast.error('No se puede crear la cama');
setIsSubmitting(false);
return;
}
} else {
if (camaSeleccionada && camaSeleccionada !== internacion.camaId) {
const camaElegida = sortedCamas.find(c => c.id === camaSeleccionada);
if (!camaElegida) {
toast.error('Cama no encontrada');
setIsSubmitting(false);
return;
}
newAreaId = camaElegida.areaId || areaFueraDeAreaId;
camaId = camaSeleccionada;
} else {
newAreaId = areaSeleccionada || internacion.areaId || areaFueraDeAreaId;
camaId = internacion.camaId;
}
}
await onActualizarInternacion(internacion.id, {
pacienteId: pacienteSeleccionado,
camaId: camaId,
areaId: newAreaId,
medicoIngresante: medico.trim(),
diagnosticoIngreso: diagnosticoIngreso.trim(),
motivoConsulta: motivoConsulta.trim() || undefined,
enfermedadActual: enfermedadActual.trim(),
antecedentesEnfermedadActual: antecedentesEnfermedadActual.trim() || undefined,
fechaIngresoHospital: fechaIngresoHospital || undefined,
fechaIngresoClinica: fechaIngresoClinica || undefined,
apache: apache.trim() || undefined,
derivacion: derivacion.trim() || undefined,
});
toast.success('Ingreso actualizado correctamente');
onVolver();
} catch (err) {
console.error('Error al actualizar ingreso:', err);
toast.error('Error al actualizar el ingreso');
} finally {
setIsSubmitting(false);
}
};
return (
@@ -198,12 +201,12 @@ export function EditIngreso({
</div>
</div>
<div className="flex gap-2 ml-auto sm:ml-0">
<Button variant="secondary" onClick={() => onVolver()}>
<Button variant="secondary" onClick={() => onVolver()} disabled={isSubmitting}>
<X className="h-4 w-4 mr-2" />
Cancelar
</Button>
<Button onClick={handleSubmit}>
<Save className="h-4 w-4 mr-2" />
<Button onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
Guardar Cambios
</Button>
</div>
@@ -214,7 +217,7 @@ export function EditIngreso({
<div className="space-y-6">
<Card className="min-h-[200px]">
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<User className="h-4 w-4" />
Datos del Paciente
</h3>
@@ -288,7 +291,7 @@ export function EditIngreso({
<div className="space-y-6">
<Card className="min-h-[200px]">
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Bed className="h-4 w-4" />
Asignación de Cama y Área
</h3>
@@ -348,7 +351,7 @@ export function EditIngreso({
<div className="space-y-6">
<Card>
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<ClipboardList className="h-4 w-4" />
Datos de Ingreso
</h3>
@@ -399,7 +402,7 @@ export function EditIngreso({
<div className="space-y-6">
<Card>
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900">
<h3 className="font-semibold text-gray-900 dark:text-white">
Datos Clínicos
</h3>
@@ -452,7 +455,7 @@ export function EditIngreso({
<Card>
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Stethoscope className="h-4 w-4" />
Médico Ingresante
</h3>
+38 -15
View File
@@ -7,15 +7,16 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { toast } from 'sonner';
import type { Evolucion, Internacion, Paciente, SignosVitales, ExamenFisico } from '@/types';
interface EvolucionesProps {
evoluciones: Evolucion[];
internaciones: Internacion[];
pacientes: Paciente[];
onAgregarEvolucion: (evolucion: Omit<Evolucion, 'id'>) => void;
onActualizarEvolucion?: (id: string, datos: Partial<Evolucion>) => void;
onEliminarEvolucion: (id: string) => void;
onAgregarEvolucion: (evolucion: Omit<Evolucion, 'id'>) => Promise<unknown> | void;
onActualizarEvolucion?: (id: string, datos: Partial<Evolucion>) => Promise<unknown> | void;
onEliminarEvolucion: (id: string) => Promise<unknown> | void;
getPacienteById: (id: string) => Paciente | undefined;
getInternacionActivaByPaciente: (pacienteId: string) => Internacion | undefined;
}
@@ -81,9 +82,13 @@ export function Evoluciones({
const abrirEditar = (evo: Evolucion) => {
setEvolucionEditando(evo);
const internacion = internaciones.find(i => i.id === evo.internacionId);
if (internacion) {
setPacienteSeleccionado(internacion.pacienteId);
}
setFecha(evo.fecha);
setHora(evo.hora);
setMedico(evo.medico);
setMedico(evo.medico || '');
setTemperatura(evo.signosVitales?.temperatura?.toString() || '');
setPresionSistolica(evo.signosVitales?.presionSistolica?.toString() || '');
setPresionDiastolica(evo.signosVitales?.presionDiastolica?.toString() || '');
@@ -103,9 +108,20 @@ export function Evoluciones({
setDialogoAbierto(true);
};
const handleGuardar = () => {
const internacion = getInternacionActivaByPaciente(pacienteSeleccionado);
if (!internacion || !medico) return;
const handleGuardar = async () => {
const internacion = evolucionEditando
? internaciones.find(i => i.id === evolucionEditando.internacionId)
: getInternacionActivaByPaciente(pacienteSeleccionado);
if (!medico.trim()) {
toast.error('Debe completar el nombre del médico');
return;
}
if (!internacion) {
toast.error('Seleccione un paciente internado');
return;
}
const signosVitales: SignosVitales | undefined =
temperatura || presionSistolica || frecuenciaCardiaca
@@ -135,7 +151,7 @@ export function Evoluciones({
const evolucionData = {
fecha,
hora,
medico,
medico: medico.trim(),
signosVitales,
examenFisico,
novedades: novedades || undefined,
@@ -143,14 +159,21 @@ export function Evoluciones({
pendientes: pendientes || undefined,
};
if (evolucionEditando && onActualizarEvolucion) {
onActualizarEvolucion(evolucionEditando.id, evolucionData);
} else {
onAgregarEvolucion({ ...evolucionData, internacionId: internacion.id });
}
try {
if (evolucionEditando && onActualizarEvolucion) {
await onActualizarEvolucion(evolucionEditando.id, evolucionData);
toast.success('Evolución actualizada correctamente');
} else {
await onAgregarEvolucion({ ...evolucionData, internacionId: internacion.id });
toast.success('Evolución agregada correctamente');
}
resetFormulario();
setDialogoAbierto(false);
resetFormulario();
setDialogoAbierto(false);
} catch (err) {
console.error('Error al guardar evolución:', err);
toast.error('Error al guardar los cambios de la evolución');
}
};
const pacientesInternados = pacientes.filter(p => {
+96 -24
View File
@@ -1,17 +1,17 @@
import { useState, useEffect } from 'react';
import { useHospitalStore } from '@/hooks/useHospitalStore';
import type { Usuario, RolUsuario, Area } from '@/types';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Plus, Pencil, Trash2, UserCog, Mail, Shield } from 'lucide-react';
import { Plus, Pencil, Trash2 } from 'lucide-react';
const API_BASE = 'http://localhost:4001/api';
const API_BASE = import.meta.env.VITE_API_URL || '/api';
export function GestionUsuarios() {
const { logout } = useHospitalStore();
@@ -92,23 +92,36 @@ export function GestionUsuarios() {
};
const handleSubmit = async () => {
if (!form.apellido.trim() || !form.nombre.trim() || !form.dni.trim()) {
alert('Por favor ingrese Apellido, Nombre y DNI.');
return;
}
try {
let res: Response;
if (editUsuario) {
await fetch(`${API_BASE}/usuarios/${editUsuario.id}`, {
res = await fetch(`${API_BASE}/usuarios/${editUsuario.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
});
} else {
await fetch(`${API_BASE}/usuarios`, {
res = await fetch(`${API_BASE}/usuarios`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
});
}
if (!res.ok) {
const errData = await res.json().catch(() => ({ error: 'Error al guardar usuario' }));
alert(errData.error || 'Error al guardar usuario');
return;
}
setDialogOpen(false);
resetForm();
fetchUsuarios();
await fetchUsuarios();
} catch (err) {
console.error(err);
alert('Error al guardar usuario');
@@ -118,10 +131,16 @@ export function GestionUsuarios() {
const handleDelete = async (id: string) => {
if (!confirm('¿Está seguro de eliminar este usuario?')) return;
try {
await fetch(`${API_BASE}/usuarios/${id}`, { method: 'DELETE' });
fetchUsuarios();
const res = await fetch(`${API_BASE}/usuarios/${id}`, { method: 'DELETE' });
if (!res.ok) {
const errData = await res.json().catch(() => ({ error: 'Error al eliminar usuario' }));
alert(errData.error || 'Error al eliminar usuario');
return;
}
await fetchUsuarios();
} catch (err) {
console.error(err);
alert('Error al eliminar usuario');
}
};
@@ -144,19 +163,20 @@ export function GestionUsuarios() {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-bold dark:text-white">Gestión de Usuarios</h1>
<p className="text-gray-500 dark:text-gray-400">Administrar usuarios del sistema</p>
</div>
<Button onClick={() => { resetForm(); setDialogOpen(true); }}>
<Button className="w-full sm:w-auto" onClick={() => { resetForm(); setDialogOpen(true); }}>
<Plus className="h-4 w-4 mr-2" />
Nuevo Usuario
</Button>
</div>
<Card>
<CardContent className="p-0">
{/* Desktop Table View */}
<Card className="hidden md:block">
<CardContent className="p-0 overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
@@ -180,7 +200,7 @@ export function GestionUsuarios() {
</Badge>
</TableCell>
<TableCell>{getAreaName(usu.areaId)}</TableCell>
<TableCell>{usu.email}</TableCell>
<TableCell>{usu.email || '-'}</TableCell>
<TableCell>{usu.matriculaProfesional || '-'}</TableCell>
<TableCell>
<div className="flex gap-2">
@@ -199,13 +219,60 @@ export function GestionUsuarios() {
</CardContent>
</Card>
{/* Mobile Cards View */}
<div className="grid grid-cols-1 gap-3 md:hidden">
{usuarios.map((usu) => (
<Card key={usu.id} className="p-4 space-y-3">
<div className="flex items-start justify-between gap-2">
<div>
<h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100">
{usu.apellido}, {usu.nombre}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">DNI: {usu.dni}</p>
</div>
<Badge variant={usu.rol === 'admin' ? 'default' : 'secondary'}>
{getRolLabel(usu.rol)}
</Badge>
</div>
<div className="grid grid-cols-2 gap-2 text-sm pt-2 border-t border-gray-100 dark:border-gray-800">
<div>
<span className="text-xs text-gray-400 block">Área</span>
<span className="font-medium text-gray-700 dark:text-gray-300">{getAreaName(usu.areaId)}</span>
</div>
<div>
<span className="text-xs text-gray-400 block">Matrícula</span>
<span className="font-medium text-gray-700 dark:text-gray-300">{usu.matriculaProfesional || '-'}</span>
</div>
{usu.email && (
<div className="col-span-2">
<span className="text-xs text-gray-400 block">Email</span>
<span className="font-medium text-gray-700 dark:text-gray-300 truncate block">{usu.email}</span>
</div>
)}
</div>
<div className="flex justify-end gap-2 pt-2 border-t border-gray-100 dark:border-gray-800">
<Button variant="outline" size="sm" onClick={() => openEdit(usu)}>
<Pencil className="h-4 w-4 mr-1" />
Editar
</Button>
<Button variant="outline" size="sm" className="text-red-600 hover:text-red-700 dark:text-red-400" onClick={() => handleDelete(usu.id)}>
<Trash2 className="h-4 w-4 mr-1" />
Eliminar
</Button>
</div>
</Card>
))}
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
<DialogContent className="max-w-lg w-[95vw] sm:w-full max-h-[90vh] overflow-y-auto p-4 sm:p-6">
<DialogHeader>
<DialogTitle>{editUsuario ? 'Editar Usuario' : 'Nuevo Usuario'}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-4 pt-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Apellido</Label>
<Input value={form.apellido} onChange={(e) => setForm({ ...form, apellido: e.target.value })} />
@@ -216,7 +283,7 @@ export function GestionUsuarios() {
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>DNI</Label>
<Input value={form.dni} onChange={(e) => setForm({ ...form, dni: e.target.value })} disabled={!!editUsuario} />
@@ -232,7 +299,7 @@ export function GestionUsuarios() {
<Input type="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Rol</Label>
<Select value={form.rol} onValueChange={(v) => setForm({ ...form, rol: v as RolUsuario })}>
@@ -269,14 +336,19 @@ export function GestionUsuarios() {
)}
<div className="space-y-2">
<Label>{editUsuario ? 'Nueva Contraseña (opcional)' : 'Contraseña'}</Label>
<Input type="password" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} />
<Label>{editUsuario ? 'Nueva Contraseña (opcional)' : 'Contraseña (opcional)'}</Label>
<Input
type="password"
placeholder={editUsuario ? 'Dejar en blanco para no modificar' : 'Por defecto se usará el DNI'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancelar</Button>
<Button onClick={handleSubmit}>
<div className="flex flex-col-reverse sm:flex-row justify-end gap-2 mt-4">
<Button variant="outline" className="w-full sm:w-auto" onClick={() => setDialogOpen(false)}>Cancelar</Button>
<Button className="w-full sm:w-auto" onClick={handleSubmit}>
{editUsuario ? 'Actualizar' : 'Crear'}
</Button>
</div>
+557 -86
View File
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { PDFDocument } from 'pdf-lib';
import { User } from 'lucide-react';
import {
@@ -52,7 +53,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, ATB, Indicacion } from '@/types';
import type { Laboratorio, Glucemia, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario, Interconsulta, ATB, Indicacion } from '@/types';
import { formatDateDDMMYYYY } from '@/lib/utils';
import { SeccionIndicaciones } from './SeccionIndicaciones';
@@ -63,6 +64,7 @@ interface HistoriaClinicaProps {
allCamas?: Cama[];
evoluciones: Evolucion[];
laboratorios: Laboratorio[];
glucemias: Glucemia[];
acidosBase: AcidoBase[];
cultivos: Cultivo[];
estudiosComplementarios: EstudioComplementario[];
@@ -77,6 +79,9 @@ interface HistoriaClinicaProps {
onAgregarLaboratorio: (laboratorios: Omit<Laboratorio, 'id'>) => void;
onActualizarLaboratorio: (id: string, datos: Partial<Laboratorio>) => void;
onEliminarLaboratorio: (id: string) => void;
onAgregarGlucemia: (glucemia: Omit<Glucemia, 'id'>) => void;
onActualizarGlucemia: (id: string, datos: Partial<Glucemia>) => void;
onEliminarGlucemia: (id: string) => void;
onAgregarLaboratorioConAcidoBase?: (l: Omit<Laboratorio, 'id'>, a?: Omit<AcidoBase, 'id'>) => void;
onAgregarAcidoBase: (acidoBase: Omit<AcidoBase, 'id'>) => void;
onActualizarAcidoBase: (id: string, datos: Partial<AcidoBase>) => void;
@@ -141,6 +146,26 @@ const RANGOS_LABORATORIO: Record<string, { min: number; max: number }> = {
'Tiempo de Protrombina': { min: 12, max: 14 },
'KPTT': { min: 30, max: 45 },
'INR': { min: 0.8, max: 1.5 },
'Colesterol Total': { min: 0, max: 200 },
'Colesterol LDL': { min: 0, max: 130 },
'Colesterol No HDL': { min: 0, max: 160 },
'Colesterol HDL': { min: 40, max: 100 },
'Triglicéridos': { min: 0, max: 150 },
'Albúmina': { min: 3.5, max: 5.0 },
'Calcio Total': { min: 8.5, max: 10.5 },
'Fosfatasa Alcalina': { min: 44, max: 147 },
'LDH': { min: 140, max: 280 },
'Hierro': { min: 50, max: 170 },
'Transferrina': { min: 200, max: 360 },
'Porcentaje de Saturación de Transferrina': { min: 20, max: 50 },
'Ferritina': { min: 10, max: 300 },
'Ácido Fólico': { min: 3, max: 17 },
'Vitamina B12': { min: 200, max: 900 },
'NT-proBNP': { min: 0, max: 125 },
'Procalcitonina': { min: 0, max: 0.5 },
'Fósforo': { min: 2.5, max: 4.5 },
'Magnesio': { min: 1.6, max: 2.6 },
'Calcio Iónico': { min: 1.12, max: 1.32 },
};
function calcularEstadoLaboratorio(parametro: string, valor: string): 'Normal' | 'Alto' | 'Bajo' {
@@ -150,6 +175,24 @@ function calcularEstadoLaboratorio(parametro: string, valor: string): 'Normal' |
const num = parseFloat(valor);
if (isNaN(num)) return 'Normal';
if (parametro === 'Tiempo de Protrombina' && num > 50) {
if (num < 70) return 'Bajo';
if (num > 100) return 'Alto';
return 'Normal';
}
if (parametro === 'Calcio Iónico') {
if (num < 2) {
if (num < 1.12) return 'Bajo';
if (num > 1.32) return 'Alto';
return 'Normal';
} else {
if (num < 4.5) return 'Bajo';
if (num > 5.6) return 'Alto';
return 'Normal';
}
}
if (num < rango.min) return 'Bajo';
if (num > rango.max) return 'Alto';
return 'Normal';
@@ -183,6 +226,7 @@ export function HistoriaClinica({
allCamas,
evoluciones,
laboratorios,
glucemias,
acidosBase,
cultivos,
estudiosComplementarios,
@@ -193,6 +237,9 @@ export function HistoriaClinica({
onAgregarLaboratorio,
onActualizarLaboratorio,
onEliminarLaboratorio,
onAgregarGlucemia,
onActualizarGlucemia,
onEliminarGlucemia,
onAgregarAcidoBase,
onActualizarAcidoBase,
onEliminarAcidoBase,
@@ -356,7 +403,7 @@ export function HistoriaClinica({
<p className="text-gray-500 text-sm">Antecedentes de Enfermedad Actual</p>
<p className="font-medium">{internacion.antecedentesEnfermedadActual || 'Sin antecedentes registrados'}</p>
</div>
<Button variant="outline" className="bg-blue-50 border-blue-300 text-blue-700 hover:bg-blue-100" onClick={async (e) => {
<Button variant="outline" className="bg-blue-50 border-blue-300 text-blue-700 hover:bg-blue-100 dark:bg-blue-950/60 dark:border-blue-800 dark:text-blue-300 dark:hover:bg-blue-900" onClick={async (e) => {
e.stopPropagation();
try {
const { PDFDocument, StandardFonts, rgb } = await import('pdf-lib');
@@ -502,7 +549,7 @@ export function HistoriaClinica({
<TabsTrigger value="glucemias">
<Droplet className="h-3 w-3 sm:h-4 sm:w-4" />
<span>Glucemias</span>
({glucemias.length})
</TabsTrigger>
<TabsTrigger value="laboratorios">
<FlaskConical className="h-3 w-3 sm:h-4 sm:w-4" />
@@ -538,6 +585,10 @@ export function HistoriaClinica({
<ScrollBar orientation="horizontal" />
</ScrollArea>
<TabsContent value="glucemias" className="mt-4 min-w-0 w-full overflow-hidden">
<SeccionGlucemias glucemias={glucemias} patientId={paciente.id} add={onAgregarGlucemia} update={onActualizarGlucemia} del={onEliminarGlucemia} canEdit={canEdit} />
</TabsContent>
<TabsContent value="laboratorios" className="mt-4 min-w-0 w-full overflow-hidden">
<SeccionLaboratorios lab={laboratorios} patientId={paciente.id} add={onAgregarLaboratorio} update={onActualizarLaboratorio} del={onEliminarLaboratorio} addAcidoBase={onAgregarAcidoBase} canEdit={canEdit} />
</TabsContent>
@@ -570,12 +621,16 @@ export function HistoriaClinica({
<SeccionIndicaciones
recomendaciones={indicadores}
internacionId={internacion.id}
pacienteId={paciente.id}
add={onAgregarIndicacion}
update={onActualizarIndicacion}
del={onEliminarIndicacion}
movimientos={movimientos}
onAgregarMovimiento={onAgregarMovimiento}
canEdit={canEdit}
atbList={atb}
addATB={onAgregarATB}
updateATB={onActualizarATB}
/>
</TabsContent>
@@ -609,6 +664,264 @@ export function HistoriaClinica({
);
}
function SeccionGlucemias({ glucemias, patientId, add, update, del, canEdit }: {
glucemias: Glucemia[];
patientId: string;
add: (g: Omit<Glucemia, 'id'>) => void;
update: (id: string, data: Partial<Glucemia>) => void;
del: (id: string) => void;
canEdit?: boolean;
}) {
const [dialog, setDialog] = useState(false);
const [evolDialog, setEvolDialog] = useState(false);
const [edit, setEdit] = useState<Glucemia | null>(null);
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
const [hora, setHora] = useState(new Date().toTimeString().slice(0, 5));
const [valor, setValor] = useState('');
const [correccion, setCorreccion] = useState('');
const reset = () => {
setEdit(null);
setFecha(new Date().toISOString().split('T')[0]);
setHora(new Date().toTimeString().slice(0, 5));
setValor('');
setCorreccion('');
};
const loadEdit = (g: Glucemia) => {
setEdit(g);
setFecha(g.fecha);
setHora(g.hora || '');
setValor(g.valor.toString());
setCorreccion(g.correccion.toString());
setDialog(true);
};
const handleGuardar = () => {
const valNum = parseFloat(valor);
const corrNum = parseFloat(correccion);
if (isNaN(valNum)) {
toast.error('El valor de glucemia debe ser un número válido');
return;
}
if (isNaN(corrNum)) {
toast.error('El valor de corrección debe ser un número válido');
return;
}
if (edit) {
update(edit.id, {
fecha,
hora,
valor: valNum,
correccion: corrNum
});
toast.success('Glucemia actualizada correctamente');
} else {
add({
pacienteId: patientId,
fecha,
hora,
valor: valNum,
correccion: corrNum
});
toast.success('Glucemia registrada correctamente');
}
setDialog(false);
reset();
};
const listGlucemias = (glucemias || [])
.filter(g => g.pacienteId === patientId)
.sort((a, b) => {
const dateA = new Date(a.fecha + 'T' + (a.hora || '00:00')).getTime();
const dateB = new Date(b.fecha + 'T' + (b.hora || '00:00')).getTime();
return dateB - dateA;
});
const datosEvolucion = [...listGlucemias]
.reverse()
.map(g => ({
fechaHora: `${formatDateDDMMYYYY(g.fecha)} ${g.hora || ''}`,
glucemia: g.valor,
correccion: g.correccion
}));
return (
<div className="space-y-4 w-full max-w-full min-w-0">
<div className="flex justify-between">
<div className="flex gap-2">
{canEdit && (
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />Nueva
</Button>
)}
<Button variant="outline" onClick={() => setEvolDialog(true)} disabled={listGlucemias.length === 0}>
<TrendingUp className="h-4 w-4 mr-2" />Evolución
</Button>
</div>
</div>
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{edit ? 'Editar' : 'Nueva'} Glucemia</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="flex gap-4">
<div className="flex-1">
<Label>Fecha</Label>
<Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} />
</div>
<div className="flex-1">
<Label>Hora</Label>
<Input type="time" value={hora} onChange={e => setHora(e.target.value)} />
</div>
</div>
<div>
<Label>Glucemia (Mg%)</Label>
<Input
type="number"
placeholder="Ej: 110"
value={valor}
onChange={e => setValor(e.target.value)}
/>
</div>
<div>
<Label>Corrección (UI Insulina)</Label>
<Input
type="number"
placeholder="Ej: 2"
value={correccion}
onChange={e => setCorreccion(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>
<Dialog open={evolDialog} onOpenChange={setEvolDialog}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Evolución de Glucemias
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{datosEvolucion.length > 0 ? (
<div className="h-64 bg-muted/30 rounded-lg p-4">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={datosEvolucion}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis dataKey="fechaHora" tick={{ fontSize: 12 }} />
<YAxis yAxisId="left" tick={{ fontSize: 12 }} domain={['auto', 'auto']} />
<YAxis yAxisId="right" orientation="right" tick={{ fontSize: 12 }} domain={[0, 'auto']} />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '8px'
}}
/>
<Line
yAxisId="left"
name="Glucemia (mg%)"
type="monotone"
dataKey="glucemia"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={{ fill: 'hsl(var(--primary))', r: 4 }}
/>
<Line
yAxisId="right"
name="Corrección (UI)"
type="monotone"
dataKey="correccion"
stroke="#f43f5e"
strokeWidth={2}
dot={{ fill: '#f43f5e', r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
) : (
<div className="text-center py-8 text-gray-500">No hay registros de glucemia para graficar</div>
)}
</div>
<div className="flex justify-end gap-2 mt-4">
<Button onClick={() => setEvolDialog(false)}>Cerrar</Button>
</div>
</DialogContent>
</Dialog>
{listGlucemias.length === 0 ? (
<div className="text-center py-8 text-gray-500">No hay registros de glucemias.</div>
) : (
<div className="rounded-md border overflow-hidden">
<div className="max-h-[400px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Fecha y Hora</TableHead>
<TableHead>Glucemia (Mg%)</TableHead>
<TableHead>Corrección (UI)</TableHead>
<TableHead className="w-[100px]">+</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{listGlucemias.map(g => (
<TableRow key={g.id}>
<TableCell>{formatDateDDMMYYYY(g.fecha)} {g.hora || ''}</TableCell>
<TableCell className="font-semibold">{g.valor} Mg%</TableCell>
<TableCell>
{g.correccion > 0 ? (
<span className="text-rose-600 font-medium">{g.correccion} UI</span>
) : (
<span className="text-gray-400">Sin corrección</span>
)}
</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(g)}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => del(g.id)} className="text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Eliminar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
);
}
function SeccionLaboratorios({ lab, patientId, add, update, del, addAcidoBase, canEdit }: {
lab: Laboratorio[];
patientId: string;
@@ -732,76 +1045,220 @@ function SeccionLaboratorios({ lab, patientId, add, update, del, addAcidoBase, c
const parseLaboratorioTexto = (texto: string): { resultados: ResultadoLaboratorio[]; observaciones: string } => {
const resultados: ResultadoLaboratorio[] = [];
const observacionesExtra: string[] = [];
const lipidosEncontrados: { parametro: string; valor: number; unidad: string }[] = [];
const ferricosEncontrados: { parametro: string; valor: number; unidad: string }[] = [];
const indEncontrados: { parametro: string; valor: number; unidad: string }[] = [];
const mapeoParametros: Record<string, { nombre: string; unidad: string; esPrincipal: boolean }> = {
const mapeoParametros: { claves: string[]; nombre: string; unidad: string; esPrincipal: boolean; esAdicional?: boolean }[] = [
{ claves: ['hematocrito', 'hto'], nombre: 'Hematocrito', unidad: '%', esPrincipal: true },
{ claves: ['hemoglobina corpuscular media', 'hcm'], nombre: 'HCM', unidad: 'pg', esPrincipal: false },
{ claves: ['hemoglobina', 'hb'], nombre: 'Hemoglobina', unidad: 'g/dL', esPrincipal: true },
{ claves: ['volumen corpuscular medio', 'vcm'], nombre: 'VCM', unidad: 'fL', esPrincipal: false },
{ claves: ['rdw'], nombre: 'RDW', unidad: '%', esPrincipal: false },
{ claves: ['eritroblastos'], nombre: 'Eritroblastos', unidad: '%', esPrincipal: false },
{ claves: ['neutrófilos', 'neutrofilos'], nombre: 'Neutrófilos', unidad: '%', esPrincipal: false },
{ claves: ['linfocitos'], nombre: 'Linfocitos', unidad: '%', esPrincipal: false },
{ claves: ['monocitos'], nombre: 'Monocitos', unidad: '%', esPrincipal: false },
{ claves: ['eosinófilos', 'eosinofilos'], nombre: 'Eosinófilos', unidad: '%', esPrincipal: false },
{ claves: ['basófilos', 'basofilos'], nombre: 'Basófilos', unidad: '%', esPrincipal: false },
{ claves: ['recuento de leucocitos', 'glóbulos blancos', 'globulos blancos', 'leucocitos', 'gb'], nombre: 'Leucocitos', unidad: '10³/µl', esPrincipal: true },
{ claves: ['recuento de plaquetas', 'plaquetas', 'plaq'], nombre: 'Plaquetas', unidad: '10³/µl', esPrincipal: true },
{ claves: ['volumen plaquetario medio', 'vpm'], nombre: 'VPM', unidad: 'fL', esPrincipal: false },
{ claves: ['glucosa', 'glucemia'], nombre: 'Glucemia', unidad: 'mg/dL', esPrincipal: true },
{ claves: ['urea'], nombre: 'Urea', unidad: 'mg/dL', esPrincipal: true },
{ claves: ['creatinina', 'creat'], nombre: 'Creatinina', unidad: 'mg/dL', esPrincipal: true },
{ claves: ['mdrd'], nombre: 'Filtrado Glomerular (MDRD)', unidad: 'ml/min', esPrincipal: false },
{ claves: ['sodio', 'na'], nombre: 'Sodio', unidad: 'mEq/L', esPrincipal: true },
{ claves: ['potasio', 'k+', 'k'], nombre: 'Potasio', unidad: 'mEq/L', esPrincipal: true },
{ claves: ['cloro', 'cl-', 'cl'], nombre: 'Cloro', unidad: 'mEq/L', esPrincipal: true },
{ claves: ['bilirrubina total', 'bt'], nombre: 'Bilirrubina Total', unidad: 'mg/dL', esPrincipal: true },
{ claves: ['bilirrubina directa', 'bd'], nombre: 'Bilirrubina Directa', unidad: 'mg/dL', esPrincipal: true },
{ claves: ['got', 'ast'], nombre: 'GOT', unidad: 'U/L', esPrincipal: true },
{ claves: ['gpt', 'alt'], nombre: 'GPT', unidad: 'U/L', esPrincipal: true },
{ claves: ['proteínas totales', 'proteinas totales'], nombre: 'Proteínas Totales', unidad: 'g/dL', esPrincipal: false },
{ claves: ['tiempo de protrombina', 't.p.', 't.p', 'tp', 'protrombina'], nombre: 'Tiempo de Protrombina', unidad: '%', esPrincipal: true },
{ claves: ['rin', 'inr'], nombre: 'INR', unidad: '', esPrincipal: true },
{ claves: ['aptt', 'kptt'], nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
'hematocrito': { nombre: 'Hematocrito', unidad: '%', esPrincipal: true },
'hemoglobina corpuscular media': { nombre: 'HCM', unidad: 'pg', 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 },
'eritroblastos': { nombre: 'Eritroblastos', unidad: '%', esPrincipal: false },
'neutrófilos': { nombre: 'Neutrófilos', unidad: '%', esPrincipal: false },
'linfocitos': { nombre: 'Linfocitos', unidad: '%', esPrincipal: false },
'monocitos': { nombre: 'Monocitos', unidad: '%', esPrincipal: false },
'eosinófilos': { nombre: 'Eosinófilos', unidad: '%', esPrincipal: false },
'basófilos': { nombre: 'Basófilos', unidad: '%', esPrincipal: false },
'leucocitos': { nombre: 'Leucocitos', unidad: '10³/µl', esPrincipal: true },
'recuento de leucocitos': { nombre: 'Leucocitos', unidad: '10³/µl', esPrincipal: true },
'recuento de plaquetas': { nombre: 'Plaquetas', unidad: '10³/µl', esPrincipal: true },
'plaquetas': { nombre: 'Plaquetas', unidad: '10³/µl', esPrincipal: true },
'volumen plaquetario medio': { nombre: 'VPM', unidad: 'fL', esPrincipal: false },
'procalcitonina': { nombre: 'Procalcitonina', unidad: 'ng/mL', esPrincipal: false },
'glucosa': { nombre: 'Glucemia', unidad: 'mg/dL', esPrincipal: true },
'urea': { nombre: 'Urea', unidad: 'mg/dL', esPrincipal: true },
'creatinina': { nombre: 'Creatinina', unidad: 'mg/dL', esPrincipal: true },
'mdrd': { nombre: 'Filtrado Glomerular (MDRD)', unidad: 'ml/min', esPrincipal: false },
'sodio': { nombre: 'Sodio', unidad: 'mEq/L', esPrincipal: true },
'potasio': { nombre: 'Potasio', unidad: 'mEq/L', esPrincipal: true },
'cloro': { nombre: 'Cloro', unidad: 'mEq/L', esPrincipal: true },
'bilirrubina total': { nombre: 'Bilirrubina Total', unidad: 'mg/dL', esPrincipal: true },
'bilirrubina directa': { nombre: 'Bilirrubina Directa', unidad: 'mg/dL', esPrincipal: true },
'got': { nombre: 'GOT', unidad: 'U/L', esPrincipal: true },
'gpt': { nombre: 'GPT', unidad: 'U/L', esPrincipal: true },
'fosfatasa alcalina': { nombre: 'Fosfatasa Alcalina', unidad: 'U/L', esPrincipal: false },
'proteínas totales': { nombre: 'Proteínas Totales', unidad: 'g/dL', esPrincipal: false },
'albúmina': { nombre: 'Albúmina', unidad: 'g/dL', esPrincipal: false },
'tiempo de protrombina': { nombre: 'Tiempo de Protrombina', unidad: '%', esPrincipal: true },
'rin': { nombre: 'INR', unidad: '', esPrincipal: true },
'aptt': { nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
'kptt': { nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
// Additional requested determinations
{ claves: ['colesterol ldl', 'ldl-c', 'ldl', 'colest. ldl', 'colest ldl'], nombre: 'Colesterol LDL', unidad: 'mg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['colesterol no-hdl', 'colesterol no hdl', 'no-hdl', 'no hdl', 'no_hdl'], nombre: 'Colesterol No HDL', unidad: 'mg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['colesterol hdl', 'hdl-c', 'hdl', 'colest. hdl', 'colest hdl'], nombre: 'Colesterol HDL', unidad: 'mg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['colesterol total', 'colesterol', 'colest. total', 'col. total', 'colest total'], nombre: 'Colesterol Total', unidad: 'mg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['triglicéridos', 'trigliceridos', 'tg', 'triglicérido', 'triglicerido'], nombre: 'Triglicéridos', unidad: 'mg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['albúmina', 'albumina'], nombre: 'Albúmina', unidad: 'g/dL', esPrincipal: false, esAdicional: true },
{ claves: ['calcio total', 'calcio', 'ca total', 'ca+', 'ca++'], nombre: 'Calcio Total', unidad: 'mg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['fosfatasa alcalina', 'fal'], nombre: 'Fosfatasa Alcalina', unidad: 'U/L', esPrincipal: false, esAdicional: true },
{ claves: ['ldh', 'lactato deshidrogenasa', 'lactatodeshidrogenasa'], nombre: 'LDH', unidad: 'U/L', esPrincipal: false, esAdicional: true },
{ claves: ['nt-probnp', 'nt probnp', 'nt_probnp', 'probnp', 'pro-bnp', 'pro bnp'], nombre: 'NT-proBNP', unidad: 'pg/mL', esPrincipal: false, esAdicional: true },
{ claves: ['procalcitonina', 'pct'], nombre: 'Procalcitonina', unidad: 'ng/mL', esPrincipal: false, esAdicional: true },
{ claves: ['fósforo', 'fosforo', 'fosfemia'], nombre: 'Fósforo', unidad: 'mg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['magnesio', 'mg', 'magnesemia'], nombre: 'Magnesio', unidad: 'mg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['calcio ionico', 'calcio iónico', 'ca ionico', 'ca iónico', 'ca++ ionico', 'ca++ iónico', 'calcio_ionico'], nombre: 'Calcio Iónico', unidad: 'mmol/L', esPrincipal: false, esAdicional: true },
// Perfil Férrico requested determinations (must put Porcentaje Saturación before Transferrina)
{ claves: ['porcentaje de saturacion de transferrina', 'porcentaje de saturación de transferrina', 'saturacion de transferrina', 'saturación de transferrina', 'sat. transferrina', 'sat transferrina', '% sat', '% de sat', '% saturacion', '% saturación'], nombre: 'Porcentaje de Saturación de Transferrina', unidad: '%', esPrincipal: false, esAdicional: true },
{ claves: ['transferrina', 'transferrin'], nombre: 'Transferrina', unidad: 'mg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['hierro', 'sideremia', 'fe'], nombre: 'Hierro', unidad: 'µg/dL', esPrincipal: false, esAdicional: true },
{ claves: ['ferritina', 'ferritin'], nombre: 'Ferritina', unidad: 'ng/mL', esPrincipal: false, esAdicional: true },
{ claves: ['acido folico', 'ácido fólico', 'folato', 'folatos', 'folico', 'fólico'], nombre: 'Ácido Fólico', unidad: 'ng/mL', esPrincipal: false, esAdicional: true },
{ claves: ['vitamina b12', 'b12', 'vit. b12'], nombre: 'Vitamina B12', unidad: 'pg/mL', esPrincipal: false, esAdicional: true }
];
const matchClave = (lineaLower: string, clave: string): boolean => {
if (clave.length > 4) {
return lineaLower.includes(clave);
}
const escaped = clave.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(?:^|[^a-z0-9_])${escaped}(?:$|[^a-z0-9_])`, 'i');
return regex.test(lineaLower);
};
const lineas = texto.split('\n');
for (const linea of lineas) {
const lineaLower = linea.toLowerCase().trim();
if (!lineaLower) continue;
for (const [clave, info] of Object.entries(mapeoParametros)) {
if (lineaLower.includes(clave)) {
// buscar primer valor numérico después de cualquier texto (letra o palabra)
const match = linea.match(/[a-zA-ZáéíóúñÁÉÍÓÚÑ]+.*?(\d+\.?\d*)/);
for (const group of mapeoParametros) {
// Skip if this parameter was already found
if (resultados.some(r => r.parametro === group.nombre)) continue;
let matchedClave = false;
for (const clave of group.claves) {
if (group.nombre === 'Colesterol Total' && clave === 'colesterol') {
if (lineaLower.includes('ldl') || lineaLower.includes('hdl') || lineaLower.includes('no')) {
continue;
}
}
if (group.nombre === 'Transferrina' && clave === 'transferrina') {
if (lineaLower.includes('saturac') || lineaLower.includes('sat') || lineaLower.includes('%')) {
continue;
}
}
if (matchClave(lineaLower, clave)) {
matchedClave = true;
break;
}
}
if (matchedClave) {
// Extraer primer valor numérico (soporta enteros y decimales con punto o coma)
const match = linea.match(/[a-zA-ZáéíóúñÁÉÍÓÚÑ]+.*?(\d+(?:[.,]\d+)?)/);
if (match && match[1]) {
const valor = parseFloat(match[1].replace(',', '.'));
if (!isNaN(valor) && valor > 0 && valor < 1000) {
const nombreNormalizado = info.nombre;
if (!isNaN(valor) && valor > 0 && valor < 1000000) {
const nombreNormalizado = group.nombre;
let valorFinal = valor;
if (nombreNormalizado === 'Leucocitos' || nombreNormalizado === 'Plaquetas') {
valorFinal = valor * 1000;
if (valor < 200) valorFinal = valor * 1000;
}
if (info.esPrincipal) {
let unidadFinal = group.unidad;
if (nombreNormalizado === 'Tiempo de Protrombina') {
if (lineaLower.includes('seg') || lineaLower.includes('segundo')) {
unidadFinal = 'seg';
} else if (lineaLower.includes('%')) {
unidadFinal = '%';
}
}
if (group.esPrincipal || group.esAdicional) {
resultados.push({
parametro: nombreNormalizado,
valor: valorFinal,
unidad: info.unidad,
unidad: unidadFinal,
estado: calcularEstadoLaboratorio(nombreNormalizado, String(valorFinal))
});
const isLipido = [
'Colesterol Total',
'Colesterol LDL',
'Colesterol No HDL',
'Colesterol HDL',
'Triglicéridos'
].includes(nombreNormalizado);
const isFerrico = [
'Hierro',
'Transferrina',
'Porcentaje de Saturación de Transferrina',
'Ferritina',
'Ácido Fólico',
'Vitamina B12'
].includes(nombreNormalizado);
const isIndependiente = [
'Albúmina',
'Calcio Total',
'Fosfatasa Alcalina',
'LDH',
'NT-proBNP',
'Procalcitonina',
'Fósforo',
'Magnesio',
'Calcio Iónico'
].includes(nombreNormalizado);
if (isLipido) {
lipidosEncontrados.push({ parametro: nombreNormalizado, valor: valorFinal, unidad: unidadFinal });
} else if (isFerrico) {
ferricosEncontrados.push({ parametro: nombreNormalizado, valor: valorFinal, unidad: unidadFinal });
} else if (isIndependiente) {
indEncontrados.push({ parametro: nombreNormalizado, valor: valorFinal, unidad: unidadFinal });
}
}
break;
}
}
break;
}
}
}
if (lipidosEncontrados.length > 0) {
observacionesExtra.push('PERFIL LIPIDICO:');
const ordenLipidos = ['Colesterol Total', 'Colesterol LDL', 'Colesterol No HDL', 'Colesterol HDL', 'Triglicéridos'];
for (const nombre of ordenLipidos) {
const item = lipidosEncontrados.find(l => l.parametro === nombre);
if (item) {
observacionesExtra.push(`- ${item.parametro}: ${item.valor} ${item.unidad}`);
}
}
}
if (ferricosEncontrados.length > 0) {
if (observacionesExtra.length > 0) {
observacionesExtra.push('');
}
observacionesExtra.push('PERFIL FERRICO:');
const ordenFerricos = ['Hierro', 'Transferrina', 'Porcentaje de Saturación de Transferrina', 'Ferritina', 'Ácido Fólico', 'Vitamina B12'];
for (const nombre of ordenFerricos) {
const item = ferricosEncontrados.find(f => f.parametro === nombre);
if (item) {
observacionesExtra.push(`- ${item.parametro}: ${item.valor} ${item.unidad}`);
}
}
}
if (indEncontrados.length > 0) {
if (observacionesExtra.length > 0) {
observacionesExtra.push('');
}
const ordenInd = [
'Albúmina',
'Calcio Total',
'Fosfatasa Alcalina',
'LDH',
'NT-proBNP',
'Procalcitonina',
'Fósforo',
'Magnesio',
'Calcio Iónico'
];
for (const nombre of ordenInd) {
const item = indEncontrados.find(i => i.parametro === nombre);
if (item) {
observacionesExtra.push(`${item.parametro}: ${item.valor} ${item.unidad}`);
}
}
}
@@ -820,7 +1277,7 @@ function SeccionLaboratorios({ lab, patientId, add, update, del, addAcidoBase, c
let sato2: number | undefined;
let lactato: number | undefined;
let fio2: number | undefined;
let fecha = importFecha;
const fecha = importFecha;
for (const linea of lineas) {
const cleanLinea = linea.replace(/show_chart|list_alt|\t+/g, ' ').replace(/,/g, '.').replace(/\s+/g, ' ').trim().toLowerCase();
@@ -1346,7 +1803,7 @@ function SeccionEvoluciones({ evos, internacionId, add, update, del, canEdit }:
setEdit(e);
setFecha(e.fecha);
setHora(e.hora);
setMedico(e.medico);
setMedico(e.medico || '');
setTemperatura(e.signosVitales?.temperatura?.toString() || '');
setPresionSistolica(e.signosVitales?.presionSistolica?.toString() || '');
setPresionDiastolica(e.signosVitales?.presionDiastolica?.toString() || '');
@@ -1368,19 +1825,33 @@ function SeccionEvoluciones({ evos, internacionId, add, update, del, canEdit }:
const evosFiltered = evos.filter(e => e.internacionId === internacionId).sort((a, b) => new Date(b.fecha + 'T' + b.hora).getTime() - new Date(a.fecha + 'T' + a.hora).getTime());
const handle = () => {
if (!medico) return;
const handle = async () => {
if (!medico.trim()) {
toast.error('Ingrese el nombre del médico');
return;
}
const signosVitales = temperatura || presionSistolica || frecuenciaCardiaca
? { temperatura: temperatura ? parseFloat(temperatura) : undefined, presionSistolica: presionSistolica ? parseInt(presionSistolica) : undefined, presionDiastolica: presionDiastolica ? parseInt(presionDiastolica) : undefined, frecuenciaCardiaca: frecuenciaCardiaca ? parseInt(frecuenciaCardiaca) : undefined, frecuenciaRespiratoria: frecuenciaRespiratoria ? parseInt(frecuenciaRespiratoria) : undefined, saturacionO2: saturacionO2 ? parseInt(saturacionO2) : undefined }
: undefined;
const examenFisico = snc || cardiovascular || respiratorio || abdominal || genitourinario || pielAnexos || soma
? { SNC: snc || undefined, Cardiovascular: cardiovascular || undefined, Respiratorio: respiratorio || undefined, Abdominal: abdominal || undefined, Genitourinario: genitourinario || undefined, PielAnexos: pielAnexos || undefined, SOMA: soma || undefined }
: undefined;
const data = { fecha, hora, medico, signosVitales, examenFisico, novedades: novedades || undefined, comentario: comentario || undefined, pendientes: pendientes || undefined };
if (edit) update(edit.id, data);
else add({ internacionId, ...data, signosVitales, examenFisico });
setDialog(false);
reset();
const data = { fecha, hora, medico: medico.trim(), signosVitales, examenFisico, novedades: novedades || undefined, comentario: comentario || undefined, pendientes: pendientes || undefined };
try {
if (edit) {
await update(edit.id, data);
toast.success('Evolución actualizada correctamente');
} else {
await add({ internacionId, ...data, signosVitales, examenFisico });
toast.success('Evolución agregada correctamente');
}
setDialog(false);
reset();
} catch (err) {
console.error('Error al guardar evolución:', err);
toast.error('Error al guardar la evolución');
}
};
return (
@@ -1445,9 +1916,9 @@ function SeccionEvoluciones({ evos, internacionId, add, update, del, canEdit }:
{canEdit && <Button size="sm" variant="outline" className="text-red-600" onClick={() => del(e.id)}><Trash2 /></Button>}
</div>
</div>
{svText && <div className="bg-blue-50 p-2 rounded-lg"><p className="text-sm font-medium text-blue-800 flex items-center gap-2"><Activity className="h-4 w-4" />Signos Vitales: {svText}</p></div>}
{svText && <div className="bg-blue-50 dark:bg-blue-950/60 p-2 rounded-lg border border-transparent dark:border-blue-800/50"><p className="text-sm font-medium text-blue-800 dark:text-blue-300 flex items-center gap-2"><Activity className="h-4 w-4" />Signos Vitales: {svText}</p></div>}
{isExpanded && <>
{e.examenFisico && <div className="bg-green-50 dark:bg-green-900 p-3 rounded-lg border border-green-200 dark:border-green-700"><p className="text-sm font-medium text-green-800 mb-2">Examen Físico:</p><div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs">
{e.examenFisico && <div className="bg-green-50 dark:bg-green-950/60 p-3 rounded-lg border border-green-200 dark:border-green-800/50"><p className="text-sm font-medium text-green-800 dark:text-green-300 mb-2">Examen Físico:</p><div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs">
{e.examenFisico.SNC && <div><span className="font-medium">SNC:</span> {e.examenFisico.SNC}</div>}
{e.examenFisico.Cardiovascular && <div><span className="font-medium">CV:</span> {e.examenFisico.Cardiovascular}</div>}
{e.examenFisico.Respiratorio && <div><span className="font-medium">Resp:</span> {e.examenFisico.Respiratorio}</div>}
@@ -1456,9 +1927,9 @@ function SeccionEvoluciones({ evos, internacionId, add, update, del, canEdit }:
{e.examenFisico.PielAnexos && <div><span className="font-medium">Piel:</span> {e.examenFisico.PielAnexos}</div>}
{e.examenFisico.SOMA && <div><span className="font-medium">SOMA:</span> {e.examenFisico.SOMA}</div>}
</div></div>}
{e.novedades && <div className="bg-red-50 p-3 rounded-lg border border-red-200 mt-3"><p className="text-sm font-medium text-red-800 mb-1">Novedades:</p><p className="text-sm text-gray-800 whitespace-pre-wrap">{e.novedades}</p></div>}
{e.comentario && <div><p className="text-sm font-medium text-gray-700">Comentario:</p><p className="text-sm text-gray-600 whitespace-pre-wrap">{e.comentario}</p></div>}
{e.pendientes && <div className="bg-amber-50 p-3 rounded-lg border border-amber-200"><p className="text-sm font-medium text-amber-800 mb-1">Pendientes:</p><p className="text-sm text-amber-700 whitespace-pre-wrap">{e.pendientes}</p></div>}
{e.novedades && <div className="bg-red-50 dark:bg-red-950/60 p-3 rounded-lg border border-red-200 dark:border-red-800/50 mt-3"><p className="text-sm font-medium text-red-800 dark:text-red-300 mb-1">Novedades:</p><p className="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{e.novedades}</p></div>}
{e.comentario && <div><p className="text-sm font-medium text-gray-700 dark:text-gray-300">Comentario:</p><p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap">{e.comentario}</p></div>}
{e.pendientes && <div className="bg-amber-50 dark:bg-amber-950/60 p-3 rounded-lg border border-amber-200 dark:border-amber-800/50"><p className="text-sm font-medium text-amber-800 dark:text-amber-300 mb-1">Pendientes:</p><p className="text-sm text-amber-700 dark:text-amber-300 whitespace-pre-wrap">{e.pendientes}</p></div>}
</>}
</div>
</CardContent></Card>
@@ -1583,7 +2054,7 @@ function SeccionAcidosBase({ ab, patientId, add, update, del, canEdit }: {
<div><Label>Lactato</Label><Input type="number" step="0.1" value={lactato} onChange={e => setLactato(e.target.value)} placeholder="1.0" /></div>
<div><Label>FiO2</Label><Input type="number" value={fio2} onChange={e => setFio2(e.target.value)} placeholder="21" /></div>
</div>
{ph && pco2 && hco3 && <div className="bg-blue-50 p-3 rounded-lg border border-blue-200"><p className="text-sm font-medium text-blue-800 flex items-center gap-2"><Activity className="h-4 w-4" />Interpretación automática: {interpretarGasometria()}</p></div>}
{ph && pco2 && hco3 && <div className="bg-blue-50 dark:bg-blue-950/60 p-3 rounded-lg border border-blue-200 dark:border-blue-800/50"><p className="text-sm font-medium text-blue-800 dark:text-blue-300 flex items-center gap-2"><Activity className="h-4 w-4" />Interpretación automática: {interpretarGasometria()}</p></div>}
<div><Label>Interpretación / Comentarios</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[80px]" value={interpretacion} onChange={e => setInterpretacion(e.target.value)} placeholder="Interpretación clínica..." /></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={handle} disabled={!ph || !pco2 || !po2 || !hco3 || !be || !sato2}><Plus className="h-4 w-4 mr-2" />Guardar Gasometría</Button></div>
@@ -1607,7 +2078,7 @@ function SeccionAcidosBase({ ab, patientId, add, update, del, canEdit }: {
<div><Label>Lactato</Label><Input type="number" step="0.1" value={lactato} onChange={e => setLactato(e.target.value)} placeholder="1.0" /></div>
<div><Label>FiO2</Label><Input type="number" value={fio2} onChange={e => setFio2(e.target.value)} placeholder="0.21" /></div>
</div>
{ph && pco2 && hco3 && <div className="bg-blue-50 p-3 rounded-lg border border-blue-200"><p className="text-sm font-medium text-blue-800 flex items-center gap-2"><Activity className="h-4 w-4" />Interpretación automática: {interpretarGasometria()}</p></div>}
{ph && pco2 && hco3 && <div className="bg-blue-50 dark:bg-blue-950/60 p-3 rounded-lg border border-blue-200 dark:border-blue-800/50"><p className="text-sm font-medium text-blue-800 dark:text-blue-300 flex items-center gap-2"><Activity className="h-4 w-4" />Interpretación automática: {interpretarGasometria()}</p></div>}
<div><Label>Interpretación / Comentarios</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[80px]" value={interpretacion} onChange={e => setInterpretacion(e.target.value)} placeholder="Interpretación clínica..." /></div>
</div>
<div className="flex justify-end gap-2 mt-4"><Button variant="outline" onClick={() => setEditDialog(false)}><X className="h-4 w-4 mr-2" />Cancelar</Button><Button onClick={handleEdit} disabled={!ph || !pco2 || !po2 || !hco3 || !be || !sato2}><Save className="h-4 w-4 mr-2" />Guardar Cambios</Button></div>
@@ -1804,18 +2275,18 @@ function SeccionCultivos({ cults, patient, add, update, del, canEdit }: {
}
};
const handle = () => { add({ pacienteId: patient.id, fechaToma, protocolo: protocolo || undefined, tipoMuestra, observaciones: observaciones || undefined, estado: 'NAF/Pendiente' }); setDialog(false); reset(); };
const handle = () => { add({ pacienteId: patient.id, fechaToma, protocolo, tipoMuestra, observaciones: observaciones || undefined, estado: 'NAF/Pendiente' }); setDialog(false); reset(); };
const handleParcial = () => {
if (!selected) return;
update(selected.id, { protocolo: protocolo || undefined, estado: 'Parcial', germen: germen || undefined, sensible: sensible || undefined, resistente: resistente || undefined });
update(selected.id, { protocolo, estado: 'Parcial', germen, sensible, resistente });
setResDialog(false);
resetRes();
};
const handleDefinitivo = () => {
if (!selected) return;
update(selected.id, { protocolo: protocolo || undefined, fechaResultado, estado: estadoResultado, germen: germen || undefined, sensible: sensible || undefined, resistente: resistente || undefined });
update(selected.id, { protocolo, fechaResultado, estado: estadoResultado, germen, sensible, resistente });
setEditDialog(false);
resetRes();
};
@@ -1938,27 +2409,27 @@ function SeccionCultivos({ cults, patient, add, update, del, canEdit }: {
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(c.id)}><Trash2 /></Button>
</div>
</div>
{c.protocolo && <p className="text-xs text-gray-500">Protocolo: {c.protocolo}</p>}
{c.observaciones && <p className="text-sm text-gray-600 bg-gray-50 p-2 rounded">{c.observaciones}</p>}
{c.protocolo && <p className="text-xs text-gray-500 dark:text-gray-400">Protocolo: {c.protocolo}</p>}
{c.observaciones && <p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 p-2 rounded">{c.observaciones}</p>}
{c.estado === 'Parcial' && c.germen && (
<div className="bg-orange-50 p-3 rounded-lg border border-orange-200">
<p className="text-sm font-medium text-orange-800 flex items-center gap-2"><AlertCircle className="h-4 w-4" />PARCIAL - Germen: {c.germen}</p>
<div className="bg-orange-50 dark:bg-orange-950/60 p-3 rounded-lg border border-orange-200 dark:border-orange-800/50">
<p className="text-sm font-medium text-orange-800 dark:text-orange-300 flex items-center gap-2"><AlertCircle className="h-4 w-4" />PARCIAL - Germen: {c.germen}</p>
{(c.sensible || c.resistente) && (
<div className="mt-2 space-y-1">
{c.sensible && <p className="text-xs text-orange-700">Sensible: {c.sensible}</p>}
{c.resistente && <p className="text-xs text-orange-700">Resistente: {c.resistente}</p>}
{c.sensible && <p className="text-xs text-orange-700 dark:text-orange-300">Sensible: {c.sensible}</p>}
{c.resistente && <p className="text-xs text-orange-700 dark:text-orange-300">Resistente: {c.resistente}</p>}
</div>
)}
</div>
)}
{c.estado === 'Positivo' && c.germen && (
<div className="bg-red-50 p-3 rounded-lg border border-red-200">
<p className="text-sm font-medium text-red-800 flex items-center gap-2"><AlertCircle className="h-4 w-4" />Germen: {c.germen}</p>
{c.fechaResultado && <p className="text-xs text-red-600 mt-1">Resultado: {c.fechaResultado}</p>}
<div className="bg-red-50 dark:bg-red-950/60 p-3 rounded-lg border border-red-200 dark:border-red-800/50">
<p className="text-sm font-medium text-red-800 dark:text-red-300 flex items-center gap-2"><AlertCircle className="h-4 w-4" />Germen: {c.germen}</p>
{c.fechaResultado && <p className="text-xs text-red-600 dark:text-red-400 mt-1">Resultado: {c.fechaResultado}</p>}
{(c.sensible || c.resistente) && (
<div className="mt-2 space-y-2">
{c.sensible && <div><p className="text-xs font-medium text-green-700 mb-1">Sensible a:</p><p className="text-sm text-green-800">{c.sensible}</p></div>}
{c.resistente && <div><p className="text-xs font-medium text-red-700 mb-1">Resistente a:</p><p className="text-sm text-red-800">{c.resistente}</p></div>}
{c.sensible && <div><p className="text-xs font-medium text-green-700 dark:text-green-300 mb-1">Sensible a:</p><p className="text-sm text-green-800 dark:text-green-200">{c.sensible}</p></div>}
{c.resistente && <div><p className="text-xs font-medium text-red-700 dark:text-red-300 mb-1">Resistente a:</p><p className="text-sm text-red-800 dark:text-red-200">{c.resistente}</p></div>}
</div>
)}
</div>
@@ -2007,9 +2478,9 @@ function SeccionEstudiosComplementarios({ estudios, internacionId, pacienteId, a
const loadEdit = (e: EstudioComplementario) => {
setEdit(e);
setFecha(e.fecha);
setTipo(e.tipo);
setResultado(e.resultado);
setFecha(e.fecha || new Date().toISOString().split('T')[0]);
setTipo(e.tipo || '');
setResultado(e.resultado || '');
setDialog(true);
};
@@ -2170,8 +2641,8 @@ function SeccionInterconsultas({ interconsultas, pacienteId, internacionId, add,
const loadEdit = (ic: Interconsulta) => {
setEdit(ic);
setFecha(ic.fecha);
setServicioInterconsultado(ic.servicioInterconsultado);
setFecha(ic.fecha || new Date().toISOString().split('T')[0]);
setServicioInterconsultado(ic.servicioInterconsultado || '');
setMotivo(ic.motivo || '');
setRespuestaInterconsulta(ic.respuestaInterconsulta || '');
setRespuestaFecha(ic.respuestaFecha || '');
@@ -2331,8 +2802,8 @@ function SeccionATB({ atb, internacionId, pacienteId, add, update, del, canEdit
const loadEdit = (a: ATB) => {
setEdit(a);
setAntibiotico(a.antibiotico);
setFechaInicio(a.fechaInicio);
setAntibiotico(a.antibiotico || '');
setFechaInicio(a.fechaInicio || new Date().toISOString().split('T')[0]);
setFechaFinalizacion(a.fechaFinalizacion || '');
};
+24 -16
View File
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { toast } from 'sonner';
import type { Internacion, Paciente, Cama, Area, Evolucion, Laboratorio, Cultivo } from '@/types';
import { formatDateDDMMYYYY } from '@/lib/utils';
@@ -78,20 +79,28 @@ export function Internaciones({
setInternacionSeleccionada(null);
};
const handleIniciarInternacion = () => {
if (pacienteSeleccionado && camaSeleccionada && areaSeleccionada && motivoConsulta && enfermedadActual && medicoIngresante) {
onIniciarInternacion({
pacienteId: pacienteSeleccionado,
camaId: camaSeleccionada,
areaId: areaSeleccionada,
diagnosticoIngreso: diagnosticoIngreso || motivoConsulta,
motivoConsulta,
enfermedadActual,
antecedentesEnfermedadActual: antecedentesEnfermedadActual || undefined,
medicoIngresante,
});
resetFormularioNueva();
setDialogoNuevaAbierto(false);
const handleIniciarInternacion = async () => {
if (pacienteSeleccionado && camaSeleccionada && areaSeleccionada && (diagnosticoIngreso || motivoConsulta) && enfermedadActual && medicoIngresante) {
try {
await onIniciarInternacion({
pacienteId: pacienteSeleccionado,
camaId: camaSeleccionada,
areaId: areaSeleccionada,
diagnosticoIngreso: diagnosticoIngreso || motivoConsulta,
motivoConsulta,
enfermedadActual,
antecedentesEnfermedadActual: antecedentesEnfermedadActual || undefined,
medicoIngresante,
});
toast.success('Internación iniciada correctamente');
resetFormularioNueva();
setDialogoNuevaAbierto(false);
} catch (err) {
console.error('Error al iniciar internación:', err);
toast.error('Error al iniciar la internación');
}
} else {
toast.error('Por favor complete todos los campos obligatorios');
}
};
@@ -286,8 +295,7 @@ export function Internaciones({
<div>
<Label>Antecedentes de Enfermedad Actual</Label>
<textarea
className="w-full p-2 border rounded-md tex</p>
</div>t-sm min-h-[80px]"
className="w-full p-2 border rounded-md text-sm min-h-[80px]"
value={antecedentesEnfermedadActual}
onChange={(e) => setAntecedentesEnfermedadActual(e.target.value)}
placeholder="Antecedentes relevantes de la enfermedad actual..."
+8 -4
View File
@@ -13,7 +13,7 @@ import {
UserCog
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetDescription, SheetHeader } from '@/components/ui/sheet';
import type { Vista, Usuario } from '@/types';
@@ -59,7 +59,7 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
filteredMenuItems.push({ vista: 'usuarios', label: 'Usuarios', icon: UserCog });
}
const NavContent = () => (
const renderNavContent = () => (
<nav className="flex flex-col gap-2">
{filteredMenuItems.map((item) => {
const Icon = item.icon;
@@ -95,7 +95,7 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
</div>
</div>
<div className="flex-1 p-4 overflow-auto">
<NavContent />
{renderNavContent()}
</div>
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-600 dark:text-gray-300 mb-2">
@@ -148,6 +148,10 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-64 p-0 dark:bg-gray-800">
<SheetHeader className="sr-only">
<SheetTitle>Menú de navegación</SheetTitle>
<SheetDescription>Navegación principal de la aplicación</SheetDescription>
</SheetHeader>
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<span className="font-bold text-lg dark:text-white">Menú</span>
@@ -158,7 +162,7 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
</div>
</div>
<div className="p-4">
<NavContent />
{renderNavContent()}
</div>
<div className="p-4 border-t border-gray-200 dark:border-gray-700 space-y-2">
<Button
+12 -12
View File
@@ -79,20 +79,20 @@ export function MapaCamas({
const getEstadoColor = (cama: Cama) => {
if (cama.estado === 'Disponible') {
return 'bg-green-100 border-green-300 text-green-800 dark:bg-green-900 dark:border-green-700 dark:text-green-300';
return 'bg-emerald-100 border-emerald-300 text-emerald-800 dark:bg-emerald-950/80 dark:border-emerald-700/60 dark:text-emerald-200';
}
if (cama.estado === 'Reservada') {
return 'bg-orange-100 border-orange-300 text-orange-800 dark:bg-orange-900 dark:border-orange-700 dark:text-orange-300';
return 'bg-orange-100 border-orange-300 text-orange-800 dark:bg-orange-950/80 dark:border-orange-700/60 dark:text-orange-200';
}
if (cama.estado === 'Ocupada') {
// Tipos de aislamiento
const aislamientos = ['Aislamiento KPC', 'Aislamiento COVID', 'Aislamiento Clostridium', 'Aislamiento Neutropenico'];
if (aislamientos.includes(cama.tipo)) {
return 'bg-red-100 border-red-300 text-red-800 dark:bg-red-900 dark:border-red-700 dark:text-red-300';
return 'bg-red-100 border-red-300 text-red-800 dark:bg-red-950/80 dark:border-red-700/60 dark:text-red-200';
}
return 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900 dark:border-blue-700 dark:text-blue-300';
return 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-950/80 dark:border-blue-700/60 dark:text-blue-200';
}
return 'bg-amber-100 border-amber-300 text-amber-800 dark:bg-amber-900 dark:border-amber-700 dark:text-amber-300';
return 'bg-amber-100 border-amber-300 text-amber-800 dark:bg-amber-950/80 dark:border-amber-700/60 dark:text-amber-200';
};
const getEstadoIcono = (cama: Cama) => {
@@ -317,13 +317,13 @@ export function MapaCamas({
</div>
{cama.estado === 'Ocupada' && paciente && internacion && (
<div className="bg-gray-50 p-4 rounded-lg space-y-2">
<p className="font-medium">Paciente:</p>
<p className="text-lg">{paciente.apellido}, {paciente.nombre}</p>
<p className="text-sm text-gray-500">DNI: {paciente.dni}</p>
<p className="text-sm text-gray-500">Ingreso: {internacion.fechaIngresoClinica ? formatDateDDMMYYYY(internacion.fechaIngresoClinica) : 'N/A'}</p>
<p className="text-sm text-gray-500">Diagnóstico: {internacion.diagnosticoIngreso}</p>
<p className="text-sm text-gray-500">Médico: {internacion.medicoIngresante}</p>
<div className="bg-gray-50 dark:bg-gray-800/80 border border-transparent dark:border-gray-700/60 p-4 rounded-lg space-y-2">
<p className="font-medium text-gray-900 dark:text-gray-100">Paciente:</p>
<p className="text-lg font-semibold text-gray-900 dark:text-gray-100">{paciente.apellido}, {paciente.nombre}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">DNI: {paciente.dni}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Ingreso: {internacion.fechaIngresoClinica ? formatDateDDMMYYYY(internacion.fechaIngresoClinica) : 'N/A'}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Diagnóstico: {internacion.diagnosticoIngreso}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Médico: {internacion.medicoIngresante}</p>
</div>
)}
+114 -89
View File
@@ -1,11 +1,12 @@
import { useState } from 'react';
import { ArrowLeft, Save, X, Bed, Calendar, Stethoscope, User, ClipboardList } from 'lucide-react';
import { ArrowLeft, Save, X, Bed, Calendar, Stethoscope, User, Loader2 } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import type { Paciente, Cama, Area, Internacion } from '@/types';
interface NuevoIngresoProps {
@@ -24,7 +25,6 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
const [pacienteSeleccionado, setPacienteSeleccionado] = useState<string>('');
const [busqueda, setBusqueda] = useState('');
const [camaSeleccionada, setCamaSeleccionada] = useState('');
const [areaSeleccionada, setAreaSeleccionada] = useState('');
const [camaInput, setCamaInput] = useState('');
const [modoCama, setModoCama] = useState<'seleccionar' | 'escribir'>('seleccionar');
const [medico, setMedico] = useState('');
@@ -36,12 +36,13 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
const [antecedentesEnfermedadActual, setAntecedentesEnfermedadActual] = useState('');
const [apache, setApache] = useState('');
const [derivacion, setDerivacion] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const resetFormulario = () => {
setPacienteSeleccionado('');
setBusqueda('');
setCamaSeleccionada('');
setAreaSeleccionada('');
setCamaInput('');
setMedico('');
setFechaIngresoHospital('');
setFechaIngresoClinica('');
@@ -53,12 +54,10 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
setDerivacion('');
};
const areasDisponibles = areas.filter(a => {
const nombre = (a.nombre || '').trim().toLowerCase();
return nombre !== 'fuera de area';
});
const areaFueraDeArea = areas.find(a => (a.nombre || '').trim().toLowerCase() === 'fuera de area');
const areaFueraDeAreaId = areaFueraDeArea?.id || '';
const normalizeStr = (str?: string) => (str || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim().toLowerCase();
const areaFueraDeArea = areas.find(a => normalizeStr(a.nombre) === 'fuera de area');
const areaFueraDeAreaId = areaFueraDeArea?.id || areas[0]?.id || 'fuera-de-area';
const pacientesSinInternar = pacientes;
@@ -106,26 +105,50 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
});
const handleSubmit = async () => {
if (!pacienteSeleccionado || !medico || !diagnosticoIngreso || !enfermedadActual) {
alert('Por favor complete los campos obligatorios');
// Validaciones explícitas con feedback claro
if (!pacienteSeleccionado) {
toast.error('Debe seleccionar un paciente de la lista');
return;
}
let camaId = '';
let newAreaId = '';
if (modoCama === 'seleccionar' && !camaSeleccionada) {
toast.error('Debe seleccionar una cama para el paciente');
return;
}
if (modoCama === 'escribir') {
if (!camaInput.trim()) {
alert('Ingrese el número de cama');
return;
}
const numeroCama = camaInput.trim();
const camaExistente = camas.find(c => c.numero === numeroCama);
if (camaExistente) {
camaId = camaExistente.id;
newAreaId = camaExistente.areaId || '';
} else if (onAgregarCama) {
try {
if (modoCama === 'escribir' && !camaInput.trim()) {
toast.error('Debe ingresar el número de cama');
return;
}
if (!diagnosticoIngreso.trim()) {
toast.error('Debe completar el Diagnóstico de Ingreso');
return;
}
if (!enfermedadActual.trim()) {
toast.error('Debe completar la Enfermedad Actual');
return;
}
if (!medico.trim()) {
toast.error('Debe ingresar el nombre del Médico Ingresante');
return;
}
setIsSubmitting(true);
try {
let camaId = '';
let newAreaId = '';
if (modoCama === 'escribir') {
const numeroCama = camaInput.trim();
const camaExistente = camas.find(c => c.numero === numeroCama);
if (camaExistente) {
camaId = camaExistente.id;
newAreaId = camaExistente.areaId || areaFueraDeAreaId;
} else if (onAgregarCama) {
camaId = await onAgregarCama({
numero: numeroCama,
areaId: areaFueraDeAreaId,
@@ -133,87 +156,87 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
estado: 'Ocupada'
});
newAreaId = areaFueraDeAreaId;
} catch (err) {
console.error('Error al crear cama:', err);
alert('Error al crear la cama');
} else {
toast.error('No se puede crear la cama');
setIsSubmitting(false);
return;
}
} else {
alert('No se puede agregar una nueva cama');
return;
const camaElegida = sortedCamas.find(c => c.id === camaSeleccionada);
if (!camaElegida) {
toast.error('Cama no encontrada');
setIsSubmitting(false);
return;
}
camaId = camaSeleccionada;
newAreaId = camaElegida.areaId || areaFueraDeAreaId;
}
} else {
if (!camaSeleccionada) {
alert('Seleccione una cama');
return;
}
const camaElegida = sortedCamas.find(c => c.id === camaSeleccionada);
if (!camaElegida) {
alert('Cama no encontrada');
return;
}
newAreaId = camaElegida.areaId || areaFueraDeAreaId;
if (!newAreaId) {
alert('La cama no tiene un área asignada');
return;
}
camaId = camaSeleccionada;
await onIniciarInternacion({
pacienteId: pacienteSeleccionado,
camaId: camaId,
areaId: newAreaId,
medicoIngresante: medico.trim(),
diagnosticoIngreso: diagnosticoIngreso.trim(),
motivoConsulta: motivoConsulta.trim() || undefined,
enfermedadActual: enfermedadActual.trim(),
antecedentesEnfermedadActual: antecedentesEnfermedadActual.trim() || undefined,
fechaIngresoHospital: fechaIngresoHospital || undefined,
fechaIngresoClinica: fechaIngresoClinica || undefined,
apache: apache.trim() || undefined,
derivacion: derivacion.trim() || undefined,
});
toast.success('Ingreso registrado con éxito');
resetFormulario();
onVolver();
} catch (err) {
console.error('Error al guardar ingreso:', err);
toast.error('Ocurrió un error al guardar el ingreso');
} finally {
setIsSubmitting(false);
}
onIniciarInternacion({
pacienteId: pacienteSeleccionado,
camaId: camaId,
areaId: newAreaId,
medicoIngresante: medico,
diagnosticoIngreso,
motivoConsulta: motivoConsulta || undefined,
enfermedadActual,
antecedentesEnfermedadActual: antecedentesEnfermedadActual || undefined,
fechaIngresoHospital: fechaIngresoHospital || undefined,
fechaIngresoClinica: fechaIngresoClinica || undefined,
apache: apache || undefined,
derivacion: derivacion || undefined,
});
resetFormulario();
onVolver();
};
const selectedPaciente = pacientes.find(p => p.id === pacienteSeleccionado);
return (
<div className="space-y-6 dark:text-white">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={onVolver}>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={onVolver} disabled={isSubmitting} className="shrink-0">
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<User className="h-6 w-6 text-blue-600" />
Nuevo Ingreso
</h1>
<p className="text-gray-500 dark:text-gray-400">Formulario de internación</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Formulario de internación</p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => { resetFormulario(); onVolver(); }}>
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
<Button variant="outline" className="flex-1 sm:flex-initial" onClick={() => { resetFormulario(); onVolver(); }} disabled={isSubmitting}>
<X className="h-4 w-4 mr-2" />
Cancelar
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleSubmit}>
<Save className="h-4 w-4 mr-2" />
<Button className="bg-blue-600 hover:bg-blue-700 flex-1 sm:flex-initial" onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Save className="h-4 w-4 mr-2" />
)}
Guardar Ingreso
</Button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Columna 1: Datos del Paciente */}
<div className="space-y-6">
<Card className="min-h-[200px]">
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<User className="h-4 w-4" />
Datos del Paciente
</h3>
@@ -226,11 +249,11 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
/>
<div className="max-h-48 overflow-auto border rounded-md">
<div className="max-h-48 overflow-auto border rounded-md divide-y dark:divide-gray-800">
{(() => {
const q = busqueda.trim().toLowerCase();
if (!q) {
return <div className="p-2 text-sm text-gray-500">Escriba para buscar</div>;
return <div className="p-3 text-sm text-gray-500">Escriba para buscar</div>;
}
const matches = pacientesSinInternar.filter(p =>
p.apellido.toLowerCase().includes(q) ||
@@ -238,27 +261,27 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
p.dni.includes(q)
);
if (matches.length === 0) {
return <div className="p-2 text-sm text-gray-500">Sin resultados</div>;
return <div className="p-3 text-sm text-gray-500">Sin resultados</div>;
}
return matches.map(p => (
<button
key={p.id}
type="button"
onClick={() => setPacienteSeleccionado(p.id)}
className="w-full text-left p-2 hover:bg-gray-50 text-sm"
className="w-full text-left p-3 hover:bg-gray-50 dark:hover:bg-gray-800 text-sm transition-colors"
>
{p.apellido}, {p.nombre} DNI: {p.dni}
<span className="font-medium">{p.apellido}, {p.nombre}</span> DNI: {p.dni}
</button>
));
})()}
</div>
</>
) : (
<div className="flex items-center gap-2 flex-wrap">
<Badge className="bg-blue-100 text-blue-800">
<div className="flex items-center gap-2 flex-wrap pt-1">
<Badge className="bg-blue-100 text-blue-800 dark:bg-blue-950/80 dark:text-blue-300 dark:border dark:border-blue-800 text-sm py-1 px-3">
{selectedPaciente?.apellido}, {selectedPaciente?.nombre}
</Badge>
<Badge variant="outline">DNI: {selectedPaciente?.dni}</Badge>
<Badge variant="outline" className="text-sm py-1 px-3">DNI: {selectedPaciente?.dni}</Badge>
<Button size="sm" variant="ghost" onClick={() => { setPacienteSeleccionado(''); setBusqueda(''); }}>
Cambiar
</Button>
@@ -272,19 +295,21 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
<div className="space-y-6">
<Card className="min-h-[200px]">
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Bed className="h-4 w-4" />
Asignación de Cama y Área
</h3>
<div className="flex gap-2 mb-4">
<div className="flex flex-col sm:flex-row gap-2 mb-4">
<Button
className="flex-1 text-xs sm:text-sm"
variant={modoCama === 'seleccionar' ? 'default' : 'outline'}
onClick={() => setModoCama('seleccionar')}
>
Seleccionar Cama
</Button>
<Button
className="flex-1 text-xs sm:text-sm"
variant={modoCama === 'escribir' ? 'default' : 'outline'}
onClick={() => setModoCama('escribir')}
>
@@ -318,7 +343,7 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
value={camaInput}
onChange={(e) => setCamaInput(e.target.value)}
/>
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Esta cama será creada como "Fuera de área"
</p>
</div>
@@ -331,7 +356,7 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
<div className="space-y-6">
<Card>
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Calendar className="h-4 w-4" />
Datos de Ingreso
</h3>
@@ -379,7 +404,7 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
<div className="space-y-6">
<Card>
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900">
<h3 className="font-semibold text-gray-900 dark:text-white">
Datos Clínicos
</h3>
@@ -429,7 +454,7 @@ export function NuevoIngreso({ pacientes, camas, areas, onIniciarInternacion, on
<div className="space-y-6">
<Card>
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Stethoscope className="h-4 w-4" />
Médico Ingresante
</h3>
+2 -2
View File
@@ -86,7 +86,7 @@ export function Pacientes({
};
const handleGuardar = () => {
if (!nombre || !apellido || !dni || !fechaNacimiento || !telefono) return;
if (!nombre || !apellido || !dni || !fechaNacimiento) return;
const datos = {
nombre,
@@ -94,7 +94,7 @@ export function Pacientes({
dni,
fechaNacimiento,
sexo,
telefono,
telefono: telefono || undefined,
email: email || undefined,
direccion: direccion || undefined,
grupoSanguineo: grupoSanguineo || undefined,
+164 -64
View File
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
@@ -8,26 +9,32 @@ import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Plus, Pencil, Trash2, X } from 'lucide-react';
import type { Indicacion, IndicacionTipo, ViaAdministracion, TipoPlanHidratacion, TipoInsulina } from '@/types';
import type { Indicacion, IndicacionTipo, ViaAdministracion, TipoPlanHidratacion, TipoInsulina, ATB } from '@/types';
export function SeccionIndicaciones({ recomendaciones, internacionId, add, update, del, movimientos, onAgregarMovimiento, canEdit }: {
export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId, add, update, del, movimientos, onAgregarMovimiento, canEdit, atbList = [], addATB, updateATB }: {
recomendaciones: Indicacion[];
internacionId: string;
add: (i: Omit<Indicacion, 'id'>) => void;
pacienteId?: string;
add: (i: Omit<Indicacion, 'id'>) => Promise<unknown>;
update: (id: string, datos: Partial<Indicacion>) => void;
del: (id: string) => void;
movimientos?: any[];
onAgregarMovimiento?: (m: any) => void;
movimientos?: unknown[];
onAgregarMovimiento?: (m: unknown) => void;
canEdit?: boolean;
atbList?: ATB[];
addATB?: (a: Omit<ATB, 'id'>) => Promise<unknown>;
updateATB?: (id: string, datos: Partial<ATB>) => void;
}) {
const list: Indicacion[] = Array.isArray(recomendaciones) ? recomendaciones : [];
const movs: any[] = Array.isArray(movimientos) ? movimientos : [];
const movs = Array.isArray(movimientos) ? movimientos : [];
const [dialog, setDialog] = useState(false);
const [edit, setEdit] = useState<Indicacion | null>(null);
const [deleteConfirmInd, setDeleteConfirmInd] = useState<Indicacion | null>(null);
const [deleteMedico, setDeleteMedico] = useState<string>('');
const formatIndicacion = (i: Indicacion): string => {
if (i.tipo === 'Farmacologica' || i.tipo === 'Farmacologica Profilactica') return `${i.droga || ''} ${i.dosis || ''} ${i.frecuenciaHoras ? `c/${i.frecuenciaHoras}hs` : ''} ${i.via || ''}`.trim();
if (i.tipo === 'Farmacologica Insulina') return `${i.tipoInsulina || ''}${i.unidadesDesayuno ? ` - Desayuno: ${i.unidadesDesayuno}U` : ''}${i.unidadesAlmuerzo ? ` - Almuerzo: ${i.unidadesAlmuerzo}U` : ''}${i.unidadesNoche ? ` - 23hs: ${i.unidadesNoche}U` : ''}`.trim();
if (i.tipo === 'Farmacologica' || i.tipo === 'Farmacologica Profilactica' || i.tipo === 'Farmacologica Antibiótico') return `${i.droga || ''} ${i.dosis || ''} ${i.frecuenciaHoras ? `c/${i.frecuenciaHoras}hs` : ''} ${i.via || ''}`.trim();
if (i.tipo === 'Farmacologica Insulina') return `${i.tipoInsulina || ''}${i.unidadesDesayuno ? ` - PreDesayuno: ${i.unidadesDesayuno}U` : ''}${i.unidadesAlmuerzo ? ` - PreAlmuerzo: ${i.unidadesAlmuerzo}U` : ''}${i.unidadesNoche ? ` - 23hs: ${i.unidadesNoche}U` : ''}`.trim();
if (i.tipo === 'No Farmacologica') return i.indicacionNoFco || '';
if (i.tipo === 'PHP' || i.tipo === 'PHP Paralelo') return `${i.tipoPlan || ''} ${i.cantidadMl || ''}ml en ${i.tiempoHoras || ''}hs`;
if (i.tipo === 'PHP Alterno') return `${i.tipoPlan || ''} ${i.cantidadMl || ''}ml + ${i.tipoPlan2 || ''} ${i.cantidadMl2 || ''}ml en ${i.tiempoHoras || ''}hs`;
@@ -47,13 +54,16 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
};
const sortedMovs = [...movs].sort((a, b) => {
let aVal: any = a[sortField] || '';
let bVal: any = b[sortField] || '';
const recordA = a as Record<string, unknown>;
const recordB = b as Record<string, unknown>;
const aVal = String(recordA[sortField] || '');
const bVal = String(recordB[sortField] || '');
if (sortField === 'fecha') {
aVal = new Date(aVal.replace(' ', 'T')).getTime();
bVal = new Date(bVal.replace(' ', 'T')).getTime();
const timeA = new Date(aVal.replace(' ', 'T')).getTime();
const timeB = new Date(bVal.replace(' ', 'T')).getTime();
return sortDir === 'asc' ? timeA - timeB : timeB - timeA;
}
return sortDir === 'asc' ? (aVal > bVal ? 1 : -1) : (aVal < bVal ? 1 : -1);
return sortDir === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
});
const [droga, setDroga] = useState('');
const [dosis, setDosis] = useState('');
@@ -75,7 +85,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
const vias: ViaAdministracion[] = ['Via Oral', 'EV', 'IM', 'SC', 'Por GGT', 'Por SNG'];
const planes: TipoPlanHidratacion[] = ['SF 0.9%', 'Dextrosa 5%', 'Dextrosa 10%', 'Dextrosa 25%', 'Ringer Lactato'];
const tiposInsulina: TipoInsulina[] = ['NPH', 'Glargina'];
const tipos: IndicacionTipo[] = ['Farmacologica', 'Farmacologica Profilactica', 'Farmacologica Insulina', 'No Farmacologica', 'PHP', 'PHP Paralelo', 'PHP Alterno'];
const tipos: IndicacionTipo[] = ['Farmacologica', 'Farmacologica Antibiótico', 'Farmacologica Profilactica', 'Farmacologica Insulina', 'No Farmacologica', 'PHP', 'PHP Paralelo', 'PHP Alterno'];
const reset = () => {
setEdit(null);
@@ -114,19 +124,19 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
setUnidadesDesayuno(i.unidadesDesayuno || '');
setUnidadesAlmuerzo(i.unidadesAlmuerzo || '');
setUnidadesNoche(i.unidadesNoche || '');
setMedico(i.medicoCrea);
setMedico(i.medicoCrea || '');
setDialog(true);
};
const handleGuardar = () => {
const handleGuardar = async () => {
if (!medico.trim()) return;
if ((tipo === 'Farmacologica' || tipo === 'Farmacologica Profilactica') && (!droga.trim() || !dosis.trim())) return;
if ((tipo === 'Farmacologica' || tipo === 'Farmacologica Profilactica' || tipo === 'Farmacologica Antibiótico') && (!droga.trim() || !dosis.trim())) return;
if (tipo === 'Farmacologica Insulina' && (!unidadesDesayuno && !unidadesAlmuerzo && !unidadesNoche)) return;
if (tipo === 'No Farmacologica' && !indicacionNoFco.trim()) return;
if ((tipo === 'PHP' || tipo === 'PHP Paralelo') && !cantidadMl) return;
if (tipo === 'PHP Alterno' && (!cantidadMl || !cantidadMl2)) return;
const data: any = {
const data: Partial<Indicacion> = {
internacionId,
tipo,
estado: 'Activa',
@@ -134,7 +144,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
fechaCrea: new Date().toISOString().split('T')[0],
};
if (tipo === 'Farmacologica' || tipo === 'Farmacologica Profilactica') {
if (tipo === 'Farmacologica' || tipo === 'Farmacologica Profilactica' || tipo === 'Farmacologica Antibiótico') {
data.droga = droga;
data.dosis = dosis;
data.frecuenciaHoras = frecuenciaHoras || undefined;
@@ -162,7 +172,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
const fecha = now.toISOString().split('T')[0] + ' ' + now.toTimeString().split(' ')[0].substring(0, 5);
if (edit) {
if (onAgregarMovimiento) {
onAgregarMovimiento({
await onAgregarMovimiento({
indicacionId: edit.id,
internacionId,
tipo: 'Modificacion',
@@ -172,14 +182,26 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
indicacionNueva: formatIndicacion({ ...edit, ...data } as Indicacion),
});
}
update(edit.id, data);
await update(edit.id, data);
} else {
const newId = add(data);
if (onAgregarMovimiento && newId) {
onAgregarMovimiento({
indicacionId: newId,
const newId = await add(data);
if (tipo === 'Farmacologica Antibiótico' && addATB && pacienteId) {
try {
await addATB({
pacienteId,
internacionId,
antibiotico: droga,
fechaInicio: data.fechaCrea,
});
} catch (atbErr) {
console.error('Error al agregar ATB automáticamente:', atbErr);
}
}
if (onAgregarMovimiento) {
await onAgregarMovimiento({
indicacionId: typeof newId === 'string' ? newId : '',
internacionId,
tipo: 'Indicacion',
tipo: 'Nueva',
fecha,
profesional: medico,
indicacionNueva: formatIndicacion({ ...data, id: newId } as Indicacion),
@@ -190,31 +212,47 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
reset();
};
const handleSuspender = (i: Indicacion, suspendioMedico: string) => {
const handleSuspender = async (i: Indicacion, suspendioMedico: string) => {
const now = new Date();
const fecha = now.toISOString().split('T')[0] + ' ' + now.toTimeString().split(' ')[0].substring(0, 5);
if (onAgregarMovimiento) {
onAgregarMovimiento({
indicacionId: i.id,
internacionId,
tipo: 'Suspencion',
fecha,
profesional: suspendioMedico,
indicacionPrevia: formatIndicacion(i),
});
}
del(i.id);
};
try {
if (onAgregarMovimiento) {
await onAgregarMovimiento({
indicacionId: i.id,
internacionId,
tipo: 'Suspencion',
fecha,
profesional: suspendioMedico,
indicacionPrevia: formatIndicacion(i),
});
}
const handleModificar = (i: Indicacion, modificoMedico: string) => {
update(i.id, {
medicoModifica: modificoMedico,
});
if (i.tipo === 'Farmacologica Antibiótico' && updateATB && atbList) {
const fechaFin = now.toISOString().split('T')[0];
const activeATB = atbList.find(a =>
a.internacionId === internacionId &&
a.antibiotico.toLowerCase() === (i.droga || '').toLowerCase() &&
!a.fechaFinalizacion
);
if (activeATB) {
try {
await updateATB(activeATB.id, { fechaFinalizacion: fechaFin });
} catch (atbErr) {
console.error('Error al finalizar ATB automáticamente:', atbErr);
}
}
}
await del(i.id);
toast.success('Indicación eliminada correctamente');
} catch (err) {
console.error('Error al eliminar indicación:', err);
toast.error('Error al eliminar la indicación');
}
};
const indsFilter = list.filter(ind => ind.internacionId === internacionId);
const activas = indsFilter.filter(ind => ind.estado === 'Activa');
const historial = indsFilter.filter(ind => ind.estado === 'Suspendida' || ind.fechaModificacion);
return (
<div className="space-y-4">
@@ -248,7 +286,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
<Input value={medico} onChange={e => setMedico(e.target.value)} placeholder="Nombre del médico" />
</div>
{tipo === 'Farmacologica' && (
{(tipo === 'Farmacologica' || tipo === 'Farmacologica Antibiótico') && (
<>
<div><Label>Droga *</Label><Input value={droga} onChange={e => setDroga(e.target.value)} placeholder="Nombre del medicamento" /></div>
<div><Label>Dosis *</Label><Input value={dosis} onChange={e => setDosis(e.target.value)} placeholder="Ej: 500mg, 1g, 10ml" /></div>
@@ -272,9 +310,18 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
<>
<div><Label>Tipo de Insulina</Label><Select value={tipoInsulina} onValueChange={(v: TipoInsulina) => setTipoInsulina(v)}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent>{tiposInsulina.map(t => <SelectItem key={t} value={t}>{t}</SelectItem>)}</SelectContent></Select></div>
<div className="grid grid-cols-3 gap-2">
<div><Label>Pre Desayuno (U)</Label><Input type="number" value={unidadesDesayuno} onChange={e => setUnidadesDesayuno(e.target.value ? parseInt(e.target.value) : '')} placeholder="U" /></div>
<div><Label>Pre Almuerzo (U)</Label><Input type="number" value={unidadesAlmuerzo} onChange={e => setUnidadesAlmuerzo(e.target.value ? parseInt(e.target.value) : '')} placeholder="U" /></div>
<div><Label>23hs (U)</Label><Input type="number" value={unidadesNoche} onChange={e => setUnidadesNoche(e.target.value ? parseInt(e.target.value) : '')} placeholder="U" /></div>
<div className="flex flex-col justify-end space-y-1.5">
<Label className="text-xs sm:text-sm">Pre Desayuno (U)</Label>
<Input type="number" value={unidadesDesayuno} onChange={e => setUnidadesDesayuno(e.target.value ? parseInt(e.target.value) : '')} placeholder="U" />
</div>
<div className="flex flex-col justify-end space-y-1.5">
<Label className="text-xs sm:text-sm">Pre Almuerzo (U)</Label>
<Input type="number" value={unidadesAlmuerzo} onChange={e => setUnidadesAlmuerzo(e.target.value ? parseInt(e.target.value) : '')} placeholder="U" />
</div>
<div className="flex flex-col justify-end space-y-1.5">
<Label className="text-xs sm:text-sm">23hs (U)</Label>
<Input type="number" value={unidadesNoche} onChange={e => setUnidadesNoche(e.target.value ? parseInt(e.target.value) : '')} placeholder="U" />
</div>
</div>
</>
)}
@@ -329,13 +376,16 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
{ind.tipo === 'Farmacologica' && (
<div className="font-medium">{ind.droga} {ind.dosis} {ind.frecuenciaHoras ? `c/${ind.frecuenciaHoras}hs` : ''} {ind.via}</div>
)}
{ind.tipo === 'Farmacologica Antibiótico' && (
<div className="font-medium">{ind.droga} {ind.dosis} {ind.frecuenciaHoras ? `c/${ind.frecuenciaHoras}hs` : ''} {ind.via} <Badge className="ml-2 bg-purple-100 text-purple-800 dark:bg-purple-950 dark:text-purple-300 hover:bg-purple-100">Antibiótico</Badge></div>
)}
{ind.tipo === 'Farmacologica Profilactica' && (
<div className="font-medium">{ind.droga} {ind.dosis} {ind.frecuenciaHoras ? `c/${ind.frecuenciaHoras}hs` : ''} {ind.via}</div>
)}
{ind.tipo === 'Farmacologica Insulina' && (
<div className="font-medium">{ind.tipoInsulina}
{ind.unidadesDesayuno && ` - Desayuno: ${ind.unidadesDesayuno}U`}
{ind.unidadesAlmuerzo && ` - Almuerzo: ${ind.unidadesAlmuerzo}U`}
{ind.unidadesDesayuno && ` - PreDesayuno: ${ind.unidadesDesayuno}U`}
{ind.unidadesAlmuerzo && ` - PreAlmuerzo: ${ind.unidadesAlmuerzo}U`}
{ind.unidadesNoche && ` - 23hs: ${ind.unidadesNoche}U`}
</div>
)}
@@ -350,16 +400,13 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
)}
</div>
<div className="flex gap-1">
<Button size="sm" variant="outline" className="text-orange-600" onClick={() => {
const med = prompt('Médico que suspende:');
if (med) handleSuspender(ind, med);
}}>
<X className="h-4 w-4" />
</Button>
{canEdit && <Button size="sm" variant="outline" onClick={() => loadEdit(ind)}>
{canEdit && <Button size="sm" variant="outline" title="Editar" onClick={() => loadEdit(ind)}>
<Pencil className="h-4 w-4" />
</Button>}
{canEdit && <Button size="sm" variant="outline" className="text-red-600" onClick={() => del(ind.id)}>
{canEdit && <Button size="sm" variant="outline" className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/30" title="Eliminar" onClick={() => {
setDeleteConfirmInd(ind);
setDeleteMedico(ind.medicoCrea || '');
}}>
<Trash2 className="h-4 w-4" />
</Button>}
</div>
@@ -369,6 +416,53 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
</div>
)}
{deleteConfirmInd && (
<Dialog open={!!deleteConfirmInd} onOpenChange={(open) => { if (!open) setDeleteConfirmInd(null); }}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="text-red-600 flex items-center gap-2">
<Trash2 className="h-5 w-5" />
Confirmar eliminación de indicación
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<p className="text-sm text-muted-foreground">
¿Está seguro de que desea suspender/eliminar esta indicación? Se registrará la baja en el historial.
</p>
<div className="p-3 bg-muted rounded-md text-sm font-medium">
<span className="text-xs text-muted-foreground block mb-1">Indicación:</span>
{formatIndicacion(deleteConfirmInd)}
</div>
<div className="space-y-1.5">
<Label htmlFor="delete-medico">Médico que suspende / elimina *</Label>
<Input
id="delete-medico"
value={deleteMedico}
onChange={(e) => setDeleteMedico(e.target.value)}
placeholder="Nombre del profesional..."
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-2">
<Button variant="outline" onClick={() => setDeleteConfirmInd(null)}>
Cancelar
</Button>
<Button
variant="destructive"
onClick={async () => {
const med = deleteMedico.trim() || deleteConfirmInd.medicoCrea || 'Médico';
const indToDel = deleteConfirmInd;
setDeleteConfirmInd(null);
await handleSuspender(indToDel, med);
}}
>
Eliminar
</Button>
</div>
</DialogContent>
</Dialog>
)}
{showHistorial && (
<Dialog open={showHistorial} onOpenChange={setShowHistorial}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-auto">
@@ -381,13 +475,13 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
<Table>
<TableHeader>
<TableRow>
<TableHead className="cursor-pointer hover:bg-gray-100" onClick={() => handleSort('fecha')}>
<TableHead className="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800" onClick={() => handleSort('fecha')}>
Fecha {sortField === 'fecha' && (sortDir === 'asc' ? '↑' : '↓')}
</TableHead>
<TableHead className="cursor-pointer hover:bg-gray-100" onClick={() => handleSort('profesional')}>
<TableHead className="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800" onClick={() => handleSort('profesional')}>
Profesional {sortField === 'profesional' && (sortDir === 'asc' ? '↑' : '↓')}
</TableHead>
<TableHead className="cursor-pointer hover:bg-gray-100" onClick={() => handleSort('tipo')}>
<TableHead className="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800" onClick={() => handleSort('tipo')}>
Tipo {sortField === 'tipo' && (sortDir === 'asc' ? '↑' : '↓')}
</TableHead>
<TableHead>Indicación</TableHead>
@@ -399,8 +493,14 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
<TableCell>{mov.fecha}</TableCell>
<TableCell>{mov.profesional}</TableCell>
<TableCell>
<Badge className={mov.tipo === 'Indicacion' ? 'bg-green-500' : mov.tipo === 'Suspencion' ? 'bg-red-500' : 'bg-orange-500'}>
{mov.tipo === 'Indicacion' ? 'Indicación' : mov.tipo === 'Suspencion' ? 'Suspensión' : 'Modificación'}
<Badge className={
mov.tipo === 'Nueva' || mov.tipo === 'Indicacion'
? 'bg-green-600 hover:bg-green-700 text-white'
: mov.tipo === 'Suspencion' || mov.tipo === 'Suspensión' || mov.tipo === 'Eliminacion'
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-orange-500 hover:bg-orange-600 text-white'
}>
{mov.tipo === 'Nueva' || mov.tipo === 'Indicacion' ? 'Nueva' : mov.tipo === 'Suspencion' || mov.tipo === 'Suspensión' || mov.tipo === 'Eliminacion' ? 'Suspensión' : 'Modificación'}
</Badge>
</TableCell>
<TableCell>
@@ -409,7 +509,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, add, updat
<div className="text-red-500 line-through">{mov.indicacionPrevia}</div>
<div className="text-green-500">{mov.indicacionNueva}</div>
</div>
) : mov.tipo === 'Suspencion' ? mov.indicacionPrevia : mov.indicacionNueva}
) : (mov.tipo === 'Suspencion' || mov.tipo === 'Suspensión' || mov.tipo === 'Eliminacion') ? mov.indicacionPrevia : mov.indicacionNueva || mov.indicacionPrevia}
</TableCell>
</TableRow>
))}
+11 -1
View File
@@ -112,6 +112,16 @@ export interface ResultadoLaboratorio {
estado: 'Normal' | 'Alto' | 'Bajo' | 'Crítico';
}
export interface Glucemia {
id: string;
pacienteId: string;
internacionId?: string;
fecha: string;
hora: string;
valor: number; // in mg%
correccion: number; // in UI insulin
}
export interface AcidoBase {
id: string;
pacienteId: string;
@@ -173,7 +183,7 @@ export interface ATB {
fechaFinalizacion?: string;
}
export type IndicacionTipo = 'Farmacologica' | 'Farmacologica Profilactica' | 'Farmacologica Insulina' | 'No Farmacologica' | 'PHP' | 'PHP Paralelo' | 'PHP Alterno';
export type IndicacionTipo = 'Farmacologica' | 'Farmacologica Profilactica' | 'Farmacologica Antibiótico' | 'Farmacologica Insulina' | 'No Farmacologica' | 'PHP' | 'PHP Paralelo' | 'PHP Alterno';
export type TipoInsulina = 'NPH' | 'Glargina';
export type ViaAdministracion = 'Via Oral' | 'EV' | 'IM' | 'SC' | 'Por GGT' | 'Por SNG';
export type TipoPlanHidratacion = 'SF 0.9%' | 'Dextrosa 5%' | 'Dextrosa 10%' | 'Dextrosa 25%' | 'Ringer Lactato';