Files
administracionHospital/src/sections/HistoriaClinica.tsx
T

3879 lines
187 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { toast } from 'sonner';
import { User } from 'lucide-react';
import { useHospitalStore } from '@/hooks/useHospitalStore';
import { getNombreProfesional } from '@/lib/utils';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
FlaskConical,
Microscope,
Plus,
Calendar,
Droplet,
AlertCircle,
CheckCircle2,
ArrowLeft,
Pencil,
Trash2,
FileText,
List,
FileDown,
Thermometer,
Heart,
Wind,
Droplets,
Save,
X,
MoreHorizontal,
Eye,
Edit,
ClipboardList,
TrendingUp,
Activity,
Pill,
Users,
ListTodo,
CheckSquare,
Square,
Copy,
Clock,
Search,
ChevronDown
} 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 { Textarea } from '@/components/ui/textarea';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGroup, SelectLabel } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
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, Glucemia, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario, Interconsulta, ATB, Indicacion, MovimientoIndicacion, Pendiente, OtroLaboratorio } from '@/types';
import { formatDateDDMMYYYY, getLocalToday } from '@/lib/utils';
import { SeccionIndicaciones } from './SeccionIndicaciones';
interface HistoriaClinicaProps {
internacion: Internacion;
paciente: Paciente;
cama?: Cama;
evoluciones: Evolucion[];
laboratorios: Laboratorio[];
glucemias: Glucemia[];
acidosBase: AcidoBase[];
cultivos: Cultivo[];
estudiosComplementarios: EstudioComplementario[];
interconsultas: Interconsulta[];
atb: ATB[];
indicadores: Indicacion[];
movimientos?: MovimientoIndicacion[];
canEdit: boolean;
onAgregarEvolucion: (evolucion: Omit<Evolucion, 'id'>) => void;
onActualizarEvolucion: (id: string, datos: Partial<Evolucion>) => void;
onEliminarEvolucion: (id: string) => void;
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;
onEliminarAcidoBase: (id: string) => void;
onAgregarCultivo: (cultivo: Omit<Cultivo, 'id'>) => void;
onActualizarCultivo: (id: string, datos: Partial<Cultivo>) => void;
onEliminarCultivo: (id: string) => void;
onAgregarEstudioComplementario: (estudio: Omit<EstudioComplementario, 'id'>) => void;
onActualizarEstudioComplementario: (id: string, datos: Partial<EstudioComplementario>) => void;
onEliminarEstudioComplementario: (id: string) => void;
onAgregarInterconsulta: (interconsulta: Omit<Interconsulta, 'id'>) => void;
onActualizarInterconsulta: (id: string, datos: Partial<Interconsulta>) => void;
onEliminarInterconsulta: (id: string) => void;
onAgregarATB: (atb: Omit<ATB, 'id'>) => void;
onActualizarATB: (id: string, datos: Partial<ATB>) => void;
onEliminarATB: (id: string) => void;
onAgregarIndicacion: (indicacion: Omit<Indicacion, 'id'>) => void;
onActualizarIndicacion: (id: string, datos: Partial<Indicacion>) => void;
onEliminarIndicacion: (id: string) => void;
onAgregarMovimiento?: (movimiento: MovimientoIndicacion) => void;
onVolver: () => void;
onEditarIngreso?: () => void;
}
const RANGOS_LABORATORIO: Record<string, { min: number; max: number }> = {
'Hematocrito': { min: 36, max: 42 },
'Hemoglobina': { min: 12, max: 14 },
'Leucocitos': { min: 4000, max: 11000 },
'Plaquetas': { min: 150000, max: 400000 },
'Glucosa': { min: 70, max: 100 },
'Urea': { min: 17, max: 48.5 },
'Creatinina': { min: 0.5, max: 1.2 },
'Sodio': { min: 135, max: 145 },
'Potasio': { min: 3.5, max: 5.1 },
'Cloro': { min: 101, max: 109 },
'Bilirrubina Total': { min: 0.1, max: 1.2 },
'Bilirrubina Directa': { min: 0, max: 0.3 },
'GOT': { min: 0, max: 35 },
'GPT': { min: 0, max: 35 },
'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' {
const rango = RANGOS_LABORATORIO[parametro];
if (!rango) return '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';
}
function wrapText(text: string, font: { widthOfTextAtSize: (text: string, size: number) => number }, fontSize: number, maxWidth: number): string[] {
const words = text.split(' ');
const lines: string[] = [];
let currentLine = '';
for (const word of words) {
const testLine = currentLine ? `${currentLine} ${word}` : word;
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
if (testWidth > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
}
if (currentLine) lines.push(currentLine);
return lines;
}
export function HistoriaClinica({
internacion,
paciente,
cama,
evoluciones,
laboratorios,
glucemias,
acidosBase,
cultivos,
estudiosComplementarios,
interconsultas,
onAgregarEvolucion,
onActualizarEvolucion,
onEliminarEvolucion,
onAgregarLaboratorio,
onActualizarLaboratorio,
onEliminarLaboratorio,
onAgregarGlucemia,
onActualizarGlucemia,
onEliminarGlucemia,
onAgregarAcidoBase,
onActualizarAcidoBase,
onEliminarAcidoBase,
onAgregarCultivo,
onActualizarCultivo,
onEliminarCultivo,
onAgregarEstudioComplementario,
onActualizarEstudioComplementario,
onEliminarEstudioComplementario,
onAgregarInterconsulta,
onActualizarInterconsulta,
onEliminarInterconsulta,
atb,
indicadores,
onAgregarATB,
onActualizarATB,
onEliminarATB,
onAgregarIndicacion,
onActualizarIndicacion,
onEliminarIndicacion,
movimientos,
onAgregarMovimiento,
onVolver,
onEditarIngreso,
canEdit,
}: HistoriaClinicaProps) {
const [tabActivo, setTabActivo] = useState('evoluciones');
const [portalNode, setPortalNode] = useState<HTMLDivElement | null>(null);
const { pendientes, otrosLaboratorios, agregarOtroLaboratorio, actualizarOtroLaboratorio, eliminarOtroLaboratorio } = useHospitalStore();
const indicacionesActivas = (indicadores || []).filter(i => i.estado === 'Activa');
const pendientesActivos = (pendientes || []).filter(p => p.pacienteId === paciente.id && p.estado === 'pendiente');
const calcularEdad = (fechaNacimiento: string) => {
const hoy = new Date();
const nac = new Date(fechaNacimiento);
let edad = hoy.getFullYear() - nac.getFullYear();
const mes = hoy.getMonth() - nac.getMonth();
if (mes < 0 || (mes === 0 && hoy.getDate() < nac.getDate())) {
edad--;
}
return edad;
};
const calcularDiasInternado = (fechaIngreso: string) => {
const hoy = new Date();
const ingreso = new Date(fechaIngreso);
const diff = hoy.getTime() - ingreso.getTime();
return Math.floor(diff / (1000 * 60 * 60 * 24));
};
return (
<div className="space-y-6 dark:text-white">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2.5">
<ClipboardList className="h-7 w-7 text-blue-600 dark:text-blue-400 shrink-0" />
{cama?.numero || 'N/A'} - Historia Clínica
</h1>
<p className="text-gray-500 dark:text-gray-400">Módulo de Historia Clínica de Internación</p>
</div>
</div>
<div className="flex gap-2 ml-auto sm:ml-0">
<Button variant="secondary" onClick={onVolver}>
<ArrowLeft className="h-4 w-4 mr-2" />
Volver
</Button>
{canEdit && onEditarIngreso && (
<Button onClick={onEditarIngreso}>
<Pencil className="h-4 w-4 mr-2" />
Editar Ingreso
</Button>
)}
</div>
<Card>
<CardContent className="p-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div><h2 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<User className="h-6 w-6 text-blue-600" />
{paciente.apellido}, {paciente.nombre}
</h2>
</div>
<div className="flex items-center gap-2">
<Badge className={internacion.fechaIngresoClinica && calcularDiasInternado(internacion.fechaIngresoClinica) > 7 ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800'}>
{internacion.fechaIngresoClinica ? calcularDiasInternado(internacion.fechaIngresoClinica) : 0} días internado
</Badge>
</div>
</div>
<div className="space-y-2">
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="text-sm px-3 py-1 bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300 dark:border-green-700">
{paciente.fechaNacimiento ? calcularEdad(paciente.fechaNacimiento) + ' años' : ''}
</Badge>
<Badge variant="outline" className="text-sm px-3 py-1 bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900 dark:text-blue-300 dark:border-blue-700">
DNI: {paciente.dni}
</Badge>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary" className="text-sm px-3 py-1">
HC: {paciente.historiaClinica || 'No posee'}
</Badge>
<Badge variant="secondary" className="text-sm px-3 py-1">
OS: {paciente.obraSocial || 'No posee'}
</Badge>
<Badge variant="secondary" className="text-sm px-3 py-1">
Nacionalidad: {paciente.nacionalidad || 'N/A'}
</Badge>
</div>
<div className="border-t p-4 space-y-4">
<div>
<p className="text-gray-500 text-sm">Antecedentes</p>
<p className="font-medium">{paciente.antecedentes || 'No refiere'}</p>
</div>
<div>
<p className="text-gray-500 text-sm">MH</p>
<p className="font-medium">{paciente.medicacionHabitual || 'No refiere'}</p>
</div>
<div>
<p className="text-gray-500">Fecha de Ingreso</p>
<p className="font-medium">{internacion.fechaIngresoClinica ? formatDateDDMMYYYY(internacion.fechaIngresoClinica) : 'N/A'}</p>
</div>
<div>
<p className="text-gray-500">Diagnóstico de Ingreso</p>
<p className="font-medium">{internacion.diagnosticoIngreso}</p>
</div>
{!internacion.activa && (
<>
<div>
<p className="text-gray-500">Fecha de Egreso</p>
<p className="font-medium">{internacion.fechaEgreso ? formatDateDDMMYYYY(internacion.fechaEgreso) : 'N/A'}</p>
</div>
<div>
<p className="text-gray-500">Diagnóstico de Egreso</p>
<p className="font-medium">{internacion.diagnosticoEgreso || 'N/A'}</p>
</div>
<div>
<p className="text-gray-500">Motivo de Egreso</p>
<p className="font-medium">
{internacion.motivoEgreso || 'N/A'}
{internacion.motivoEgreso === 'Pase servicio' && internacion.servicioAlQuePasa ? ` (${internacion.servicioAlQuePasa})` : ''}
</p>
</div>
</>
)}
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Detalle Episodio Actual</AccordionTrigger>
<AccordionContent className="p4">
<div className="border-t space-y-4">
<div>
<p className="text-gray-500 text-sm">Motivo de Consulta</p>
<p className="font-medium">{internacion.motivoConsulta || 'Sin motivo registrado'}</p>
</div>
<div>
<p className="text-gray-500 text-sm">Enfermedad Actual</p>
<p className="font-medium">{internacion.enfermedadActual || 'Sin información'}</p>
</div>
<div>
<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 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');
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([595, 842]);
const { width, height } = page.getSize();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const colorPrimary = rgb(0.18, 0.34, 0.55);
const colorText = rgb(0.13, 0.13, 0.13);
const colorGray = rgb(0.45, 0.45, 0.45);
const colorLightGray = rgb(0.92, 0.92, 0.92);
const edad = paciente.fechaNacimiento ? calcularEdad(paciente.fechaNacimiento) : '';
const fechaFormateada = internacion.fechaIngresoClinica ? formatDateDDMMYYYY(internacion.fechaIngresoClinica) : 'N/A';
const fechaHospital = internacion.fechaIngresoHospital ? formatDateDDMMYYYY(internacion.fechaIngresoHospital) : '';
// Header
page.drawRectangle({ x: 0, y: height - 100, width, height: 100, color: colorPrimary });
page.drawText('HOSPITAL DONACIÓN F. SANTOJANNI DIVISIÓN CLÍNICA MÉDICA', { x: 60, y: height - 35, size: 14, font: fontBold, color: rgb(1, 1, 1) });
page.drawText('HISTORIA CLÍNICA DE INGRESO', { x: 170, y: height - 55, size: 12, font: fontBold, color: rgb(1, 1, 1) });
let y = height - 140;
// Datos Filiatorios
page.drawRectangle({ x: 45, y: y - 8, width: 505, height: 24, color: colorLightGray });
page.drawText('DATOS FILIATORIOS', { x: 50, y: y, size: 12, font: fontBold, color: colorPrimary });
y -= 35;
// Primera línea: Fechas y cama
const fechaIngresoHospText = fechaHospital ? `Fecha ingreso al hospital: ${fechaHospital}` : '';
const fechaIngresoClinText = fechaFormateada ? `Fecha ingreso a clínica: ${fechaFormateada}` : '';
const camaText = `Cama: ${cama?.numero || 'N/A'}`;
page.drawText(fechaIngresoHospText, { x: 50, y, size: 10, font: fontBold, color: colorGray });
page.drawText(fechaIngresoClinText, { x: 260, y, size: 10, font: fontBold, color: colorGray });
y -= 18;
page.drawText(camaText, { x: 50, y, size: 10, font: fontBold, color: colorGray });
y -= 25;
// Segunda línea: Apellido, Nombre y DNI
page.drawText(`Apellido y Nombre: ${paciente.apellido || ''}, ${paciente.nombre || ''}`, { x: 50, y, size: 10, font: fontBold, color: colorGray });
y -= 18;
page.drawText(`DNI: ${paciente.dni || ''}`, { x: 50, y, size: 10, font: fontBold, color: colorGray });
page.drawText(`Edad: ${edad ? `${edad} años` : ''}`, { x: 280, y, size: 10, font: fontBold, color: colorGray });
y -= 40;
// Datos Clínicos
page.drawRectangle({ x: 45, y: y - 8, width: 505, height: 24, color: colorLightGray });
page.drawText('DATOS CLÍNICOS', { x: 50, y: y, size: 12, font: fontBold, color: colorPrimary });
y -= 35;
page.drawText('Motivo de Consulta:', { x: 50, y, size: 10, font: fontBold, color: colorGray });
y -= 18;
const motivoLines = wrapText(internacion.motivoConsulta || 'Sin información', font, 10, 480);
for (const line of motivoLines.slice(0, 4)) {
page.drawText(line, { x: 50, y, size: 10, font, color: colorText });
y -= 14;
}
y -= 10;
if (motivoLines.length > 4) y -= (motivoLines.length - 4) * 14;
page.drawText('Enfermedad Actual:', { x: 50, y, size: 10, font: fontBold, color: colorGray });
y -= 18;
const enfermedadLines = wrapText(internacion.enfermedadActual || 'Sin información', font, 10, 480);
for (const line of enfermedadLines.slice(0, 4)) {
page.drawText(line, { x: 50, y, size: 10, font, color: colorText });
y -= 14;
}
y -= 10;
if (enfermedadLines.length > 4) y -= (enfermedadLines.length - 4) * 14;
page.drawText('Antecedentes de la Enfermedad Actual:', { x: 50, y, size: 10, font: fontBold, color: colorGray });
y -= 18;
const antecedentesLines = wrapText(internacion.antecedentesEnfermedadActual || 'Sin antecedentes registrados', font, 10, 480);
for (const line of antecedentesLines.slice(0, 4)) {
page.drawText(line, { x: 50, y, size: 10, font, color: colorText });
y -= 14;
}
y -= 10;
if (antecedentesLines.length > 4) y -= (antecedentesLines.length - 4) * 14;
page.drawText('Diagnóstico de Ingreso:', { x: 50, y, size: 10, font: fontBold, color: colorGray });
y -= 18;
const diagnosticoLines = wrapText(internacion.diagnosticoIngreso || 'Sin diagnóstico', font, 10, 480);
for (const line of diagnosticoLines.slice(0, 3)) {
page.drawText(line, { x: 50, y, size: 10, font, color: colorText });
y -= 14;
}
// Footer
y -= 20;
page.drawLine({ start: { x: 50, y }, end: { x: 250, y }, thickness: 0.5, color: colorGray });
page.drawText('Firma del Médico', { x: 50, y: y - 15, size: 9, font, color: colorGray });
page.drawLine({ start: { x: 320, y }, end: { x: 520, y }, thickness: 0.5, color: colorGray });
page.drawText('Aclaración / Sello', { x: 320, y: y - 15, size: 9, font, color: colorGray });
page.drawText('v2.0', { x: width - 60, y: 30, size: 8, font, color: colorGray });
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `FormularioIngreso_${paciente.apellido}_${paciente.dni}.pdf`;
link.click();
URL.revokeObjectURL(url);
} catch (err) {
console.error('Error generating PDF:', err);
}
}}>
<FileDown className="h-4 w-4 mr-2" />
Descargar Formulario de Ingreso
</Button>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 w-full">
<div className="w-full overflow-x-auto rounded-md pb-2">
<Tabs className="w-full" value={tabActivo} onValueChange={setTabActivo}>
<div className="flex flex-col sm:flex-row sm:items-center gap-4 mb-4">
<Select value={tabActivo} onValueChange={setTabActivo}>
<SelectTrigger className="w-full sm:w-[280px] font-semibold bg-gray-50 border-gray-300">
<SelectValue placeholder="Menú de Secciones" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Menú</SelectLabel>
<SelectItem value="evoluciones"><div className="flex items-center gap-2"><FileText className="h-4 w-4 text-muted-foreground" /> <span>Evoluciones ({evoluciones.length})</span></div></SelectItem>
<SelectItem value="indicaciones"><div className="flex items-center gap-2"><List className="h-4 w-4 text-muted-foreground" /> <span>Indicaciones ({indicacionesActivas.length})</span></div></SelectItem>
<SelectItem value="glucemias"><div className="flex items-center gap-2"><Droplet className="h-4 w-4 text-muted-foreground" /> <span>Glucemias ({glucemias.length})</span></div></SelectItem>
<SelectItem value="laboratorios"><div className="flex items-center gap-2"><FlaskConical className="h-4 w-4 text-muted-foreground" /> <span>Laboratorio ({laboratorios.length})</span></div></SelectItem>
<SelectItem value="acidobase"><div className="flex items-center gap-2"><Activity className="h-4 w-4 text-muted-foreground" /> <span>EAB ({acidosBase.length})</span></div></SelectItem>
<SelectItem value="cultivos"><div className="flex items-center gap-2"><Microscope className="h-4 w-4 text-muted-foreground" /> <span>Cultivos ({cultivos.length})</span></div></SelectItem>
<SelectItem value="atb"><div className="flex items-center gap-2"><Pill className="h-4 w-4 text-muted-foreground" /> <span>ATB ({atb.length})</span></div></SelectItem>
<SelectItem value="estudios"><div className="flex items-center gap-2"><ClipboardList className="h-4 w-4 text-muted-foreground" /> <span>Estudios ({estudiosComplementarios.length})</span></div></SelectItem>
<SelectItem value="interconsultas"><div className="flex items-center gap-2"><Users className="h-4 w-4 text-muted-foreground" /> <span>Interconsultas ({interconsultas.length})</span></div></SelectItem>
<SelectItem value="pendientes"><div className="flex items-center gap-2"><ListTodo className="h-4 w-4 text-muted-foreground" /> <span>Pendientes ({pendientesActivos.length})</span></div></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div ref={setPortalNode} id="menu-actions" className="flex flex-1 items-center gap-2 overflow-x-auto min-h-[40px]"></div>
</div>
<TabsContent value="glucemias" className="mt-4 min-w-0 w-full overflow-hidden">
<SeccionGlucemias portalNode={portalNode} glucemias={glucemias} patientId={paciente.id} internacionId={internacion.id} add={onAgregarGlucemia} update={onActualizarGlucemia} del={onEliminarGlucemia} canEdit={canEdit} />
</TabsContent>
<TabsContent value="laboratorios" className="mt-4 min-w-0 w-full overflow-hidden">
<SeccionLaboratorios
portalNode={portalNode}
lab={laboratorios}
otrosLaboratorios={otrosLaboratorios}
patientId={paciente.id}
internacionId={internacion.id}
add={onAgregarLaboratorio}
update={onActualizarLaboratorio}
del={onEliminarLaboratorio}
addAcidoBase={onAgregarAcidoBase}
addOtroLaboratorio={agregarOtroLaboratorio}
updateOtroLaboratorio={actualizarOtroLaboratorio}
delOtroLaboratorio={eliminarOtroLaboratorio}
canEdit={canEdit}
/>
</TabsContent>
<TabsContent value="evoluciones" className="mt-4">
<SeccionEvoluciones portalNode={portalNode} evos={evoluciones} internacionId={internacion.id} add={onAgregarEvolucion} update={onActualizarEvolucion} del={onEliminarEvolucion} canEdit={canEdit} />
</TabsContent>
<TabsContent value="acidobase" className="mt-4">
<SeccionAcidosBase portalNode={portalNode} ab={acidosBase} patientId={paciente.id} internacionId={internacion.id} add={onAgregarAcidoBase} update={onActualizarAcidoBase} del={onEliminarAcidoBase} canEdit={canEdit} />
</TabsContent>
<TabsContent value="cultivos" className="mt-4">
<SeccionCultivos portalNode={portalNode} cults={cultivos} patient={paciente} internacionId={internacion.id} add={onAgregarCultivo} update={onActualizarCultivo} del={onEliminarCultivo} canEdit={canEdit} />
</TabsContent>
<TabsContent value="atb" className="mt-4">
<SeccionATB
portalNode={portalNode}
atb={atb}
internacionId={internacion.id}
pacienteId={paciente.id}
add={onAgregarATB}
update={onActualizarATB}
del={onEliminarATB}
canEdit={canEdit}
/>
</TabsContent>
<TabsContent value="indicaciones" className="mt-4">
<SeccionIndicaciones
portalNode={portalNode}
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>
<TabsContent value="estudios" className="mt-4">
<SeccionEstudiosComplementarios
portalNode={portalNode}
estudios={estudiosComplementarios}
internacionId={internacion.id}
pacienteId={paciente.id}
add={onAgregarEstudioComplementario}
update={onActualizarEstudioComplementario}
del={onEliminarEstudioComplementario}
canEdit={canEdit}
/>
</TabsContent>
<TabsContent value="interconsultas" className="mt-4">
<SeccionInterconsultas
portalNode={portalNode}
interconsultas={interconsultas as Interconsulta[]}
pacienteId={paciente.id}
internacionId={internacion.id}
add={onAgregarInterconsulta}
update={onActualizarInterconsulta}
del={onEliminarInterconsulta}
canEdit={canEdit}
/>
</TabsContent>
<TabsContent value="pendientes" className="mt-4">
<SeccionPendientes
portalNode={portalNode}
pacienteId={paciente.id}
internacionId={internacion.id}
canEdit={canEdit}
/>
</TabsContent>
</Tabs>
</div>
</div>
</div>
);
}
function SeccionGlucemias({ glucemias, patientId, internacionId, add, update, del, canEdit, portalNode}: {
glucemias: Glucemia[];
patientId: string;
internacionId: string;
add: (g: Omit<Glucemia, 'id'>) => void;
update: (id: string, data: Partial<Glucemia>) => void;
del: (id: string) => void;
canEdit?: boolean;
portalNode?: HTMLDivElement | null;
}) {
const [dialog, setDialog] = useState(false);
const [evolDialog, setEvolDialog] = useState(false);
const [edit, setEdit] = useState<Glucemia | null>(null);
const [fecha, setFecha] = useState(getLocalToday());
const [hora, setHora] = useState(new Date().toTimeString().slice(0, 5));
const [valor, setValor] = useState('');
const [correccion, setCorreccion] = useState('');
const reset = () => {
setEdit(null);
setFecha(getLocalToday());
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,
internacionId,
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">
{portalNode ? createPortal(
<>
{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>
</>,
portalNode
) : (
<div className="flex justify-between mb-4">
<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, otrosLaboratorios = [], patientId, internacionId, add, update, del, addAcidoBase, addOtroLaboratorio, updateOtroLaboratorio, delOtroLaboratorio, canEdit, portalNode}: {
lab: Laboratorio[];
otrosLaboratorios?: OtroLaboratorio[];
patientId: string;
internacionId: string;
add: (l: Omit<Laboratorio, 'id'>) => void;
update: (id: string, data: Partial<Laboratorio>) => void;
del: (id: string) => void;
addAcidoBase?: (a: Omit<AcidoBase, 'id'>) => void;
addOtroLaboratorio?: (o: Omit<OtroLaboratorio, 'id'>) => void;
updateOtroLaboratorio?: (id: string, data: Partial<OtroLaboratorio>) => void;
delOtroLaboratorio?: (id: string) => void;
canEdit?: boolean;
portalNode?: HTMLDivElement | null;
}) {
const [dialog, setDialog] = useState(false);
const [obsDialog, setObsDialog] = useState(false);
const [otrosDialog, setOtrosDialog] = useState(false);
const [nuevaObsFecha, setNuevaObsFecha] = useState(getLocalToday());
const [nuevaObsTexto, setNuevaObsTexto] = useState('');
const [editOtrosDialog, setEditOtrosDialog] = useState(false);
const [editOtrosItem, setEditOtrosItem] = useState<{ id: string; fecha: string; hora: string; observaciones: string; source: 'lab' | 'otros' } | null>(null);
const [editOtrosFecha, setEditOtrosFecha] = useState('');
const [editOtrosHora, setEditOtrosHora] = useState('');
const [editOtrosTexto, setEditOtrosTexto] = useState('');
const handleStartEditOtros = (item: { id: string; fecha: string; hora?: string; observaciones?: string; source: 'lab' | 'otros' }) => {
setEditOtrosItem({ id: item.id, fecha: item.fecha, hora: item.hora || '00:00', observaciones: item.observaciones || '', source: item.source });
setEditOtrosFecha(item.fecha);
setEditOtrosHora(item.hora || '00:00');
setEditOtrosTexto(item.observaciones || '');
setEditOtrosDialog(true);
};
const handleSaveEditOtros = () => {
if (!editOtrosItem || !editOtrosTexto.trim()) return;
if (editOtrosItem.source === 'otros') {
if (updateOtroLaboratorio) {
updateOtroLaboratorio(editOtrosItem.id, {
fecha: editOtrosFecha,
hora: editOtrosHora,
observaciones: editOtrosTexto
});
toast.success('Registro de Otros Laboratorios actualizado');
}
} else if (editOtrosItem.source === 'lab') {
if (update) {
update(editOtrosItem.id, {
fecha: editOtrosFecha,
hora: editOtrosHora,
observaciones: editOtrosTexto
});
toast.success('Observación de laboratorio actualizada');
}
}
setEditOtrosDialog(false);
setEditOtrosItem(null);
};
const handleDeleteOtros = (item: { id: string; source: 'lab' | 'otros' }) => {
if (window.confirm('¿Está seguro de que desea eliminar este registro?')) {
if (item.source === 'otros') {
if (delOtroLaboratorio) {
delOtroLaboratorio(item.id);
toast.success('Registro eliminado');
}
} else if (item.source === 'lab') {
if (update) {
update(item.id, { observaciones: '' });
toast.success('Observación de laboratorio eliminada');
}
}
}
};
const [evolDialog, setEvolDialog] = useState(false);
const [edit, setEdit] = useState<Laboratorio | null>(null);
const [obsLab, setObsLab] = useState<Laboratorio | null>(null);
const [fecha, setFecha] = useState(getLocalToday());
const [hora, setHora] = useState(new Date().toTimeString().slice(0, 5));
const [observaciones, setObservaciones] = useState('');
const [hto, setHto] = useState('');
const [hb, setHb] = useState('');
const [gb, setGb] = useState('');
const [plaq, setPlaq] = useState('');
const [gluc, setGluc] = useState('');
const [urea, setUrea] = useState('');
const [creat, setCreat] = useState('');
const [na, setNa] = useState('');
const [k, setK] = useState('');
const [cl, setCl] = useState('');
const [bt, setBt] = useState('');
const [bd, setBd] = useState('');
const [got, setGot] = useState('');
const [gpt, setGpt] = useState('');
const [tp, setTp] = useState('');
const [kptt, setKptt] = useState('');
const [rin, setRin] = useState('');
const [parametroEvolucion, setParametroEvolucion] = useState('');
const [importDialog, setImportDialog] = useState(false);
const [importFecha, setImportFecha] = useState(getLocalToday());
const [importHora, setImportHora] = useState(new Date().toTimeString().slice(0, 5));
const [importTexto, setImportTexto] = useState('');
const [importResultados, setImportResultados] = useState<ResultadoLaboratorio[]>([]);
const [importObservaciones, setImportObservaciones] = useState('');
const [importAcidoBase, setImportAcidoBase] = useState<Omit<AcidoBase, 'id'> | null>(null);
const [labSortAsc, setLabSortAsc] = useState(false);
const parametrosLaboratorio = [
'Hematocrito', 'Hemoglobina', 'Leucocitos', 'Plaquetas',
'Glucemia', 'Urea', 'Creatinina',
'Sodio', 'Potasio', 'Cloro',
'Bilirrubina Total', 'Bilirrubina Directa', 'GOT', 'GPT',
'Tiempo de Protrombina', 'KPTT', 'INR'
];
const datosEvolucion = lab
.filter(l => l.pacienteId === patientId && l.resultados.some(r => r.parametro === parametroEvolucion))
.sort((a, b) => new Date(a.fecha + 'T' + (a.hora || '00:00')).getTime() - new Date(b.fecha + 'T' + (b.hora || '00:00')).getTime())
.map(l => {
const r = l.resultados.find(r => r.parametro === parametroEvolucion);
return { fecha: `${l.fecha}${l.hora ? ' ' + l.hora : ''}`, valor: r ? parseFloat(String(r.valor)) : null };
});
const copiarResumenLaboratorio = (l: Laboratorio) => {
const resumen = l.resultados.map(r => `${r.parametro}: ${r.valor} ${r.unidad || ''}`.trim()).join(' | ');
const texto = `Laboratorio (${l.fecha}${l.hora ? ' ' + l.hora : ''}): ${resumen}${l.observaciones ? '\nObs: ' + l.observaciones : ''}`;
navigator.clipboard.writeText(texto);
toast.success('Resumen de laboratorio copiado al portapapeles');
};
const getValor = (l: Laboratorio, param: string) => {
const r = l.resultados.find(r => r.parametro === param);
return r ? `${r.valor}` : '-';
};
const getValorRaw = (l: Laboratorio, param: string) => {
const r = l.resultados.find(r => r.parametro === param);
return r ? `${r.valor}` : '';
};
const getValorStyle = (l: Laboratorio, param: string) => {
const r = l.resultados.find(r => r.parametro === param);
if (!r) return '';
if (r.estado === 'Normal') return 'text-green-600 font-medium';
return 'text-red-600 font-medium';
};
const buildResultados = (): ResultadoLaboratorio[] => {
const res: ResultadoLaboratorio[] = [];
if (hto) res.push({ parametro: 'Hematocrito', valor: hto, unidad: '%', estado: calcularEstadoLaboratorio('Hematocrito', hto) });
if (hb) res.push({ parametro: 'Hemoglobina', valor: hb, unidad: 'g/dL', estado: calcularEstadoLaboratorio('Hemoglobina', hb) });
if (gb) res.push({ parametro: 'Leucocitos', valor: gb, unidad: '/mm3', estado: calcularEstadoLaboratorio('Leucocitos', gb) });
if (plaq) res.push({ parametro: 'Plaquetas', valor: plaq, unidad: '/mm3', estado: calcularEstadoLaboratorio('Plaquetas', plaq) });
if (gluc) res.push({ parametro: 'Glucemia', valor: gluc, unidad: 'mg/dL', estado: calcularEstadoLaboratorio('Glucemia', gluc) });
if (urea) res.push({ parametro: 'Urea', valor: urea, unidad: 'mg/dL', estado: calcularEstadoLaboratorio('Urea', urea) });
if (creat) res.push({ parametro: 'Creatinina', valor: creat, unidad: 'mg/dL', estado: calcularEstadoLaboratorio('Creatinina', creat) });
if (na) res.push({ parametro: 'Sodio', valor: na, unidad: 'mEq/L', estado: calcularEstadoLaboratorio('Sodio', na) });
if (k) res.push({ parametro: 'Potasio', valor: k, unidad: 'mEq/L', estado: calcularEstadoLaboratorio('Potasio', k) });
if (cl) res.push({ parametro: 'Cloro', valor: cl, unidad: 'mEq/L', estado: calcularEstadoLaboratorio('Cloro', cl) });
if (bt) res.push({ parametro: 'Bilirrubina Total', valor: bt, unidad: 'mg/dL', estado: calcularEstadoLaboratorio('Bilirrubina Total', bt) });
if (bd) res.push({ parametro: 'Bilirrubina Directa', valor: bd, unidad: 'mg/dL', estado: calcularEstadoLaboratorio('Bilirrubina Directa', bd) });
if (got) res.push({ parametro: 'GOT', valor: got, unidad: 'U/L', estado: calcularEstadoLaboratorio('GOT', got) });
if (gpt) res.push({ parametro: 'GPT', valor: gpt, unidad: 'U/L', estado: calcularEstadoLaboratorio('GPT', gpt) });
if (tp) res.push({ parametro: 'Tiempo de Protrombina', valor: tp, unidad: 'seg', estado: calcularEstadoLaboratorio('Tiempo de Protrombina', tp) });
if (kptt) res.push({ parametro: 'KPTT', valor: kptt, unidad: 'seg', estado: calcularEstadoLaboratorio('KPTT', kptt) });
if (rin) res.push({ parametro: 'INR', valor: rin, unidad: '', estado: calcularEstadoLaboratorio('INR', rin) });
return res;
};
const reset = () => {
setFecha(getLocalToday());
setHora(new Date().toTimeString().slice(0, 5));
setObservaciones('');
setHto(''); setHb(''); setGb(''); setPlaq('');
setGluc(''); setUrea(''); setCreat('');
setNa(''); setK(''); setCl('');
setBt(''); setBd(''); setGot(''); setGpt('');
setTp(''); setKptt(''); setRin('');
setEdit(null);
};
const buildObservacionesFromResultados = (resultadosArray: ResultadoLaboratorio[]): string => {
const observacionesExtra: string[] = [];
// 1. Lípidos
const ordenLipidos = ['Colesterol Total', 'Colesterol LDL', 'Colesterol No HDL', 'Colesterol HDL', 'Triglicéridos'];
const lipidosEncontrados = resultadosArray.filter(r => ordenLipidos.includes(r.parametro));
if (lipidosEncontrados.length > 0) {
observacionesExtra.push('PERFIL LIPIDICO:');
for (const nombre of ordenLipidos) {
const item = lipidosEncontrados.find(l => l.parametro === nombre);
if (item) {
observacionesExtra.push(`- ${item.parametro}: ${item.valor} ${item.unidad}`);
}
}
}
// 2. Férricos
const ordenFerricos = ['Hierro', 'Transferrina', 'Porcentaje de Saturación de Transferrina', 'Ferritina', 'Ácido Fólico', 'Vitamina B12'];
const ferricosEncontrados = resultadosArray.filter(r => ordenFerricos.includes(r.parametro));
if (ferricosEncontrados.length > 0) {
if (observacionesExtra.length > 0) {
observacionesExtra.push('');
}
observacionesExtra.push('PERFIL FERRICO:');
for (const nombre of ordenFerricos) {
const item = ferricosEncontrados.find(f => f.parametro === nombre);
if (item) {
observacionesExtra.push(`- ${item.parametro}: ${item.valor} ${item.unidad}`);
}
}
}
// 3. Independientes
const ordenInd = [
'Albúmina',
'Calcio Total',
'Fosfatasa Alcalina',
'LDH',
'NT-proBNP',
'Procalcitonina',
'Fósforo',
'Magnesio',
'Calcio Iónico'
];
const indEncontrados = resultadosArray.filter(r => ordenInd.includes(r.parametro));
if (indEncontrados.length > 0) {
if (observacionesExtra.length > 0) {
observacionesExtra.push('');
}
for (const nombre of ordenInd) {
const item = indEncontrados.find(i => i.parametro === nombre);
if (item) {
observacionesExtra.push(`${item.parametro}: ${item.valor} ${item.unidad}`);
}
}
}
// 4. VCM & HCM
const ordenHemogramaAdicionales = ['VCM', 'HCM'];
const hemogramaAdicionalesEncontrados = resultadosArray.filter(r => ordenHemogramaAdicionales.includes(r.parametro));
if (hemogramaAdicionalesEncontrados.length > 0) {
if (observacionesExtra.length > 0) {
observacionesExtra.push('');
}
for (const nombre of ordenHemogramaAdicionales) {
const item = hemogramaAdicionalesEncontrados.find(h => h.parametro === nombre);
if (item) {
observacionesExtra.push(`${item.parametro}: ${item.valor} ${item.unidad}`);
}
}
}
return observacionesExtra.join('\n');
};
const parseLaboratorioTexto = (texto: string): { resultados: ResultadoLaboratorio[]; observaciones: string } => {
const resultados: ResultadoLaboratorio[] = [];
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, esAdicional: true },
{ claves: ['hemoglobina', 'hb'], nombre: 'Hemoglobina', unidad: 'g/dL', esPrincipal: true },
{ claves: ['volumen corpuscular medio', 'vcm'], nombre: 'VCM', unidad: 'fL', esPrincipal: false, esAdicional: true },
{ 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 },
// 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', 'magnesemia', 'mg++', 'mg+', 'mg2+', 'mg 2+', 'mg.', 'magnesio plasmatico', 'magnesio plasmático', 'magnesio serico', 'magnesio sérico', 'magnesio en sangre', 'mg serico', 'mg sérico', 'mg plasmatico', 'mg plasmático', 'mg'], 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
{ 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 === 'mg') {
const execMatch = /(?:^|[^a-z0-9_])mg(?=[:\s=+\d]|$)(?!\/(?:dl|l|24h|ml)|%)/i.exec(lineaLower);
if (!execMatch) return false;
const prefix = lineaLower.substring(0, execMatch.index);
return !/\d+\s*$/.test(prefix);
}
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 group of mapeoParametros) {
// Skip if this parameter was already found
if (resultados.some(r => r.parametro === group.nombre)) continue;
let matchedClave = false;
let matchedClaveString = '';
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;
matchedClaveString = clave;
break;
}
}
if (matchedClave) {
let subLinea = linea;
const idxClave = lineaLower.indexOf(matchedClaveString);
if (idxClave !== -1) {
subLinea = linea.substring(idxClave + matchedClaveString.length);
}
// Clean parenthesized reference values/ranges if present (e.g. "(1.6 - 2.6 mg/dL)")
let cleanSubLinea = subLinea.replace(/\([^)]*?(?:-||—|:|[0-9]\s*[-–—]\s*[0-9]|ref|vr|val|norm|min|max)[^)]*?\)/gi, ' ');
let match = cleanSubLinea.match(/(\d+(?:[.,]\d+)?)/);
if (!match) {
match = subLinea.match(/(\d+(?:[.,]\d+)?)/);
}
if (!match) {
match = linea.match(/[a-zA-ZáéíóúñÁÉÍÓÚÑ]+.*?(\d+(?:[.,]\d+)?)/);
}
if (match && match[1]) {
const valor = parseFloat(match[1].replace(',', '.'));
if (!isNaN(valor) && valor > 0 && valor < 1000000) {
const nombreNormalizado = group.nombre;
let valorFinal = valor;
if (nombreNormalizado === 'Leucocitos' || nombreNormalizado === 'Plaquetas') {
if (valor < 200) valorFinal = valor * 1000;
}
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: unidadFinal,
estado: calcularEstadoLaboratorio(nombreNormalizado, String(valorFinal))
});
}
break;
}
}
}
}
}
const observacionesStr = buildObservacionesFromResultados(resultados);
return { resultados, observaciones: observacionesStr };
};
const parseAcidoBaseTexto = (texto: string): Omit<AcidoBase, 'id'> | null => {
const lineas = texto.split('\n');
let ph: number | undefined;
let pco2: number | undefined;
let po2: number | undefined;
let hco3: number | undefined;
let be: number | undefined;
let sato2: number | undefined;
let lactato: number | undefined;
let fio2: number | undefined;
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();
const getValue = (param: string): number | undefined => {
const idx = cleanLinea.indexOf(param);
if (idx === -1) return undefined;
const after = cleanLinea.slice(idx + param.length).trim();
const parts = after.split(' ');
for (const p of parts) {
const v = parseFloat(p);
if (!isNaN(v) && v > 0 && v < 1000) return v;
}
return undefined;
};
if (cleanLinea.includes('estado') && cleanLinea.includes('ácido') || cleanLinea.includes('base')) {
ph = getValue('ph') || (cleanLinea.match(/ph\s+(\d+\.?\d*)/)?.[1] ? parseFloat(cleanLinea.match(/ph\s+(\d+\.?\d*)/)![1]) : undefined);
pco2 = getValue('pco2') || getValue('pco₂');
po2 = getValue('po2') || getValue('po₂');
hco3 = getValue('hco3');
be = getValue('exceso de base') || getValue('base excess') || getValue('exceso');
sato2 = getValue('saturación') || getValue('sato2') || getValue('sat');
lactato = getValue('lactato');
fio2 = getValue('fio2');
break;
}
}
if (!ph) {
for (const linea of lineas) {
const cleanLinea = linea.replace(/show_chart|list_alt|\t+/g, ' ').replace(/,/g, '.').replace(/\s+/g, ' ').trim().toLowerCase();
const getValue = (param: string): number | undefined => {
const idx = cleanLinea.indexOf(param);
if (idx === -1) return undefined;
const after = cleanLinea.slice(idx + param.length).trim();
const parts = after.split(' ');
for (const p of parts) {
const v = parseFloat(p);
if (!isNaN(v) && v > 0 && v < 1000) return v;
}
return undefined;
};
if (cleanLinea.includes('ph') && !cleanLinea.includes('pco2') && !cleanLinea.includes('pco')) { ph = getValue('ph'); }
else if (cleanLinea.includes('pco2') || cleanLinea.includes('pco₂')) { pco2 = getValue('pco2'); }
else if (cleanLinea.includes('po2') || cleanLinea.includes('po₂')) { po2 = getValue('po2'); }
else if (cleanLinea.includes('hco3')) { hco3 = getValue('hco3'); }
else if (cleanLinea.includes('exceso') || cleanLinea.includes('base excess')) { be = getValue('exceso') || getValue('base'); }
else if (cleanLinea.includes('saturación') || cleanLinea.includes('sato2')) { sato2 = getValue('saturación') || getValue('sat'); }
else if (cleanLinea.includes('lactato')) { lactato = getValue('lactato'); }
else if (cleanLinea.includes('fio2')) { fio2 = getValue('fio2'); }
}
}
if (ph) {
return { pacienteId: patientId, fecha, hora: '', ph, pco2: pco2 || 40, po2: po2 || 85, hco3: hco3 || 24, be: be || 0, sato2: sato2 || 97, lactato, fio2, interpretacion: '' };
}
return null;
};
const esParametroCore = (param: string): boolean => {
const coreParameters = [
// Hemograma
'hematocrito', 'hto', 'hemoglobina', 'hb', 'leucocitos', 'gb', 'plaquetas', 'plaq',
'rdw', 'eritroblastos', 'neutrófilos', 'neutrofilos', 'linfocitos', 'monocitos', 'eosinófilos', 'eosinofilos', 'basófilos', 'basofilos', 'vpm',
// Glucemia
'glucemia', 'glucosa',
// Urea
'urea',
// Creatinina
'creatinina', 'creat', 'filtrado glomerular (mdrd)', 'mdrd',
// Ionograma
'sodio', 'na', 'potasio', 'k', 'k+', 'cloro', 'cl', 'cl-',
// Hepatograma
'bilirrubina total', 'bt', 'bilirrubina directa', 'bd', 'got', 'ast', 'gpt', 'alt', 'proteínas totales', 'proteinas totales',
// Coagulograma
'tiempo de protrombina', 'tp', 't.p.', 't.p', 'rin', 'inr', 'aptt', 'kptt'
];
return coreParameters.includes(param.toLowerCase().trim());
};
const handleEliminarDeterminacion = (index: number) => {
setImportResultados(prev => {
const filtered = prev.filter((_, i) => i !== index);
setImportObservaciones(buildObservacionesFromResultados(filtered));
return filtered;
});
};
const handleProcesarTexto = () => {
const { resultados, observaciones } = parseLaboratorioTexto(importTexto);
const acidoBase = parseAcidoBaseTexto(importTexto);
setImportResultados(resultados);
setImportObservaciones(observaciones);
setImportAcidoBase(acidoBase);
};
const handleImportarLaboratorio = () => {
if (!addAcidoBase) return;
if (importResultados.length === 0 && !importObservaciones && !importAcidoBase) return;
if (importAcidoBase) {
addAcidoBase({ ...importAcidoBase, pacienteId: patientId, internacionId, fecha: importFecha, hora: importHora });
}
if (importResultados.length > 0) {
add({
pacienteId: patientId,
internacionId,
fecha: importFecha,
hora: importHora,
resultados: importResultados,
});
}
if (importObservaciones && importObservaciones.trim() !== '') {
if (addOtroLaboratorio) {
addOtroLaboratorio({
pacienteId: patientId,
internacionId,
fecha: importFecha,
hora: importHora,
observaciones: importObservaciones
});
}
}
setImportDialog(false);
setImportTexto('');
setImportResultados([]);
setImportObservaciones('');
setImportAcidoBase(null);
setImportFecha(getLocalToday());
setImportHora(new Date().toTimeString().slice(0, 5));
};
const loadEdit = (l: Laboratorio) => {
setEdit(l);
setFecha(l.fecha);
setHora(l.hora || '');
setObservaciones(l.observaciones || '');
setHto(getValorRaw(l, 'Hematocrito'));
setHb(getValorRaw(l, 'Hemoglobina'));
setGb(getValorRaw(l, 'Leucocitos'));
setPlaq(getValorRaw(l, 'Plaquetas'));
setGluc(getValorRaw(l, 'Glucemia'));
setUrea(getValorRaw(l, 'Urea'));
setCreat(getValorRaw(l, 'Creatinina'));
setNa(getValorRaw(l, 'Sodio'));
setK(getValorRaw(l, 'Potasio'));
setCl(getValorRaw(l, 'Cloro'));
setBt(getValorRaw(l, 'Bilirrubina Total'));
setBd(getValorRaw(l, 'Bilirrubina Directa'));
setGot(getValorRaw(l, 'GOT'));
setGpt(getValorRaw(l, 'GPT'));
setTp(getValorRaw(l, 'Tiempo de Protrombina'));
setKptt(getValorRaw(l, 'KPTT'));
setRin(getValorRaw(l, 'INR'));
setDialog(true);
};
const handleGuardar = () => {
const resultadosLaboratorio = buildResultados();
if (edit) {
update(edit.id, { fecha, hora, resultados: resultadosLaboratorio, observaciones });
} else {
add({ pacienteId: patientId || '', internacionId, fecha, hora, resultados: resultadosLaboratorio, observaciones });
}
setDialog(false);
reset();
};
const labs = lab.filter(l => l.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();
const diff = dateB - dateA;
return labSortAsc ? -diff : diff;
});
const labsConObservaciones = lab
.filter(l => l.pacienteId === patientId && l.observaciones && l.observaciones.trim() !== '')
.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;
});
return (
<div className="space-y-4 w-full max-w-full min-w-0">
{portalNode ? createPortal(
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<List className="h-4 w-4 mr-2" />
Acciones
<ChevronDown className="h-4 w-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
{canEdit && (
<DropdownMenuItem onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />
Nuevo
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => { setParametroEvolucion('Hematocrito'); setEvolDialog(true); }}>
<TrendingUp className="h-4 w-4 mr-2" />
Evolución
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" onClick={() => setOtrosDialog(true)}><Eye className="h-4 w-4 mr-2" />Otros</Button>
<Button variant="outline" onClick={() => { setImportFecha(getLocalToday()); setImportHora(new Date().toTimeString().slice(0, 5)); setImportTexto(''); setImportResultados([]); setImportObservaciones(''); setImportDialog(true); }}><FileText className="h-4 w-4 mr-2" />Importar</Button>
</>,
portalNode
) : (
<div className="flex justify-between mb-4">
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<List className="h-4 w-4 mr-2" />
Acciones
<ChevronDown className="h-4 w-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
{canEdit && (
<DropdownMenuItem onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />
Nuevo
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => { setParametroEvolucion('Hematocrito'); setEvolDialog(true); }}>
<TrendingUp className="h-4 w-4 mr-2" />
Evolución
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" onClick={() => setOtrosDialog(true)}><Eye className="h-4 w-4 mr-2" />Otros</Button>
<Button variant="outline" onClick={() => { setImportFecha(getLocalToday()); setImportHora(new Date().toTimeString().slice(0, 5)); setImportTexto(''); setImportResultados([]); setImportObservaciones(''); setImportDialog(true); }}><FileText className="h-4 w-4 mr-2" />Importar</Button>
</div>
</div>
)}
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader><DialogTitle>{edit ? 'Editar' : 'Nuevo'} Laboratorio</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 className="bg-purple-50 dark:bg-purple-900 p-3 rounded-lg border border-purple-200 dark:border-purple-700">
<p className="text-sm font-bold text-purple-800 mb-3">Hemograma</p>
<div className="grid grid-cols-4 gap-2">
<div><Label className="text-xs">Hto</Label><Input placeholder="%" value={hto} onChange={e => setHto(e.target.value)} /></div>
<div><Label className="text-xs">Hb</Label><Input placeholder="g/dL" value={hb} onChange={e => setHb(e.target.value)} /></div>
<div><Label className="text-xs">GB</Label><Input placeholder="/mm3" value={gb} onChange={e => setGb(e.target.value)} /></div>
<div><Label className="text-xs">Plaq</Label><Input placeholder="/mm3" value={plaq} onChange={e => setPlaq(e.target.value)} /></div>
</div>
</div>
<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-bold text-green-800 mb-3">Química</p>
<div className="grid grid-cols-3 gap-2">
<div><Label className="text-xs">Gluc</Label><Input placeholder="mg/dL" value={gluc} onChange={e => setGluc(e.target.value)} /></div>
<div><Label className="text-xs">Urea</Label><Input placeholder="mg/dL" value={urea} onChange={e => setUrea(e.target.value)} /></div>
<div><Label className="text-xs">Creat.</Label><Input placeholder="mg/dL" value={creat} onChange={e => setCreat(e.target.value)} /></div>
</div>
</div>
<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-bold text-green-800 mb-3">Ionograma</p>
<div className="grid grid-cols-3 gap-2">
<div><Label className="text-xs">Na+</Label><Input placeholder="mEq/L" value={na} onChange={e => setNa(e.target.value)} /></div>
<div><Label className="text-xs">K+</Label><Input placeholder="mEq/L" value={k} onChange={e => setK(e.target.value)} /></div>
<div><Label className="text-xs">Cl-</Label><Input placeholder="mEq/L" value={cl} onChange={e => setCl(e.target.value)} /></div>
</div>
</div>
<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-bold text-green-800 mb-3">Hepatograma</p>
<div className="grid grid-cols-4 gap-2">
<div><Label className="text-xs">Bt</Label><Input placeholder="mg/dL" value={bt} onChange={e => setBt(e.target.value)} /></div>
<div><Label className="text-xs">BD</Label><Input placeholder="mg/dL" value={bd} onChange={e => setBd(e.target.value)} /></div>
<div><Label className="text-xs">GOT</Label><Input placeholder="U/L" value={got} onChange={e => setGot(e.target.value)} /></div>
<div><Label className="text-xs">GPT</Label><Input placeholder="U/L" value={gpt} onChange={e => setGpt(e.target.value)} /></div>
</div>
</div>
<div className="bg-cyan-50 dark:bg-cyan-900 p-3 rounded-lg border border-cyan-200 dark:border-cyan-700">
<p className="text-sm font-bold text-cyan-800 mb-3">Coagulograma</p>
<div className="grid grid-cols-3 gap-2">
<div><Label className="text-xs">TP</Label><Input placeholder="seg" value={tp} onChange={e => setTp(e.target.value)} /></div>
<div><Label className="text-xs">KPTT</Label><Input placeholder="seg" value={kptt} onChange={e => setKptt(e.target.value)} /></div>
<div><Label className="text-xs">RIN</Label><Input placeholder="" value={rin} onChange={e => setRin(e.target.value)} /></div>
</div>
</div>
<div><Label>Observaciones</Label><textarea className="w-full p-2 border rounded-md" value={observaciones} onChange={e => setObservaciones(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={obsDialog} onOpenChange={setObsDialog}>
<DialogContent className="sm:max-w-lg">
<DialogHeader><DialogTitle>Observaciones</DialogTitle></DialogHeader>
<div className="space-y-2">
<p className="text-sm text-gray-500">{obsLab?.fecha}</p>
<p className="text-sm text-gray-500">{obsLab?.tipo}</p>
<div className="bg-gray-50 p-3 rounded-lg border border-gray-200 dark:bg-gray-800 dark:border-gray-700">
<p className="whitespace-pre-wrap">{obsLab?.observaciones}</p>
</div>
</div>
<div className="flex justify-end gap-2 mt-4"><Button onClick={() => setObsDialog(false)}>Cerrar</Button></div>
</DialogContent>
</Dialog>
<Dialog open={otrosDialog} onOpenChange={setOtrosDialog}>
<DialogContent className="sm:max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Otros / Observaciones de Laboratorio</DialogTitle>
</DialogHeader>
<div className="space-y-4 my-2">
{canEdit && (
<div className="bg-gray-50 dark:bg-gray-900 p-4 rounded-lg border border-gray-200 dark:border-gray-700 space-y-3 mb-6">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Agregar Nuevo Registro (Otros)</h4>
<div className="flex flex-col sm:flex-row gap-3">
<div className="w-full sm:w-1/3">
<Label>Fecha</Label>
<Input type="date" value={nuevaObsFecha} onChange={e => setNuevaObsFecha(e.target.value)} />
</div>
<div className="w-full sm:w-2/3">
<Label>Observaciones / Determinaciones</Label>
<Textarea
placeholder="Ingrese los detalles, resultados, etc..."
value={nuevaObsTexto}
onChange={e => setNuevaObsTexto(e.target.value)}
className="min-h-[80px]"
/>
</div>
</div>
<div className="flex justify-end">
<Button onClick={() => {
if (addOtroLaboratorio && nuevaObsTexto.trim() !== '') {
addOtroLaboratorio({
pacienteId: patientId,
internacionId,
fecha: nuevaObsFecha,
hora: new Date().toTimeString().slice(0, 5),
observaciones: nuevaObsTexto
});
setNuevaObsTexto('');
setNuevaObsFecha(getLocalToday());
toast.success('Registro guardado correctamente');
}
}} disabled={!nuevaObsTexto.trim()}>
<Save className="h-4 w-4 mr-2" /> Guardar
</Button>
</div>
</div>
)}
<div>
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 border-b pb-2">Registros</h4>
<div className="space-y-3 max-h-[40vh] overflow-y-auto pr-2">
{[
...labsConObservaciones.map(l => ({ id: l.id, fecha: l.fecha, hora: l.hora, observaciones: l.observaciones, source: 'lab' })),
...(otrosLaboratorios || []).filter(o => o.pacienteId === patientId).map(o => ({ id: o.id, fecha: o.fecha, hora: o.hora, observaciones: o.observaciones, source: 'otros' }))
].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;
}).map((item, idx) => (
<div key={item.id + idx} className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 space-y-2">
<div className="flex justify-between items-center text-xs text-gray-500 dark:text-gray-400 font-medium">
<span>{formatDateDDMMYYYY(item.fecha)} {item.hora || ''}</span>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-[10px] px-1.5 py-0.5">{item.source === 'lab' ? 'Obs. de Lab' : 'Otros'}</Badge>
{canEdit && (
<div className="flex items-center gap-1">
<Button size="icon" variant="ghost" className="h-6 w-6 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700" onClick={() => handleStartEditOtros(item as any)} title="Editar">
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/40" onClick={() => handleDeleteOtros(item as any)} title="Eliminar">
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
)}
</div>
</div>
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap font-sans">{item.observaciones}</p>
</div>
))}
{(labsConObservaciones.length === 0 && (!otrosLaboratorios || otrosLaboratorios.filter(o => o.pacienteId === patientId).length === 0)) && (
<p className="text-sm text-gray-500 text-center py-6">No hay registros adicionales.</p>
)}
</div>
</div>
</div>
<div className="flex justify-end gap-2 mt-2">
<Button onClick={() => setOtrosDialog(false)}>Cerrar</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={editOtrosDialog} onOpenChange={setEditOtrosDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Editar Registro de Otros Laboratorios</DialogTitle>
</DialogHeader>
<div className="space-y-4 my-2">
<div className="flex gap-3">
<div className="w-1/2">
<Label>Fecha</Label>
<Input type="date" value={editOtrosFecha} onChange={e => setEditOtrosFecha(e.target.value)} />
</div>
<div className="w-1/2">
<Label>Hora</Label>
<Input type="time" value={editOtrosHora} onChange={e => setEditOtrosHora(e.target.value)} />
</div>
</div>
<div>
<Label>Observaciones / Determinaciones</Label>
<Textarea
placeholder="Ingrese los detalles, resultados, etc..."
value={editOtrosTexto}
onChange={e => setEditOtrosTexto(e.target.value)}
className="min-h-[120px]"
/>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setEditOtrosDialog(false)}>Cancelar</Button>
<Button onClick={handleSaveEditOtros} disabled={!editOtrosTexto.trim()}>
<Save className="h-4 w-4 mr-2" /> Guardar Cambios
</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 Laboratorio
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Parámetro a visualizar</Label>
<Select value={parametroEvolucion} onValueChange={setParametroEvolucion}>
<SelectTrigger><SelectValue placeholder="Seleccionar parámetro" /></SelectTrigger>
<SelectContent>
{parametrosLaboratorio.map(p => (
<SelectItem key={p} value={p}>{p}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{parametroEvolucion && 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="fecha" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} domain={['auto', 'auto']} />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '8px'
}}
/>
<Line
type="monotone"
dataKey="valor"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={{ fill: 'hsl(var(--primary))', r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
{parametroEvolucion && datosEvolucion.length === 0 && (
<div className="text-center py-8 text-gray-400">
<TrendingUp className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No hay datos para este parámetro</p>
</div>
)}
</div>
<div className="flex justify-end gap-2 mt-4">
<Button onClick={() => setEvolDialog(false)}>Cerrar</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={importDialog} onOpenChange={setImportDialog}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Importar Laboratorio
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="flex gap-4">
<div className="flex-1">
<Label>Fecha del Laboratorio</Label>
<Input type="date" value={importFecha} onChange={e => setImportFecha(e.target.value)} />
</div>
<div className="flex-1">
<Label>Hora del Laboratorio</Label>
<Input type="time" value={importHora} onChange={e => setImportHora(e.target.value)} />
</div>
</div>
<div>
<Label>Pegar texto del resultado de laboratorio</Label>
<textarea
className="w-full p-2 border rounded-md text-sm min-h-[200px] font-mono"
placeholder="Pegue aquí el texto del resultado de laboratorio..."
value={importTexto}
onChange={e => setImportTexto(e.target.value)}
/>
</div>
<Button variant="outline" onClick={handleProcesarTexto} className="w-full">
<FileText className="h-4 w-4 mr-2" />
Procesar Texto
</Button>
{importResultados.length > 0 && (
<div className="border rounded-md max-h-48 overflow-y-auto overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-10"></TableHead>
<TableHead>Parámetro</TableHead>
<TableHead>Valor</TableHead>
<TableHead>Unidad</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{importResultados.map((r, idx) => (
<TableRow key={idx}>
<TableCell className="text-center p-2 w-10">
{!esParametroCore(r.parametro) && (
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-red-600 hover:text-red-800 hover:bg-red-50 dark:hover:bg-red-950/50 font-bold"
onClick={() => handleEliminarDeterminacion(idx)}
>
X
</Button>
)}
</TableCell>
<TableCell>{r.parametro}</TableCell>
<TableCell className={r.estado === 'Alto' ? 'text-amber-600' : r.estado === 'Bajo' ? 'text-blue-600' : r.estado === 'Crítico' ? 'text-red-600 font-bold' : 'text-green-600'}>{r.valor}</TableCell>
<TableCell>{r.unidad}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{importAcidoBase && (
<div className="border rounded-md p-3 bg-green-50">
<p className="text-sm font-medium mb-2 flex items-center gap-2">
<Activity className="h-4 w-4" />
Estado Ácido Base detectado:
</p>
<div className="grid grid-cols-4 gap-2 text-sm">
<div><span className="text-gray-500">pH:</span> <span className="font-medium">{importAcidoBase.ph}</span></div>
<div><span className="text-gray-500">pCO2:</span> <span className="font-medium">{importAcidoBase.pco2}</span></div>
<div><span className="text-gray-500">pO2:</span> <span className="font-medium">{importAcidoBase.po2}</span></div>
<div><span className="text-gray-500">HCO3:</span> <span className="font-medium">{importAcidoBase.hco3}</span></div>
<div><span className="text-gray-500">BE:</span> <span className="font-medium">{importAcidoBase.be}</span></div>
<div><span className="text-gray-500">SatO2:</span> <span className="font-medium">{importAcidoBase.sato2}%</span></div>
{importAcidoBase.lactato && <div><span className="text-gray-500">Lactato:</span> <span className="font-medium">{importAcidoBase.lactato}</span></div>}
{importAcidoBase.fio2 && <div><span className="text-gray-500">FiO2:</span> <span className="font-medium">{importAcidoBase.fio2}</span></div>}
</div>
</div>
)}
{importObservaciones && (
<div className="border rounded-md p-3 bg-muted/50">
<p className="text-sm font-medium mb-1">Valores adicionales en observaciones:</p>
<p className="text-sm whitespace-pre-wrap">{importObservaciones}</p>
</div>
)}
{importResultados.length === 0 && !importObservaciones && !importAcidoBase && importTexto && (
<div className="text-center py-4 text-gray-500">
<p>No se detectaron parámetros. Verifique que el texto contenga los nombres correctos.</p>
</div>
)}
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setImportDialog(false)}>Cancelar</Button>
<Button onClick={handleImportarLaboratorio} disabled={importResultados.length === 0 && !importObservaciones && !importAcidoBase}>
<Save className="h-4 w-4 mr-2" />
{(importAcidoBase ? 'Guardar' : 'Guardar Laboratorio')}
</Button>
</div>
</DialogContent>
</Dialog>
{labs.length === 0 ? (
<div className="text-center py-8 text-gray-400"><FlaskConical className="h-12 w-12 mx-auto mb-2 opacity-50" /><p>No hay laboratorios</p></div>
) : (
<div className="grid grid-cols-1 w-full">
<div className="w-full overflow-x-auto rounded-md border pb-2">
<Table className="w-full min-w-max text-sm">
<TableHeader>
<TableRow>
<TableHead className="cursor-pointer hover:bg-muted" onClick={() => setLabSortAsc(!labSortAsc)}>
Fecha {labSortAsc ? '↑' : '↓'}
</TableHead>
<TableHead>Hto</TableHead>
<TableHead>Hb</TableHead>
<TableHead>GB</TableHead>
<TableHead>Plaq</TableHead>
<TableHead>Gluc</TableHead>
<TableHead>Urea</TableHead>
<TableHead>Creat.</TableHead>
<TableHead>Na+</TableHead>
<TableHead>K+</TableHead>
<TableHead>Cl-</TableHead>
<TableHead>BT</TableHead>
<TableHead>BDir</TableHead>
<TableHead>GOT</TableHead>
<TableHead>GPT</TableHead>
<TableHead>TP</TableHead>
<TableHead>KPTT</TableHead>
<TableHead>RIN</TableHead>
<TableHead>+</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{labs.map(l => (
<TableRow key={l.id}>
<TableCell>{l.fecha} {l.hora || ''}</TableCell>
<TableCell className={getValorStyle(l, 'Hematocrito')}>{getValor(l, 'Hematocrito')}</TableCell>
<TableCell className={getValorStyle(l, 'Hemoglobina')}>{getValor(l, 'Hemoglobina')}</TableCell>
<TableCell className={getValorStyle(l, 'Leucocitos')}>{getValor(l, 'Leucocitos')}</TableCell>
<TableCell className={getValorStyle(l, 'Plaquetas')}>{getValor(l, 'Plaquetas')}</TableCell>
<TableCell className={getValorStyle(l, 'Glucemia')}>{getValor(l, 'Glucemia')}</TableCell>
<TableCell className={getValorStyle(l, 'Urea')}>{getValor(l, 'Urea')}</TableCell>
<TableCell className={getValorStyle(l, 'Creatinina')}>{getValor(l, 'Creatinina')}</TableCell>
<TableCell className={getValorStyle(l, 'Sodio')}>{getValor(l, 'Sodio')}</TableCell>
<TableCell className={getValorStyle(l, 'Potasio')}>{getValor(l, 'Potasio')}</TableCell>
<TableCell className={getValorStyle(l, 'Cloro')}>{getValor(l, 'Cloro')}</TableCell>
<TableCell className={getValorStyle(l, 'Bilirrubina Total')}>{getValor(l, 'Bilirrubina Total')}</TableCell>
<TableCell className={getValorStyle(l, 'Bilirrubina Directa')}>{getValor(l, 'Bilirrubina Directa')}</TableCell>
<TableCell className={getValorStyle(l, 'GOT')}>{getValor(l, 'GOT')}</TableCell>
<TableCell className={getValorStyle(l, 'GPT')}>{getValor(l, 'GPT')}</TableCell>
<TableCell className={getValorStyle(l, 'Tiempo de Protrombina')}>{getValor(l, 'Tiempo de Protrombina')}</TableCell>
<TableCell className={getValorStyle(l, 'KPTT')}>{getValor(l, 'KPTT')}</TableCell>
<TableCell className={getValorStyle(l, 'INR')}>{getValor(l, 'INR')}</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(l)}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => del(l.id)} className="text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Eliminar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { setObsLab(l); setObsDialog(true); }}>
<ClipboardList className="mr-2 h-4 w-4" />
Ver Observaciones
</DropdownMenuItem>
<DropdownMenuItem onClick={() => copiarResumenLaboratorio(l)}>
<FileText className="mr-2 h-4 w-4 text-emerald-600" />
Copiar para Evolución
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
);
}
function SeccionEvoluciones({ evos, internacionId, add, update, del, canEdit, portalNode}: {
evos: Evolucion[];
internacionId: string;
add: (e: Omit<Evolucion, 'id'>) => void;
update: (id: string, data: Partial<Evolucion>) => void;
del: (id: string) => void;
canEdit?: boolean;
portalNode?: HTMLDivElement | null;
}) {
const { currentUser } = useHospitalStore();
const [dialog, setDialog] = useState(false);
const [edit, setEdit] = useState<Evolucion | null>(null);
const [evoDetalle, setEvoDetalle] = useState<Evolucion | null>(null);
const [fecha, setFecha] = useState(getLocalToday());
const [hora, setHora] = useState(new Date().toTimeString().slice(0, 5));
const effectiveMedico = getNombreProfesional(currentUser);
const [temperatura, setTemperatura] = useState('');
const [presionSistolica, setPresionSistolica] = useState('');
const [presionDiastolica, setPresionDiastolica] = useState('');
const [frecuenciaCardiaca, setFrecuenciaCardiaca] = useState('');
const [frecuenciaRespiratoria, setFrecuenciaRespiratoria] = useState('');
const [saturacionO2, setSaturacionO2] = useState('');
const [snc, setSnc] = useState('');
const [cardiovascular, setCardiovascular] = useState('');
const [respiratorio, setRespiratorio] = useState('');
const [abdominal, setAbdominal] = useState('');
const [genitourinario, setGenitourinario] = useState('');
const [pielAnexos, setPielAnexos] = useState('');
const [soma, setSoma] = useState('');
const [novedades, setNovedades] = useState('');
const [comentario, setComentario] = useState('');
const [pendientes, setPendientes] = useState('');
const reset = () => {
setFecha(getLocalToday());
setHora(new Date().toTimeString().slice(0, 5));
setTemperatura('');
setPresionSistolica('');
setPresionDiastolica('');
setFrecuenciaCardiaca('');
setFrecuenciaRespiratoria('');
setSaturacionO2('');
setSnc('');
setCardiovascular('');
setRespiratorio('');
setAbdominal('');
setGenitourinario('');
setPielAnexos('');
setSoma('');
setNovedades('');
setComentario('');
setPendientes('');
setEdit(null);
};
const openEdit = (e: Evolucion) => {
setEdit(e);
setFecha(e.fecha);
setHora(e.hora);
setTemperatura(e.signosVitales?.temperatura?.toString() || '');
setPresionSistolica(e.signosVitales?.presionSistolica?.toString() || '');
setPresionDiastolica(e.signosVitales?.presionDiastolica?.toString() || '');
setFrecuenciaCardiaca(e.signosVitales?.frecuenciaCardiaca?.toString() || '');
setFrecuenciaRespiratoria(e.signosVitales?.frecuenciaRespiratoria?.toString() || '');
setSaturacionO2(e.signosVitales?.saturacionO2?.toString() || '');
setSnc(e.examenFisico?.SNC || '');
setCardiovascular(e.examenFisico?.Cardiovascular || '');
setRespiratorio(e.examenFisico?.Respiratorio || '');
setAbdominal(e.examenFisico?.Abdominal || '');
setGenitourinario(e.examenFisico?.Genitourinario || '');
setPielAnexos(e.examenFisico?.PielAnexos || '');
setSoma(e.examenFisico?.SOMA || '');
setNovedades(e.novedades || '');
setComentario(e.comentario || '');
setPendientes(e.pendientes || '');
setDialog(true);
};
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 = async () => {
if (!effectiveMedico.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: effectiveMedico.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 (
<div className="space-y-4">
{portalNode ? createPortal(
canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}><Plus className="h-4 w-4 mr-2" />Nueva Evolución</Button>,
portalNode
) : (
<div className="flex mb-4">
{canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}><Plus className="h-4 w-4 mr-2" />Nueva Evolución</Button>}
</div>
)}
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader><DialogTitle>{edit ? 'Editar' : 'Nueva'} Evolución</DialogTitle></DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div><Label>Fecha *</Label><Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} /></div>
<div><Label>Hora *</Label><Input type="time" value={hora} onChange={e => setHora(e.target.value)} /></div>
</div>
<div className="bg-blue-50 dark:bg-blue-900 p-3 rounded-lg border border-blue-200 dark:border-blue-700">
<p className="text-sm font-bold text-blue-800 mb-3 flex items-center gap-2"><Activity className="h-4 w-4" />Signos Vitales</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
<div><Label className="flex items-center gap-1"><Heart className="h-4 w-4" />TAS</Label><Input type="number" value={presionSistolica} onChange={e => setPresionSistolica(e.target.value)} placeholder="120" /></div>
<div><Label className="flex items-center gap-1"><Heart className="h-4 w-4" />TAD</Label><Input type="number" value={presionDiastolica} onChange={e => setPresionDiastolica(e.target.value)} placeholder="80" /></div>
<div><Label className="flex items-center gap-1"><Activity className="h-4 w-4" />FC</Label><Input type="number" value={frecuenciaCardiaca} onChange={e => setFrecuenciaCardiaca(e.target.value)} placeholder="72" /></div>
<div><Label className="flex items-center gap-1"><Wind className="h-4 w-4" />FR</Label><Input type="number" value={frecuenciaRespiratoria} onChange={e => setFrecuenciaRespiratoria(e.target.value)} placeholder="16" /></div>
<div><Label className="flex items-center gap-1"><Thermometer className="h-4 w-4" />T°</Label><Input type="number" step="0.1" value={temperatura} onChange={e => setTemperatura(e.target.value)} placeholder="36.5" /></div>
<div><Label className="flex items-center gap-1"><Droplets className="h-4 w-4" />SatO2</Label><Input type="number" value={saturacionO2} onChange={e => setSaturacionO2(e.target.value)} placeholder="98" /></div>
</div>
</div>
<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-bold text-green-800 mb-3 flex items-center gap-2"><FileText className="h-4 w-4" />Examen Físico</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div><Label>SNC</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[60px] bg-transparent dark:bg-input/30" value={snc} onChange={e => setSnc(e.target.value)} placeholder="Estado neurológico" /></div>
<div><Label>Cardiovascular</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[60px] bg-transparent dark:bg-input/30" value={cardiovascular} onChange={e => setCardiovascular(e.target.value)} placeholder="Hallazgos cardiovasculares" /></div>
<div><Label>Respiratorio</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[60px] bg-transparent dark:bg-input/30" value={respiratorio} onChange={e => setRespiratorio(e.target.value)} placeholder="Hallazgos respiratorios" /></div>
<div><Label>Abdominal</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[60px] bg-transparent dark:bg-input/30" value={abdominal} onChange={e => setAbdominal(e.target.value)} placeholder="Hallazgos abdominales" /></div>
<div><Label>Genitourinario</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[60px] bg-transparent dark:bg-input/30" value={genitourinario} onChange={e => setGenitourinario(e.target.value)} placeholder="Hallazgos genitourinarios" /></div>
<div><Label>Piel y anexos</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[60px] bg-transparent dark:bg-input/30" value={pielAnexos} onChange={e => setPielAnexos(e.target.value)} placeholder="Estado de piel y anexos" /></div>
<div><Label>SOMA</Label><Input value={soma} onChange={e => setSoma(e.target.value)} placeholder="Estado SOMA" /></div>
</div>
</div>
<div className="space-y-4">
<div><Label>Novedades</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[80px] bg-transparent dark:bg-input/30" value={novedades} onChange={e => setNovedades(e.target.value)} placeholder="Novedades..." /></div>
<div><Label>Comentario</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[80px] bg-transparent dark:bg-input/30" value={comentario} onChange={e => setComentario(e.target.value)} placeholder="Comentarios adicionales..." /></div>
<div><Label>Pendientes</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[80px] bg-transparent dark:bg-input/30" value={pendientes} onChange={e => setPendientes(e.target.value)} placeholder="Pendientes..." /></div>
</div>
<div><Label>Médico *</Label><Input value={getNombreProfesional(currentUser)} disabled placeholder="Nombre del médico" /></div>
</div>
<div className="flex justify-end gap-2 mt-4"><Button variant="outline" onClick={() => { reset(); setDialog(false); }}><X className="h-4 w-4 mr-2" />Cancelar</Button><Button onClick={handle} disabled={!effectiveMedico}><Save className="h-4 w-4 mr-2" />Guardar Evolución</Button></div>
</DialogContent>
</Dialog>
{evosFiltered.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<FileText className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No hay evoluciones</p>
</div>
) : (
<div className="rounded-md border overflow-x-auto bg-card">
<Table className="w-full min-w-max text-sm">
<TableHeader>
<TableRow>
<TableHead>Fecha</TableHead>
<TableHead>Profesional</TableHead>
<TableHead className="text-center">TAS</TableHead>
<TableHead className="text-center">TAD</TableHead>
<TableHead className="text-center">FC</TableHead>
<TableHead className="text-center">FR</TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center">Sat</TableHead>
<TableHead className="text-center w-[60px]">Acciones</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{evosFiltered.map((e) => {
const sv = e.signosVitales;
return (
<TableRow key={e.id} className="hover:bg-muted/50">
<TableCell className="whitespace-nowrap font-medium text-xs">
{e.fecha} {e.hora || ''}
</TableCell>
<TableCell className="text-xs whitespace-nowrap">
{e.medico || '-'}
</TableCell>
<TableCell className="text-center text-xs">{sv?.presionSistolica ?? '-'}</TableCell>
<TableCell className="text-center text-xs">{sv?.presionDiastolica ?? '-'}</TableCell>
<TableCell className="text-center text-xs">{sv?.frecuenciaCardiaca ?? '-'}</TableCell>
<TableCell className="text-center text-xs">{sv?.frecuenciaRespiratoria ?? '-'}</TableCell>
<TableCell className="text-center text-xs">{sv?.temperatura !== undefined ? `${sv.temperatura}°C` : '-'}</TableCell>
<TableCell className="text-center text-xs">{sv?.saturacionO2 !== undefined ? `${sv.saturacionO2}%` : '-'}</TableCell>
<TableCell className="text-center p-1">
<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={() => setEvoDetalle(e)}>
<Eye className="mr-2 h-4 w-4" />
Detalles
</DropdownMenuItem>
{canEdit && (
<DropdownMenuItem onClick={() => openEdit(e)}>
<Pencil className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
)}
{canEdit && (
<DropdownMenuItem onClick={() => del(e.id)} className="text-red-600 focus:text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Eliminar
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
{/* Modal Detalles Evolucion */}
<Dialog open={!!evoDetalle} onOpenChange={(open) => { if (!open) setEvoDetalle(null); }}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="h-5 w-5 text-blue-600" />
Detalle de Evolución
</DialogTitle>
</DialogHeader>
{evoDetalle && (() => {
const svDet = evoDetalle.signosVitales;
const efDet = evoDetalle.examenFisico;
return (
<div className="space-y-4 text-sm">
<div className="flex flex-wrap items-center justify-between gap-2 p-3 bg-muted/40 rounded-lg border">
<div className="text-xs text-muted-foreground">
<p><span className="font-medium text-foreground">Fecha:</span> {evoDetalle.fecha} &nbsp;|&nbsp; <span className="font-medium text-foreground">Hora:</span> {evoDetalle.hora}</p>
</div>
<div className="text-right text-xs text-muted-foreground">
<p><span className="font-medium text-foreground">Médico:</span> {evoDetalle.medico}</p>
</div>
</div>
{/* Signos Vitales */}
{svDet && (
<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-xs font-semibold text-blue-800 dark:text-blue-300 mb-2 flex items-center gap-1.5">
<Activity className="h-4 w-4" />
Signos Vitales
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs">
<div><span className="font-medium">TAS:</span> {svDet.presionSistolica ?? '-'} mmHg</div>
<div><span className="font-medium">TAD:</span> {svDet.presionDiastolica ?? '-'} mmHg</div>
<div><span className="font-medium">FC:</span> {svDet.frecuenciaCardiaca ?? '-'} lpm</div>
<div><span className="font-medium">FR:</span> {svDet.frecuenciaRespiratoria ?? '-'} rpm</div>
<div><span className="font-medium">:</span> {svDet.temperatura !== undefined ? `${svDet.temperatura}°C` : '-'}</div>
<div><span className="font-medium">SatO2:</span> {svDet.saturacionO2 !== undefined ? `${svDet.saturacionO2}%` : '-'}</div>
</div>
</div>
)}
{/* Examen Físico */}
{efDet && (
<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-xs font-semibold text-green-800 dark:text-green-300 mb-2">Examen Físico:</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs">
{efDet.SNC && <div><span className="font-semibold">SNC:</span> {efDet.SNC}</div>}
{efDet.Cardiovascular && <div><span className="font-semibold">Cardiovascular:</span> {efDet.Cardiovascular}</div>}
{efDet.Respiratorio && <div><span className="font-semibold">Respiratorio:</span> {efDet.Respiratorio}</div>}
{efDet.Abdominal && <div><span className="font-semibold">Abdominal:</span> {efDet.Abdominal}</div>}
{efDet.Genitourinario && <div><span className="font-semibold">Genitourinario:</span> {efDet.Genitourinario}</div>}
{efDet.PielAnexos && <div><span className="font-semibold">Piel y Anexos:</span> {efDet.PielAnexos}</div>}
{efDet.SOMA && <div><span className="font-semibold">SOMA:</span> {efDet.SOMA}</div>}
</div>
</div>
)}
{/* Novedades */}
{evoDetalle.novedades && (
<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-xs font-semibold text-red-800 dark:text-red-300 mb-1">Novedades:</p>
<p className="text-xs text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{evoDetalle.novedades}</p>
</div>
)}
{/* Comentario */}
{evoDetalle.comentario && (
<div className="bg-slate-100 dark:bg-slate-800/60 p-3 rounded-lg border border-slate-200 dark:border-slate-700/50">
<p className="text-xs font-semibold text-slate-800 dark:text-slate-200 mb-1">Comentario:</p>
<p className="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap">{evoDetalle.comentario}</p>
</div>
)}
{/* Pendientes */}
{evoDetalle.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-xs font-semibold text-amber-800 dark:text-amber-300 mb-1">Pendientes:</p>
<p className="text-xs text-amber-700 dark:text-amber-300 whitespace-pre-wrap">{evoDetalle.pendientes}</p>
</div>
)}
</div>
);
})()}
</DialogContent>
</Dialog>
</div>
);
}
function SeccionAcidosBase({ ab, patientId, internacionId, add, update, del, canEdit, portalNode}: {
ab: AcidoBase[];
patientId: string;
internacionId: string;
add: (a: Omit<AcidoBase, 'id'>) => void;
update: (id: string, datos: Partial<AcidoBase>) => void;
del: (id: string) => void;
canEdit?: boolean;
portalNode?: HTMLDivElement | null;
}) {
const [dialog, setDialog] = useState(false);
const [editDialog, setEditDialog] = useState(false);
const [selected, setSelected] = useState<AcidoBase | null>(null);
const [fecha, setFecha] = useState(getLocalToday());
const [hora, setHora] = useState(new Date().toTimeString().slice(0, 5));
const [ph, setPh] = useState('');
const [pco2, setPco2] = useState('');
const [po2, setPo2] = useState('');
const [hco3, setHco3] = useState('');
const [be, setBe] = useState('');
const [sato2, setSato2] = useState('');
const [lactato, setLactato] = useState('');
const [fio2, setFio2] = useState('');
const [interpretacion, setInterpretacion] = useState('');
const [eabSortAsc, setEabSortAsc] = useState(false);
const reset = () => {
setFecha(getLocalToday());
setHora(new Date().toTimeString().slice(0, 5));
setPh(''); setPco2(''); setPo2(''); setHco3(''); setBe(''); setSato2(''); setLactato(''); setFio2(''); setInterpretacion('');
setSelected(null);
};
const loadEdit = (a: AcidoBase) => {
setSelected(a);
setFecha(a.fecha);
setHora(a.hora);
setPh(String(a.ph));
setPco2(String(a.pco2));
setPo2(String(a.po2));
setHco3(String(a.hco3));
setBe(String(a.be));
setSato2(String(a.sato2));
setLactato(a.lactato ? String(a.lactato) : '');
setFio2(a.fio2 ? String(a.fio2) : '');
setInterpretacion(a.interpretacion || '');
setEditDialog(true);
};
const abFiltered = ab.filter(a => a.pacienteId === patientId).sort((a, b) => new Date(b.fecha + 'T' + b.hora).getTime() - new Date(a.fecha + 'T' + a.hora).getTime());
const interpretarGasometria = () => {
const phVal = parseFloat(ph), pco2Val = parseFloat(pco2), hco3Val = parseFloat(hco3), beVal = parseFloat(be);
if (!phVal || !pco2Val || !hco3Val) return '';
let interp = '';
if (phVal < 7.35) { interp += 'Acidemia - '; interp += pco2Val > 45 ? 'Acidosis Respiratoria' : hco3Val < 22 ? 'Acidosis Metabólica' : 'Acidosis Mixta'; }
else if (phVal > 7.45) { interp += 'Alcalemia - '; interp += pco2Val < 35 ? 'Alcalosis Respiratoria' : hco3Val > 26 ? 'Alcalosis Metabólica' : 'Alcalosis Mixta'; }
else { interp += 'pH Normal - '; interp += pco2Val > 45 || hco3Val < 22 ? 'Compensación en curso' : 'Equilibrio Ácido-Base'; }
if (beVal && Math.abs(beVal) > 2) interp += beVal > 0 ? ' (Exceso de bases elevado)' : ' (Déficit de bases)';
return interp;
};
const handle = () => {
if (!ph || !pco2 || !po2 || !hco3 || !be || !sato2) return;
const interpretacionAuto = interpretarGasometria();
add({ pacienteId: patientId, internacionId, fecha, hora, ph: parseFloat(ph), pco2: parseFloat(pco2), po2: parseFloat(po2), hco3: parseFloat(hco3), be: parseFloat(be), sato2: parseFloat(sato2), lactato: lactato ? parseFloat(lactato) : undefined, fio2: fio2 ? parseFloat(fio2) : undefined, interpretacion: interpretacion || interpretacionAuto });
setDialog(false);
reset();
};
const handleEdit = () => {
if (!selected || !ph || !pco2 || !po2 || !hco3 || !be || !sato2) return;
const interpretacionAuto = interpretarGasometria();
update(selected.id, { fecha, hora, ph: parseFloat(ph), pco2: parseFloat(pco2), po2: parseFloat(po2), hco3: parseFloat(hco3), be: parseFloat(be), sato2: parseFloat(sato2), lactato: lactato ? parseFloat(lactato) : undefined, fio2: fio2 ? parseFloat(fio2) : undefined, interpretacion: interpretacion || interpretacionAuto });
setEditDialog(false);
reset();
};
return (
<div className="space-y-4">
{portalNode ? createPortal(
canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}><Plus className="h-4 w-4 mr-2" />Nueva Gasometría</Button>,
portalNode
) : (
<div className="flex mb-4">
{canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}><Plus className="h-4 w-4 mr-2" />Nueva Gasometría</Button>}
</div>
)}
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader><DialogTitle>Nueva Gasometría Arterial</DialogTitle></DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div><Label>Fecha *</Label><Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} /></div>
<div><Label>Hora *</Label><Input type="time" value={hora} onChange={e => setHora(e.target.value)} /></div>
</div>
<div className="grid grid-cols-4 sm:grid-cols-8 gap-2">
<div><Label>pH *</Label><Input type="number" step="0.01" value={ph} onChange={e => setPh(e.target.value)} placeholder="7.40" /></div>
<div><Label>pCO2 *</Label><Input type="number" value={pco2} onChange={e => setPco2(e.target.value)} placeholder="40" /></div>
<div><Label>pO2 *</Label><Input type="number" value={po2} onChange={e => setPo2(e.target.value)} placeholder="85" /></div>
<div><Label>HCO3 *</Label><Input type="number" step="0.1" value={hco3} onChange={e => setHco3(e.target.value)} placeholder="24" /></div>
<div><Label>BE *</Label><Input type="number" step="0.1" value={be} onChange={e => setBe(e.target.value)} placeholder="0" /></div>
<div><Label>SatO2 *</Label><Input type="number" value={sato2} onChange={e => setSato2(e.target.value)} placeholder="97" /></div>
<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 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>
</DialogContent>
</Dialog>
<Dialog open={editDialog} onOpenChange={setEditDialog}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader><DialogTitle>Editar Gasometría</DialogTitle></DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div><Label>Fecha *</Label><Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} /></div>
<div><Label>Hora *</Label><Input type="time" value={hora} onChange={e => setHora(e.target.value)} /></div>
</div>
<div className="grid grid-cols-4 sm:grid-cols-8 gap-2">
<div><Label>pH *</Label><Input type="number" step="0.01" value={ph} onChange={e => setPh(e.target.value)} placeholder="7.40" /></div>
<div><Label>pCO2 *</Label><Input type="number" value={pco2} onChange={e => setPco2(e.target.value)} placeholder="40" /></div>
<div><Label>pO2 *</Label><Input type="number" value={po2} onChange={e => setPo2(e.target.value)} placeholder="85" /></div>
<div><Label>HCO3 *</Label><Input type="number" step="0.1" value={hco3} onChange={e => setHco3(e.target.value)} placeholder="24" /></div>
<div><Label>BE *</Label><Input type="number" step="0.1" value={be} onChange={e => setBe(e.target.value)} placeholder="0" /></div>
<div><Label>SatO2 *</Label><Input type="number" value={sato2} onChange={e => setSato2(e.target.value)} placeholder="97" /></div>
<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 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>
</DialogContent>
</Dialog>
{abFiltered.length === 0 ? <div className="text-center py-8 text-gray-400"><Activity className="h-12 w-12 mx-auto mb-2 opacity-50" /><p>No hay gasometrías</p></div> :
<div className="grid grid-cols-1 w-full">
<div className="w-full overflow-x-auto rounded-md border pb-2">
<Table className="w-full min-w-max text-sm">
<TableHeader>
<TableRow>
<TableHead className="cursor-pointer hover:bg-muted" onClick={() => setEabSortAsc(!eabSortAsc)}>
Fecha {eabSortAsc ? '↑' : '↓'}
</TableHead>
<TableHead>pH</TableHead>
<TableHead>pCO2</TableHead>
<TableHead>pO2</TableHead>
<TableHead>HCO3</TableHead>
<TableHead>BE</TableHead>
<TableHead>SatO2</TableHead>
<TableHead>Lactato</TableHead>
<TableHead>FiO2</TableHead>
<TableHead>PaFiO2</TableHead>
<TableHead>...</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{abFiltered.map(a => (
<TableRow key={a.id}>
<TableCell>{a.fecha} {a.hora || ''}</TableCell>
<TableCell>{a.ph}</TableCell>
<TableCell>{a.pco2}</TableCell>
<TableCell>{a.po2}</TableCell>
<TableCell>{a.hco3}</TableCell>
<TableCell>{a.be}</TableCell>
<TableCell>{a.sato2}</TableCell>
<TableCell>{a.lactato}</TableCell>
<TableCell>{a.fio2}</TableCell>
<TableCell>{(() => {
if (a.fio2 == null || a.fio2 <= 0) return <span className="text-gray-400"></span>;
const pf = a.po2 / a.fio2;
const badgeClass =
pf > 300 ? "bg-green-500 text-white hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-500" :
pf > 200 ? "bg-yellow-500 text-white hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-500" :
pf > 100 ? "bg-orange-500 text-white hover:bg-orange-600 dark:bg-orange-600 dark:hover:bg-orange-500" :
"bg-red-500 text-white hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-500";
return (
<Badge className={badgeClass}>
{pf.toFixed(1)}
</Badge>
);
})()}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => loadEdit(a)}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => del(a.id)} className="text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Eliminar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
}
</div>
);
}
function SeccionCultivos({ cults, patient, internacionId, add, update, del, canEdit, portalNode}: {
cults: Cultivo[];
patient: Paciente;
internacionId: string;
add: (c: Omit<Cultivo, 'id'>) => void;
update: (id: string, data: Partial<Cultivo>) => void;
del: (id: string) => void;
canEdit?: boolean;
portalNode?: HTMLDivElement | null;
}) {
const [dialog, setDialog] = useState(false);
const [resDialog, setResDialog] = useState(false);
const [editDialog, setEditDialog] = useState(false);
const [selected, setSelected] = useState<Cultivo | null>(null);
const [fechaToma, setFechaToma] = useState(getLocalToday());
const [protocolo, setProtocolo] = useState('');
const [tipoMuestra, setTipoMuestra] = useState<Cultivo['tipoMuestra']>('HMCx2');
const [observaciones, setObservaciones] = useState('');
const [fechaResultado, setFechaResultado] = useState(getLocalToday());
const [estadoResultado, setEstadoResultado] = useState<Cultivo['estado']>('Parcial');
const [germen, setGermen] = useState('');
const [sensible, setSensible] = useState('');
const [resistente, setResistente] = useState('');
const reset = () => {
setFechaToma(getLocalToday());
setProtocolo('');
setTipoMuestra('HMCx2');
setObservaciones('');
setSelected(null);
};
const resetRes = () => {
setFechaResultado(getLocalToday());
setEstadoResultado('Positivo');
setGermen('');
setSensible('');
setResistente('');
setSelected(null);
};
const openParcial = (c: Cultivo) => {
setSelected(c);
setProtocolo(c.protocolo || '');
setGermen(c.germen || '');
setSensible(c.sensible || '');
setResistente(c.resistente || '');
setResDialog(true);
};
const openDefinitivo = (c: Cultivo) => {
setSelected(c);
setProtocolo(c.protocolo || '');
setFechaResultado(c.fechaResultado || getLocalToday());
setEstadoResultado(c.estado === 'Parcial' ? 'Positivo' : c.estado);
setGermen(c.germen || '');
setSensible(c.sensible || '');
setResistente(c.resistente || '');
setEditDialog(true);
};
const cs = cults.filter(c => c.pacienteId === patient.id).sort((a, b) => new Date(b.fechaToma).getTime() - new Date(a.fechaToma).getTime());
const getEstadoColor = (estado: Cultivo['estado']) => {
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';
}
};
const getEstadoLabel = (estado: Cultivo['estado']) => {
switch (estado) {
case 'NAF/Pendiente': return 'NAF/Pendiente';
case 'Parcial': return 'Parcial';
case 'Positivo': return 'Positivo';
case 'Negativo': return 'Negativo Final';
}
};
const handle = () => { add({ pacienteId: patient.id, internacionId, fechaToma, protocolo, tipoMuestra, observaciones: observaciones || undefined, estado: 'NAF/Pendiente' }); setDialog(false); reset(); };
const handleParcial = () => {
if (!selected) return;
update(selected.id, {
protocolo,
estado: 'Parcial',
germen,
sensible,
resistente,
fechaResultado: selected.fechaResultado || getLocalToday()
});
setResDialog(false);
resetRes();
};
const handleDefinitivo = () => {
if (!selected) return;
update(selected.id, { protocolo, fechaResultado, estado: estadoResultado, germen, sensible, resistente });
setEditDialog(false);
resetRes();
};
return (
<div className="space-y-4">
{portalNode ? createPortal(
canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}><Plus className="h-4 w-4 mr-2" />Nuevo Cultivo</Button>,
portalNode
) : (
<div className="flex mb-4">
{canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}><Plus className="h-4 w-4 mr-2" />Nuevo Cultivo</Button>}
</div>
)}
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-lg">
<DialogHeader><DialogTitle>Nuevo Cultivo</DialogTitle></DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div><Label>Fecha de Toma *</Label><Input type="date" value={fechaToma} onChange={e => setFechaToma(e.target.value)} /></div>
<div><Label>Protocolo</Label><Input value={protocolo} onChange={e => setProtocolo(e.target.value)} placeholder="N° Protocolo" /></div>
</div>
<div>
<Label>Tipo de Muestra *</Label>
<Select value={tipoMuestra} onValueChange={(v: Cultivo['tipoMuestra']) => setTipoMuestra(v)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="HMCx2">HMCx2</SelectItem>
<SelectItem value="RC">RC</SelectItem>
<SelectItem value="PC">PC</SelectItem>
<SelectItem value="UC">UC</SelectItem>
<SelectItem value="LCR">LCR</SelectItem>
<SelectItem value="LP">LP</SelectItem>
<SelectItem value="LAsc">LAsc</SelectItem>
<SelectItem value="LAbd">LAbd</SelectItem>
<SelectItem value="Coleccion">Coleccion</SelectItem>
<SelectItem value="Partes Blandas">Partes Blandas</SelectItem>
<SelectItem value="Esputo GC">Esputo GC</SelectItem>
<SelectItem value="Esputo TBC">Esputo TBC</SelectItem>
<SelectItem value="Baciloscopia">Baciloscopia</SelectItem>
<SelectGroup>
<SelectLabel>HNF</SelectLabel>
<SelectItem value="HNF Test Rápido">Test Rápido</SelectItem>
<SelectItem value="HNF Panel PCR">Panel PCR</SelectItem>
</SelectGroup>
<SelectItem value="Hisopado Rectal KPC">Hisopado Rectal KPC</SelectItem>
</SelectContent>
</Select>
</div>
<div><Label>Observaciones</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[60px]" value={observaciones} onChange={e => setObservaciones(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 variant="outline" onClick={handle}><Plus className="h-4 w-4 mr-2" />Guardar</Button></div>
</DialogContent>
</Dialog>
<Dialog open={resDialog} onOpenChange={setResDialog}>
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
<DialogHeader><DialogTitle>Cargar Resultado Parcial</DialogTitle></DialogHeader>
<div className="space-y-4">
<div className="bg-orange-50 border border-orange-200 p-3 rounded-lg">
<p className="text-sm font-medium text-orange-800">Resultado Parcial - Sujeto a modificación</p>
<p className="text-xs text-orange-600 mt-1">Ingrese resultado parcial. Esto puede modificarse posteriormente.</p>
</div>
<div><Label>Protocolo</Label><Input value={protocolo} onChange={e => setProtocolo(e.target.value)} placeholder="N° Protocolo" /></div>
<div><Label>Germen *</Label><Input value={germen} onChange={e => setGermen(e.target.value)} placeholder="Ej: Staphylococcos aureus" /></div>
<div><Label>Sensible a:</Label><Input value={sensible} onChange={e => setSensible(e.target.value)} placeholder="Ej: Amikacina, Vancomicina" /></div>
<div><Label>Resistente a:</Label><Input value={resistente} onChange={e => setResistente(e.target.value)} placeholder="Ej: Oxacilina" /></div>
</div>
<div className="flex justify-end gap-2 mt-4"><Button variant="outline" onClick={() => { resetRes(); setResDialog(false); }}><X className="h-4 w-4 mr-2" />Cancelar</Button><Button variant="outline" onClick={handleParcial} disabled={!germen}><Save className="h-4 w-4 mr-2" />Guardar Parcial</Button></div>
</DialogContent>
</Dialog>
<Dialog open={editDialog} onOpenChange={setEditDialog}>
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
<DialogHeader><DialogTitle>Cargar Resultado Definitivo</DialogTitle></DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div><Label>Fecha de Resultado *</Label><Input type="date" value={fechaResultado} onChange={e => setFechaResultado(e.target.value)} /></div>
<div><Label>Resultado *</Label>
<Select value={estadoResultado} onValueChange={(v: Cultivo['estado']) => setEstadoResultado(v)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="Positivo">Positivo</SelectItem>
<SelectItem value="Negativo">Negativo Final</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div><Label>Protocolo</Label><Input value={protocolo} onChange={e => setProtocolo(e.target.value)} placeholder="N° Protocolo" /></div>
{estadoResultado === 'Positivo' && (
<>
<div><Label>Germen *</Label><Input value={germen} onChange={e => setGermen(e.target.value)} placeholder="Ej: Staphylococcus aureus" /></div>
<div><Label>Sensible a:</Label><Input value={sensible} onChange={e => setSensible(e.target.value)} placeholder="Ej: Amikacina, Gentamicina" /></div>
<div><Label>Resistente a:</Label><Input value={resistente} onChange={e => setResistente(e.target.value)} placeholder="Ej: Ampicilina, Cefazolina" /></div>
</>
)}
</div>
<div className="flex justify-end gap-2 mt-4"><Button variant="outline" onClick={() => { resetRes(); setEditDialog(false); }}><X className="h-4 w-4 mr-2" />Cancelar</Button><Button variant="outline" onClick={handleDefinitivo} disabled={estadoResultado === 'Positivo' && !germen}><Save className="h-4 w-4 mr-2" />Guardar Definitivo</Button></div>
</DialogContent>
</Dialog>
<div>
<div className="grid grid-cols-1 gap-4">
{cs.length === 0 ? <div className="text-center py-8 text-gray-400"><Microscope className="h-12 w-12 mx-auto mb-2 opacity-50" /><p>No hay cultivos</p></div> :
cs.map(c => (
<Card key={c.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-4">
<div className="flex items-center gap-3">
<div className={`h-10 w-10 rounded-full flex items-center justify-center flex-shrink-0 ${c.estado === 'NAF/Pendiente' ? 'bg-amber-100' : c.estado === 'Parcial' ? 'bg-orange-100' : c.estado === 'Positivo' ? 'bg-red-100' : 'bg-green-100'}`}>
<Microscope className={`h-5 w-5 ${c.estado === 'NAF/Pendiente' ? 'text-amber-600' : c.estado === 'Parcial' ? 'text-orange-600' : c.estado === 'Positivo' ? 'text-red-600' : 'text-green-600'}`} />
</div>
<div>
<p className="font-medium">{formatDateDDMMYYYY(c.fechaToma)}</p>
<div className="flex items-center gap-2 text-sm text-gray-500">
<Badge variant="outline">{c.tipoMuestra}</Badge>
<Badge className={getEstadoColor(c.estado)}>{getEstadoLabel(c.estado)}</Badge>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
{c.estado === 'NAF/Pendiente' && (
<>
<Button size="sm" variant="outline" onClick={() => openParcial(c)}><AlertCircle className="h-4 w-4 mr-1" />Parcial</Button>
<Button size="sm" variant="outline" onClick={() => openDefinitivo(c)}><CheckCircle2 className="h-4 w-4 mr-1" />Definitivo</Button>
</>
)}
{c.estado === 'Parcial' && (
<>
<Button size="sm" variant="outline" onClick={() => openParcial(c)}><Pencil className="h-4 w-4 mr-1" />Editar Parcial</Button>
<Button size="sm" variant="outline" onClick={() => openDefinitivo(c)}><CheckCircle2 className="h-4 w-4 mr-1" />Definitivo</Button>
</>
)}
{(c.estado === 'Positivo' || c.estado === 'Negativo') && <Button size="sm" variant="outline" onClick={() => openDefinitivo(c)}><Pencil className="h-4 w-4" /></Button>}
<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 dark:text-gray-400">Protocolo: {c.protocolo}</p>}
{c.fechaResultado && (c.estado === 'Positivo' || c.estado === 'Negativo' || c.estado === 'Parcial') && (
<p className="text-xs text-gray-500 dark:text-gray-400">
{c.estado === 'Parcial' ? 'Fecha Resultado Parcial' : 'Fecha Definitivo'}: {formatDateDDMMYYYY(c.fechaResultado)}
</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 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 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="flex flex-col gap-2 mt-2">
<div>
<Badge
variant="outline"
className="border-solid border-2 border-red-600 bg-red-50 text-red-800 dark:bg-red-950/60 dark:text-red-300 dark:border-red-500 rounded px-2.5 py-1 font-semibold text-sm inline-flex items-center gap-1.5"
>
<AlertCircle className="h-4 w-4 text-red-600 shrink-0" />
<span>Germen: {c.germen}</span>
</Badge>
</div>
{(c.sensible || c.resistente) && (
<div className="flex flex-wrap gap-2">
{c.sensible && (
<Badge
variant="outline"
className="border-green-600 border bg-green-50 text-green-800 dark:bg-green-950/60 dark:text-green-300 dark:border-green-500 rounded px-2.5 py-1 font-medium text-sm inline-flex items-center gap-1.5"
>
<span>Sensibilidad: {c.sensible}</span>
</Badge>
)}
{c.resistente && (
<Badge
variant="outline"
className="border-red-600 border bg-red-50 text-red-800 dark:bg-red-950/60 dark:text-red-300 dark:border-red-500 rounded px-2.5 py-1 font-medium text-sm inline-flex items-center gap-1.5"
>
<span>Resistencia: {c.resistente}</span>
</Badge>
)}
</div>
)}
</div>
)}
{c.estado === 'Negativo' && (
<div className="bg-green-50 dark:bg-green-900 p-3 rounded-lg border border-green-200 dark:border-green-700 mt-2">
<p className="text-sm font-medium text-green-800 flex items-center gap-2"><CheckCircle2 className="h-4 w-4" />Sin crecimiento de microorganismos</p>
</div>
)}
</div>
</CardContent></Card>
))}
</div>
</div>
</div>
);
}
function SeccionEstudiosComplementarios({ estudios, internacionId, pacienteId, add, update, del, canEdit, portalNode}: {
estudios: EstudioComplementario[];
internacionId: string;
pacienteId: string;
add: (e: Omit<EstudioComplementario, 'id'>) => void;
update: (id: string, datos: Partial<EstudioComplementario>) => void;
del: (id: string) => void;
canEdit?: boolean;
portalNode?: HTMLDivElement | null;
}) {
const [dialog, setDialog] = useState(false);
const [filtro, setFiltro] = useState<'todos' | 'internacion'>('internacion');
const [fecha, setFecha] = useState(getLocalToday());
const [tipo, setTipo] = useState('');
const [resultado, setResultado] = useState('');
const [edit, setEdit] = useState<EstudioComplementario | null>(null);
const estudiosFiltrados = filtro === 'internacion'
? estudios.filter(e => e.internacionId === internacionId)
: estudios;
const reset = () => {
setFecha(getLocalToday());
setTipo('');
setResultado('');
setEdit(null);
};
const loadEdit = (e: EstudioComplementario) => {
setEdit(e);
setFecha(e.fecha || getLocalToday());
setTipo(e.tipo || '');
setResultado(e.resultado || '');
setDialog(true);
};
const handleGuardar = () => {
if (!tipo || !resultado) return;
if (edit) {
update(edit.id, { fecha, tipo, resultado });
} else {
add({
pacienteId,
internacionId,
fecha,
tipo,
resultado,
});
}
setDialog(false);
reset();
};
return (
<div className="space-y-4">
{portalNode ? createPortal(
canEdit && (
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />
Nuevo Estudio
</Button>
),
portalNode
) : (
<div className="flex justify-between items-center mb-4">
{canEdit && (
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />
Nuevo Estudio
</Button>
)}
</div>
)}
<div className="flex justify-end">
<Select value={filtro} onValueChange={(v: 'todos' | 'internacion') => setFiltro(v)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filtrar" />
</SelectTrigger>
<SelectContent>
<SelectItem value="internacion">Internación Actual</SelectItem>
<SelectItem value="todos">Todos</SelectItem>
</SelectContent>
</Select>
</div>
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{edit ? 'Editar' : 'Nuevo'} Estudio Complementario</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Fecha</Label>
<Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} />
</div>
<div>
<Label>Tipo de Estudio</Label>
<Input
value={tipo}
onChange={e => setTipo(e.target.value)}
placeholder="Ej: Radiografía de tórax, ECG, Tomografía, etc."
/>
</div>
<div>
<Label>Resultado</Label>
<textarea
className="w-full p-2 border rounded-md text-sm min-h-[100px]"
value={resultado}
onChange={e => setResultado(e.target.value)}
placeholder="Ingrese el resultado del estudio..."
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setDialog(false)}>Cancelar</Button>
<Button onClick={handleGuardar} disabled={!tipo || !resultado}>
<Save className="h-4 w-4 mr-2" />
Guardar
</Button>
</div>
</DialogContent>
</Dialog>
{estudiosFiltrados.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<ClipboardList className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No hay estudios complementarios</p>
</div>
) : (
<div className="space-y-3">
{estudiosFiltrados.sort((a, b) => new Date(b.fecha).getTime() - new Date(a.fecha).getTime()).map(e => (
<Card key={e.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Calendar className="h-4 w-4 text-gray-500" />
<span className="text-sm font-medium">{e.fecha}</span>
</div>
<h4 className="font-semibold text-gray-900 dark:text-white">{e.tipo}</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1 whitespace-pre-wrap">{e.resultado}</p>
</div>
<div className="flex gap-1 self-start">
{canEdit && <>
<Button size="sm" variant="outline" onClick={() => loadEdit(e)}>
<Pencil className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(e.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</>}
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}
function SeccionInterconsultas({ interconsultas, pacienteId, internacionId, add, update, del, canEdit, portalNode}: {
interconsultas: Interconsulta[];
pacienteId: string;
internacionId: string;
add: (ic: Omit<Interconsulta, 'id'>) => void;
update: (id: string, datos: Partial<Interconsulta>) => void;
del: (id: string) => void;
canEdit?: boolean;
portalNode?: HTMLDivElement | null;
}) {
const [dialog, setDialog] = useState(false);
const [fecha, setFecha] = useState(getLocalToday());
const [servicioInterconsultado, setServicioInterconsultado] = useState('');
const [motivo, setMotivo] = useState('');
const [respuestaInterconsulta, setRespuestaInterconsulta] = useState('');
const [respuestaFecha, setRespuestaFecha] = useState('');
const [edit, setEdit] = useState<Interconsulta | null>(null);
const reset = () => {
setFecha(getLocalToday());
setServicioInterconsultado('');
setMotivo('');
setRespuestaInterconsulta('');
setRespuestaFecha('');
setEdit(null);
};
const handleGuardar = () => {
if (!servicioInterconsultado) return;
const datos: Partial<Interconsulta> = { fecha, servicioInterconsultado, motivo, respuestaInterconsulta };
if (respuestaInterconsulta) {
datos.respuestaFecha = respuestaFecha || getLocalToday();
}
if (edit) {
update(edit.id, datos);
} else {
add({ pacienteId, internacionId, ...datos });
}
setDialog(false);
reset();
};
const loadEdit = (ic: Interconsulta) => {
setEdit(ic);
setFecha(ic.fecha || getLocalToday());
setServicioInterconsultado(ic.servicioInterconsultado || '');
setMotivo(ic.motivo || '');
setRespuestaInterconsulta(ic.respuestaInterconsulta || '');
setRespuestaFecha(ic.respuestaFecha || '');
setDialog(true);
};
const icsFiltrados = (interconsultas || []).filter(ic => ic.internacionId === internacionId);
return (
<div className="space-y-4">
{portalNode ? createPortal(
canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />
Nueva IC
</Button>,
portalNode
) : (
<div className="flex justify-between items-center mb-4">
{canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />
Nueva IC
</Button>}
</div>
)}
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{edit ? 'Editar' : 'Nueva'} Interconsulta</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Fecha de Solicitud</Label>
<Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} />
</div>
<div>
<Label>Servicio Interconsultado</Label>
<Input
value={servicioInterconsultado}
onChange={e => setServicioInterconsultado(e.target.value)}
placeholder="Ej: Cardiología, Neurología, Infectología, etc."
/>
</div>
<div>
<Label>Motivo</Label>
<Input
value={motivo}
onChange={e => setMotivo(e.target.value)}
placeholder="Motivo de la interconsulta"
/>
</div>
<div>
<Label>Fecha de Respuesta</Label>
<Input type="date" value={respuestaFecha} onChange={e => setRespuestaFecha(e.target.value)} />
</div>
<div>
<Label>Respuesta</Label>
<textarea
className="w-full p-2 border rounded-md text-sm min-h-[100px]"
value={respuestaInterconsulta}
onChange={e => setRespuestaInterconsulta(e.target.value)}
placeholder="Ingrese la respuesta de la interconsulta..."
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setDialog(false)}>Cancelar</Button>
<Button onClick={handleGuardar} disabled={!servicioInterconsultado}>
<Save className="h-4 w-4 mr-2" />
Guardar
</Button>
</div>
</DialogContent>
</Dialog>
{icsFiltrados.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<Users className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No hay interconsultas</p>
</div>
) : (
<div className="space-y-3">
{icsFiltrados.sort((a, b) => new Date(b.fecha).getTime() - new Date(a.fecha).getTime()).map(ic => (
<Card key={ic.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="flex-1">
<div className="flex flex-wrap items-center gap-2 mb-2">
<Calendar className="h-4 w-4 text-gray-500" />
<span className="text-sm font-medium">{ic.fecha}</span>
{!ic.respuestaInterconsulta ? (
<Badge className="bg-orange-500 text-white hover:bg-orange-600">Pendiente</Badge>
) : (
<Badge className="bg-green-500 text-white hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600">Resuelta</Badge>
)}
</div>
<h4 className="font-semibold text-gray-900 dark:text-white">{ic.servicioInterconsultado}</h4>
{ic.motivo && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{ic.motivo}</p>
)}
{ic.respuestaInterconsulta && (
<>
{ic.respuestaFecha && (
<Badge variant="outline" className="mt-2 bg-green-50 text-green-700 border-green-200 dark:bg-green-900 dark:text-green-300 dark:border-green-700">
<CheckCircle2 className="h-3 w-3 mr-1" />
<p className="font-semibold">Respuesta: {ic.respuestaFecha}<br />
<span className="text-sm text-gray-600 dark:text-gray-300 mt-2 whitespace-pre-wrap">{ic.respuestaInterconsulta} <br /></span>
</p>
</Badge>
)}
</>
)}
</div>
<div className="flex gap-1 self-start">
{canEdit && <>
<Button size="sm" variant="outline" onClick={() => loadEdit(ic)}>
<Pencil className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(ic.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</>}
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}
function SeccionATB({ atb, internacionId, pacienteId, add, update, del, canEdit, portalNode}: {
atb: ATB[];
internacionId: string;
pacienteId: string;
add: (a: Omit<ATB, 'id'>) => void;
update: (id: string, data: Partial<ATB>) => void;
del: (id: string) => void;
canEdit?: boolean;
portalNode?: HTMLDivElement | null;
}) {
const [dialog, setDialog] = useState(false);
const [edit, setEdit] = useState<ATB | null>(null);
const [antibiotico, setAntibiotico] = useState('');
const [fechaInicio, setFechaInicio] = useState(getLocalToday());
const [fechaFinalizacion, setFechaFinalizacion] = useState('');
const calcularDias = (inicio: string, fin?: string): number => {
const ini = new Date(inicio);
const finDate = fin ? new Date(fin) : new Date();
const diff = finDate.getTime() - ini.getTime();
return Math.ceil(diff / (1000 * 60 * 60 * 24));
};
const reset = () => {
setEdit(null);
setAntibiotico('');
setFechaInicio(getLocalToday());
setFechaFinalizacion('');
};
const loadEdit = (a: ATB) => {
setEdit(a);
setAntibiotico(a.antibiotico || '');
setFechaInicio(a.fechaInicio || getLocalToday());
setFechaFinalizacion(a.fechaFinalizacion || '');
};
const handleGuardar = () => {
if (!antibiotico.trim() || !fechaInicio) return;
const todayStr = getLocalToday();
if (edit) {
// 1. Modificar el antibiótico actual agregando como fecha de finalización la fecha actual en qué se modifica (manteniendo el nombre y fecha de inicio original)
update(edit.id, {
antibiotico: edit.antibiotico,
fechaInicio: edit.fechaInicio,
fechaFinalizacion: todayStr
});
// 2. Agregar un nuevo esquema antibiótico con el antibiótico nuevo y fecha de inicio de hoy
add({
pacienteId,
internacionId,
antibiotico,
fechaInicio: todayStr,
fechaFinalizacion: fechaFinalizacion || undefined
});
} else {
add({ pacienteId, internacionId, antibiotico, fechaInicio, fechaFinalizacion: fechaFinalizacion || undefined });
}
setDialog(false);
reset();
};
const atbOrdenado = atb.sort((a, b) => new Date(b.fechaInicio).getTime() - new Date(a.fechaInicio).getTime());
return (
<div className="space-y-4">
{portalNode ? createPortal(
canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />Nuevo
</Button>,
portalNode
) : (
<div className="flex justify-between mb-4">
{canEdit && <Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />Nuevo
</Button>}
</div>
)}
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{edit ? 'Editar' : 'Nuevo'} Antibiótico</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Antibiótico</Label>
<Input value={antibiotico} onChange={e => setAntibiotico(e.target.value)} placeholder="Nombre del antibiótico" />
</div>
<div>
<Label>Fecha de Inicio</Label>
<Input type="date" value={fechaInicio} onChange={e => setFechaInicio(e.target.value)} />
</div>
<div>
<Label>Fecha de Finalización (opcional)</Label>
<Input type="date" value={fechaFinalizacion} onChange={e => setFechaFinalizacion(e.target.value)} />
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setDialog(false)}>
<X className="h-4 w-4 mr-2" />Cancelar
</Button>
<Button onClick={handleGuardar}>
<Save className="h-4 w-4 mr-2" />Guardar
</Button>
</div>
</DialogContent>
</Dialog>
{atbOrdenado.length === 0 ? (
<div className="text-center py-8 text-gray-500">No hay esquemas antibioticos registrados</div>
) : (
<div className="space-y-2">
{atbOrdenado.map(a => (
<Card key={a.id}>
<CardContent className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex-1">
<div className="font-medium">{a.antibiotico}</div>
<div className="text-sm text-gray-500 flex flex-wrap gap-2 mt-1">
<span>Inicio: {formatDateDDMMYYYY(a.fechaInicio)}</span>
{a.fechaFinalizacion && (
<span>Fin: {formatDateDDMMYYYY(a.fechaFinalizacion)}</span>
)}
<Badge className="bg-green-100 text-green-800">
{calcularDias(a.fechaInicio, a.fechaFinalizacion)} días
</Badge>
</div>
</div>
<div className="flex gap-1">
{canEdit && <>
<Button size="sm" variant="outline" onClick={() => loadEdit(a)}>
<Pencil className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(a.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</>}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}
function SeccionPendientes({ pacienteId, internacionId, canEdit, portalNode}: { pacienteId: string; internacionId: string; canEdit: boolean; portalNode?: HTMLDivElement | null; }) {
const { pendientes, agregarPendiente, actualizarPendiente, eliminarPendiente, currentUser } = useHospitalStore();
const [modalAbierto, setModalAbierto] = useState(false);
const [pendienteEditar, setPendienteEditar] = useState<Pendiente | null>(null);
// Form fields
const [descripcion, setDescripcion] = useState('');
const [categoria, setCategoria] = useState<Pendiente['categoria']>('General');
const [prioridad, setPrioridad] = useState<Pendiente['prioridad']>('media');
const [observaciones, setObservaciones] = useState('');
const [fechaProgramada, setFechaProgramada] = useState('');
const [horaProgramada, setHoraProgramada] = useState('');
// Filters
const [filtroEstado, setFiltroEstado] = useState<'todos' | 'pendiente' | 'realizado' | 'cancelado'>('pendiente');
const [filtroCategoria, setFiltroCategoria] = useState<string>('todas');
const [busqueda, setBusqueda] = useState('');
const misPendientes = (pendientes || []).filter(p => p.pacienteId === pacienteId || p.internacionId === internacionId);
const pendientesFiltrados = misPendientes.filter(p => {
if (filtroEstado !== 'todos' && p.estado !== filtroEstado) return false;
if (filtroCategoria !== 'todas' && p.categoria !== filtroCategoria) return false;
if (busqueda.trim()) {
const query = busqueda.toLowerCase();
const descMatch = p.descripcion.toLowerCase().includes(query);
const obsMatch = (p.observaciones || '').toLowerCase().includes(query);
const profMatch = (p.profesional || '').toLowerCase().includes(query);
if (!descMatch && !obsMatch && !profMatch) return false;
}
return true;
}).sort((a, b) => {
if (a.estado === 'pendiente' && b.estado !== 'pendiente') return -1;
if (a.estado !== 'pendiente' && b.estado === 'pendiente') return 1;
const prioWeight = { alta: 3, media: 2, baja: 1 };
if (prioWeight[b.prioridad] !== prioWeight[a.prioridad]) {
return prioWeight[b.prioridad] - prioWeight[a.prioridad];
}
return new Date(`${b.fechaCreacion}T${b.horaCreacion || '00:00'}`).getTime() - new Date(`${a.fechaCreacion}T${a.horaCreacion || '00:00'}`).getTime();
});
const resetForm = () => {
setDescripcion('');
setCategoria('General');
setPrioridad('media');
setObservaciones('');
setFechaProgramada('');
setHoraProgramada('');
setPendienteEditar(null);
};
const openEditar = (p: Pendiente) => {
setPendienteEditar(p);
setDescripcion(p.descripcion);
setCategoria(p.categoria);
setPrioridad(p.prioridad);
setObservaciones(p.observaciones || '');
setFechaProgramada(p.fechaProgramada || '');
setHoraProgramada(p.horaProgramada || '');
setModalAbierto(true);
};
const handleGuardar = async () => {
if (!descripcion.trim()) {
toast.error('La descripción del pendiente es obligatoria');
return;
}
if (horaProgramada.trim()) {
const timeRegex = /^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/;
if (!timeRegex.test(horaProgramada.trim())) {
toast.error('La hora programada debe estar en formato de 24hs (ej: 14:30)');
return;
}
}
try {
const payload: Partial<Pendiente> = {
descripcion,
categoria,
prioridad,
observaciones: observaciones.trim() || undefined,
fechaProgramada: fechaProgramada || undefined,
horaProgramada: horaProgramada || undefined,
};
if (pendienteEditar) {
await actualizarPendiente(pendienteEditar.id, payload);
toast.success('Pendiente actualizado con éxito');
} else {
const profesional = currentUser ? getNombreProfesional(currentUser) : 'Sistema';
await agregarPendiente({
pacienteId,
internacionId,
...payload,
estado: 'pendiente',
fechaCreacion: getLocalToday(),
horaCreacion: new Date().toTimeString().slice(0, 5),
profesional,
});
toast.success('Pendiente añadido con éxito');
}
setModalAbierto(false);
resetForm();
} catch {
toast.error('Error al guardar el pendiente');
}
};
const handleToggleEstado = async (p: Pendiente) => {
const nuevoEstado = p.estado === 'pendiente' ? 'realizado' : 'pendiente';
try {
await actualizarPendiente(p.id, {
estado: nuevoEstado,
fechaRealizado: nuevoEstado === 'realizado' ? getLocalToday() : undefined,
usuarioRealizado: nuevoEstado === 'realizado' && currentUser ? getNombreProfesional(currentUser) : undefined,
});
toast.success(nuevoEstado === 'realizado' ? 'Marcado como realizado' : 'Marcado como pendiente');
} catch {
toast.error('Error al cambiar estado');
}
};
const handleEliminar = async (id: string) => {
try {
await eliminarPendiente(id);
toast.success('Pendiente eliminado');
} catch {
toast.error('Error al eliminar');
}
};
const handleCopiarPendientes = () => {
const activos = misPendientes.filter(p => p.estado === 'pendiente');
if (activos.length === 0) {
toast.info('No hay pendientes activos para copiar');
return;
}
const texto = activos.map((p, idx) => {
let line = `${idx + 1}. [${p.categoria.toUpperCase()}] ${p.descripcion}`;
if (p.fechaProgramada) line += ` (Programado: ${formatDateDDMMYYYY(p.fechaProgramada)}${p.horaProgramada ? ' ' + p.horaProgramada : ''})`;
if (p.prioridad === 'alta') line += ' (ALTA PRIORIDAD)';
if (p.observaciones) line += ` - Obs: ${p.observaciones}`;
return line;
}).join('\n');
navigator.clipboard.writeText(`PENDIENTES DEL PACIENTE:\n${texto}`);
toast.success('Pendientes copiados al portapapeles');
};
const getPriorityBadge = (prio: Pendiente['prioridad']) => {
switch (prio) {
case 'alta':
return <Badge className="bg-red-100 text-red-800 dark:bg-red-900/60 dark:text-red-200 border-red-200">Alta</Badge>;
case 'media':
return <Badge className="bg-amber-100 text-amber-800 dark:bg-amber-900/60 dark:text-amber-200 border-amber-200">Media</Badge>;
case 'baja':
return <Badge className="bg-blue-100 text-blue-800 dark:bg-blue-900/60 dark:text-blue-200 border-blue-200">Baja</Badge>;
}
};
const getCategoryBadge = (cat: Pendiente['categoria']) => {
return <Badge variant="outline" className="text-xs">{cat}</Badge>;
};
return (
<div className="space-y-4">
{/* Header controls */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 bg-card p-4 rounded-lg border shadow-sm">
<div className="flex flex-wrap items-center gap-2 w-full sm:w-auto">
<div className="relative flex-1 sm:w-60">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar pendientes..."
value={busqueda}
onChange={e => setBusqueda(e.target.value)}
className="pl-8 h-9 text-xs"
/>
</div>
<Select value={filtroEstado} onValueChange={(val: 'todos' | 'pendiente' | 'realizado' | 'cancelado') => setFiltroEstado(val)}>
<SelectTrigger className="w-[130px] h-9 text-xs">
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pendiente">Pendientes</SelectItem>
<SelectItem value="realizado">Realizados</SelectItem>
<SelectItem value="cancelado">Cancelados</SelectItem>
<SelectItem value="todos">Todos</SelectItem>
</SelectContent>
</Select>
<Select value={filtroCategoria} onValueChange={setFiltroCategoria}>
<SelectTrigger className="w-[150px] h-9 text-xs">
<SelectValue placeholder="Categoría" />
</SelectTrigger>
<SelectContent>
<SelectItem value="todas">Todas las cat.</SelectItem>
<SelectItem value="General">General</SelectItem>
<SelectItem value="Estudio">Estudio</SelectItem>
<SelectItem value="Procedimiento">Procedimiento</SelectItem>
<SelectItem value="Laboratorio">Laboratorio</SelectItem>
<SelectItem value="Interconsulta">Interconsulta</SelectItem>
<SelectItem value="Tratamiento">Tratamiento</SelectItem>
<SelectItem value="Administrativo">Administrativo</SelectItem>
</SelectContent>
</Select>
</div>
{portalNode ? createPortal(
<>
<Button variant="outline" size="sm" onClick={handleCopiarPendientes} title="Copiar pendientes activos">
<Copy className="h-4 w-4 mr-1.5" />
Copiar
</Button>
{canEdit && (
<Button size="sm" onClick={() => { resetForm(); setModalAbierto(true); }}>
<Plus className="h-4 w-4 mr-1.5" />
Nuevo Pendiente
</Button>
)}
</>,
portalNode
) : (
<div className="flex items-center gap-2 w-full sm:w-auto justify-end mb-4">
<Button variant="outline" size="sm" onClick={handleCopiarPendientes} title="Copiar pendientes activos">
<Copy className="h-4 w-4 mr-1.5" />
Copiar
</Button>
{canEdit && (
<Button size="sm" onClick={() => { resetForm(); setModalAbierto(true); }}>
<Plus className="h-4 w-4 mr-1.5" />
Nuevo Pendiente
</Button>
)}
</div>
)}
</div>
{/* List of Pendientes */}
{pendientesFiltrados.length === 0 ? (
<Card className="p-8 text-center text-muted-foreground border-dashed">
<ListTodo className="h-10 w-10 mx-auto mb-2 opacity-40" />
<p className="font-medium">No se encontraron registros de pendientes</p>
<p className="text-xs mt-1">
{misPendientes.length === 0
? 'Utilice el botón "Nuevo Pendiente" para agregar una tarea pendiente para este paciente.'
: 'Pruebe cambiar los filtros para ver otros registros.'}
</p>
</Card>
) : (
<div className="space-y-2.5">
{pendientesFiltrados.map(p => (
<Card
key={p.id}
className={`transition-colors ${
p.estado === 'realizado' ? 'bg-muted/30 border-muted opacity-75' : 'hover:border-primary/50'
}`}
>
<CardContent className="p-4 flex items-start gap-3">
{canEdit && (
<button
onClick={() => handleToggleEstado(p)}
className="mt-0.5 text-muted-foreground hover:text-primary transition-colors focus:outline-none"
title={p.estado === 'pendiente' ? 'Marcar como realizado' : 'Marcar como pendiente'}
>
{p.estado === 'realizado' ? (
<CheckSquare className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
) : (
<Square className="h-5 w-5" />
)}
</button>
)}
<div className="flex-1 min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className={`font-medium text-sm ${p.estado === 'realizado' ? 'line-through text-muted-foreground' : ''}`}>
{p.descripcion}
</span>
{getPriorityBadge(p.prioridad)}
{getCategoryBadge(p.categoria)}
{p.estado === 'realizado' && (
<Badge className="bg-emerald-100 text-emerald-800 dark:bg-emerald-900/60 dark:text-emerald-200">
Realizado
</Badge>
)}
</div>
{p.fechaProgramada && (
<div className="flex items-center gap-1.5 text-xs font-semibold text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 px-2 py-1 rounded w-fit">
<Clock className="h-3.5 w-3.5" />
Programado: {formatDateDDMMYYYY(p.fechaProgramada)} {p.horaProgramada ? `a las ${p.horaProgramada} hs` : ''}
</div>
)}
{p.observaciones && (
<p className="text-xs text-muted-foreground bg-muted/40 p-2 rounded border mt-1">
{p.observaciones}
</p>
)}
<div className="flex flex-wrap items-center gap-3 text-[11px] text-muted-foreground pt-1">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
Creado: {formatDateDDMMYYYY(p.fechaCreacion)} {p.horaCreacion || ''}
</span>
{p.profesional && <span>Por: {p.profesional}</span>}
{p.fechaRealizado && (
<span className="text-emerald-600 dark:text-emerald-400 font-medium">
Realizado el {formatDateDDMMYYYY(p.fechaRealizado)} {p.usuarioRealizado ? `por ${p.usuarioRealizado}` : ''}
</span>
)}
</div>
</div>
{canEdit && (
<div className="flex items-center gap-1">
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => openEditar(p)} title="Editar">
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/40"
onClick={() => handleEliminar(p.id)}
title="Eliminar pendiente"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
)}
</CardContent>
</Card>
))}
</div>
)}
{/* Modal Crear/Editar */}
<Dialog open={modalAbierto} onOpenChange={setModalAbierto}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ListTodo className="h-5 w-5 text-blue-600" />
{pendienteEditar ? 'Editar Pendiente' : 'Nuevo Pendiente de Paciente'}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 pt-2">
<div>
<Label className="text-xs font-medium">Descripción del Pendiente *</Label>
<Textarea
placeholder="Ej: Solicitar ecografía abdominal, Chequear laboratorio de control, Pendiente interconsulta con Cardiología..."
value={descripcion}
onChange={e => setDescripcion(e.target.value)}
rows={3}
className="mt-1"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-xs font-medium">Categoría</Label>
<Select value={categoria} onValueChange={(val: Pendiente['categoria']) => setCategoria(val)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Categoría" />
</SelectTrigger>
<SelectContent>
<SelectItem value="General">General</SelectItem>
<SelectItem value="Estudio">Estudio</SelectItem>
<SelectItem value="Procedimiento">Procedimiento</SelectItem>
<SelectItem value="Laboratorio">Laboratorio</SelectItem>
<SelectItem value="Interconsulta">Interconsulta</SelectItem>
<SelectItem value="Tratamiento">Tratamiento</SelectItem>
<SelectItem value="Administrativo">Administrativo</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs font-medium">Prioridad</Label>
<Select value={prioridad} onValueChange={(val: Pendiente['prioridad']) => setPrioridad(val)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Prioridad" />
</SelectTrigger>
<SelectContent>
<SelectItem value="alta">Alta</SelectItem>
<SelectItem value="media">Media</SelectItem>
<SelectItem value="baja">Baja</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-3 p-3 bg-muted/30 rounded-lg border">
<div>
<Label className="text-xs font-medium">Fecha Programada</Label>
<Input
type="date"
value={fechaProgramada}
onChange={e => setFechaProgramada(e.target.value)}
className="mt-1 h-9 text-xs"
/>
</div>
<div>
<Label className="text-xs font-medium">Hora Programada (24hs)</Label>
<Input
type="text"
placeholder="HH:MM (ej. 14:30)"
value={horaProgramada}
onChange={e => {
let val = e.target.value;
val = val.replace(/[^0-9:]/g, '');
if (val.length <= 5) {
setHoraProgramada(val);
}
}}
className="mt-1 h-9 text-xs"
/>
</div>
</div>
<div>
<Label className="text-xs font-medium">Observaciones / Detalles (opcional)</Label>
<Textarea
placeholder="Detalles adicionales, indicaciones específicas..."
value={observaciones}
onChange={e => setObservaciones(e.target.value)}
rows={2}
className="mt-1"
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-4 pt-2 border-t">
<Button variant="outline" onClick={() => setModalAbierto(false)}>
Cancelar
</Button>
<Button onClick={handleGuardar}>
<Save className="h-4 w-4 mr-1.5" />
Guardar
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}