Actualizar control de glucemias con eje Y secundario para correcciones e integracion de tienda
This commit is contained in:
+12
-28
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || '');
|
||||
};
|
||||
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user