Files
administracionHospital/src/sections/HistoriaClinica.tsx
T

2377 lines
121 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 { PDFDocument } from 'pdf-lib';
import { User } from 'lucide-react';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
FlaskConical,
Microscope,
Plus,
Calendar,
Clock,
AlertCircle,
CheckCircle2,
ArrowLeft,
Pencil,
Trash2,
FileText,
ChevronDown,
ChevronUp,
List,
FileDown,
Thermometer,
Heart,
Wind,
Droplets,
Save,
X,
MoreHorizontal,
Edit,
ClipboardList,
TrendingUp,
TypeOutline,
Activity,
Pill,
Users
} from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } 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, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario, Interconsulta, ATB } from '@/types';
import { formatDateDDMMYYYY } from '@/lib/utils';
interface HistoriaClinicaProps {
internacion: Internacion;
paciente: Paciente;
cama?: Cama;
allCamas?: Cama[];
evoluciones: Evolucion[];
laboratorios: Laboratorio[];
acidosBase: AcidoBase[];
cultivos: Cultivo[];
estudiosComplementarios: EstudioComplementario[];
interconsultas: Interconsulta[];
atb: ATB[];
onAgregarEvolucion: (evolucion: Omit<Evolucion, 'id'>) => void;
onActualizarEvolucion: (id: string, datos: Partial<Evolucion>) => void;
onEliminarEvolucion: (id: string) => void;
onAgregarLaboratorio: (laboratorios: Omit<Laboratorio, 'id'>) => void;
onActualizarLaboratorio: (id: string, datos: Partial<Laboratorio>) => void;
onEliminarLaboratorio: (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;
onActualizarInternacion: (id: string, datos: Partial<Internacion>) => void;
onActualizarCama: (id: string, datos: Partial<Cama>) => void;
onVolver: () => void;
onEditarIngreso?: () => void;
}
const PARAMETROS_COMUNES: Record<string, { unidad: string; referencia: string }> = {
'Hemoglobina': { unidad: 'g/dL', referencia: '12-14' },
'Hematocrito': { unidad: '%', referencia: '36-42' },
'Glóbulos Blancos': { unidad: 'cel/μL', referencia: '4000-11000' },
'Plaquetas': { unidad: 'unidades/μL', referencia: '150000-400000' },
'Glucosa': { unidad: 'mg/dL', referencia: '70-100' },
'Urea': { unidad: 'mg/dL', referencia: '17-48.5' },
'Creatinina': { unidad: 'mg/dL', referencia: '0.5-1' },
'Sodio': { unidad: 'mEq/L', referencia: '135-145' },
'Potasio': { unidad: 'mEq/L', referencia: '3.5-5.1' },
'Cloro': { unidad: 'mEq/L', referencia: '101-109' },
'Bilirrubina Total': { unidad: 'mg/dL', referencia: '0-1.2' },
'Bilirrubina Directa': { unidad: 'mg/dL', referencia: '0-0.3' },
'TGO/AST': { unidad: 'U/L', referencia: '0-35' },
'TGP/ALT': { unidad: 'U/L', referencia: '0-35' },
'TP': { unidad: '%', referencia: '70-100' },
'KPTT': { unidad: 'seg', referencia: '30-45' },
'RIN': { unidad: '', referencia: '0.8-1.5' },
};
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 },
'Glucemia': { 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 },
};
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 (num < rango.min) return 'Bajo';
if (num > rango.max) return 'Alto';
return 'Normal';
}
function wrapText(text: string, font: any, 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,
allCamas,
evoluciones,
laboratorios,
acidosBase,
cultivos,
estudiosComplementarios,
interconsultas,
onAgregarEvolucion,
onActualizarEvolucion,
onEliminarEvolucion,
onAgregarLaboratorio,
onActualizarLaboratorio,
onEliminarLaboratorio,
onAgregarAcidoBase,
onActualizarAcidoBase,
onEliminarAcidoBase,
onAgregarCultivo,
onActualizarCultivo,
onEliminarCultivo,
onAgregarEstudioComplementario,
onActualizarEstudioComplementario,
onEliminarEstudioComplementario,
onAgregarInterconsulta,
onActualizarInterconsulta,
onEliminarInterconsulta,
atb,
onAgregarATB,
onActualizarATB,
onEliminarATB,
onActualizarInternacion,
onActualizarCama,
onVolver,
onEditarIngreso,
}: HistoriaClinicaProps) {
const [tabActivo, setTabActivo] = useState('evoluciones');
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">
<ClipboardList className="h-6 w-6 text-blue-600" />
{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>
<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>
<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" 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}>
<ScrollArea className="w-full rounded-md border bg-background whitespace-nowrap">
<TabsList className="w-full">
<TabsTrigger value="evoluciones">
<FileText className="h-3 w-3 sm:h-4 sm:w-4" />
<span>Evoluciones</span>
({evoluciones.length})
</TabsTrigger>
<TabsTrigger value="indicaciones">
<List className="h-3 w-3 sm:h-4 sm:w-4" />
<span>Indicaciones</span>
</TabsTrigger>
<TabsTrigger value="laboratorios">
<FlaskConical className="h-3 w-3 sm:h-4 sm:w-4" />
<span>Laboratorio</span>
({laboratorios.length})
</TabsTrigger>
<TabsTrigger value="acidobase">
<Activity className="h-3 w-3 sm:h-4 sm:w-4" />
<span>EAB</span>
({acidosBase.length})
</TabsTrigger>
<TabsTrigger value="cultivos">
<Microscope className="h-3 w-3 sm:h-4 sm:w-4" />
<span>Cultivos</span>
({cultivos.length})
</TabsTrigger>
<TabsTrigger value="atb">
<Pill className="h-3 w-3 sm:h-4 sm:w-4" />
<span>ATB</span>
({atb.length})
</TabsTrigger>
<TabsTrigger value="estudios">
<ClipboardList className="h-3 w-3 sm:h-4 sm:w-4" />
<span>Estudios</span>
({estudiosComplementarios.length})
</TabsTrigger>
<TabsTrigger value="interconsultas">
<Users className="h-3 w-3 sm:h-4 sm:w-4" />
<span>Interconsultas</span>
({interconsultas.length})
</TabsTrigger>
</TabsList>
<ScrollBar orientation="horizontal" />
</ScrollArea>
<TabsContent value="laboratorios" className="mt-4 min-w-0 w-full overflow-hidden">
<SeccionLaboratorios lab={laboratorios} patientId={paciente.id} add={onAgregarLaboratorio} update={onActualizarLaboratorio} del={onEliminarLaboratorio} addAcidoBase={onAgregarAcidoBase} />
</TabsContent>
<TabsContent value="evoluciones" className="mt-4">
<SeccionEvoluciones evos={evoluciones} internacionId={internacion.id} add={onAgregarEvolucion} update={onActualizarEvolucion} del={onEliminarEvolucion} />
</TabsContent>
<TabsContent value="acidobase" className="mt-4">
<SeccionAcidosBase ab={acidosBase} patientId={paciente.id} add={onAgregarAcidoBase} update={onActualizarAcidoBase} del={onEliminarAcidoBase} />
</TabsContent>
<TabsContent value="cultivos" className="mt-4">
<SeccionCultivos cults={cultivos} patient={paciente} add={onAgregarCultivo} update={onActualizarCultivo} del={onEliminarCultivo} />
</TabsContent>
<TabsContent value="atb" className="mt-4">
<SeccionATB
atb={atb}
internacionId={internacion.id}
pacienteId={paciente.id}
add={onAgregarATB}
update={onActualizarATB}
del={onEliminarATB}
/>
</TabsContent>
<TabsContent value="estudios" className="mt-4">
<SeccionEstudiosComplementarios
estudios={estudiosComplementarios}
internacionId={internacion.id}
pacienteId={paciente.id}
add={onAgregarEstudioComplementario}
update={onActualizarEstudioComplementario}
del={onEliminarEstudioComplementario}
/>
</TabsContent>
<TabsContent value="interconsultas" className="mt-4">
<SeccionInterconsultas
interconsultas={interconsultas as Interconsulta[]}
pacienteId={paciente.id}
internacionId={internacion.id}
add={onAgregarInterconsulta}
update={onActualizarInterconsulta}
del={onEliminarInterconsulta}
/>
</TabsContent>
</Tabs>
</div>
</div>
</div>
);
}
function SeccionLaboratorios({ lab, patientId, add, update, del, addAcidoBase }: {
lab: Laboratorio[];
patientId: string;
add: (l: Omit<Laboratorio, 'id'>) => void;
update: (id: string, data: Partial<Laboratorio>) => void;
del: (id: string) => void;
addAcidoBase?: (a: Omit<AcidoBase, 'id'>) => void;
}) {
const [dialog, setDialog] = useState(false);
const [obsDialog, setObsDialog] = useState(false);
const [evolDialog, setEvolDialog] = useState(false);
const [selectedLab, setSelectedLab] = useState<Laboratorio | null>(null);
const [edit, setEdit] = useState<Laboratorio | null>(null);
const [obsLab, setObsLab] = useState<Laboratorio | null>(null);
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
const [hora, setHora] = useState(new Date().toTimeString().slice(0, 5));
const [observaciones, setObservaciones] = useState('');
const [resultados, setResultados] = useState<ResultadoLaboratorio[]>([]);
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(new Date().toISOString().split('T')[0]);
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.resultados.some(r => r.parametro === parametroEvolucion))
.sort((a, b) => new Date(a.fecha).getTime() - new Date(b.fecha).getTime())
.map(l => {
const r = l.resultados.find(r => r.parametro === parametroEvolucion);
return { fecha: l.fecha, valor: r ? parseFloat(String(r.valor)) : null };
});
const getEstadoColor = (estado: ResultadoLaboratorio['estado']) => {
switch (estado) {
case 'Normal': return 'bg-green-100 text-green-800';
case 'Alto': return 'bg-amber-100 text-amber-800';
case 'Bajo': return 'bg-blue-100 text-blue-800';
case 'Crítico': return 'bg-red-100 text-red-800';
}
};
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 === 'Crítico') return 'text-red-600 font-bold';
if (r.estado === 'Alto') return 'text-amber-600';
if (r.estado === 'Bajo') return 'text-blue-600';
return 'text-green-600';
};
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(new Date().toISOString().split('T')[0]);
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 parseLaboratorioTexto = (texto: string): { resultados: ResultadoLaboratorio[]; observaciones: string } => {
const resultados: ResultadoLaboratorio[] = [];
const observacionesExtra: string[] = [];
const mapeoParametros: Record<string, { nombre: string; unidad: string; esPrincipal: boolean }> = {
'hematocrito': { nombre: 'Hematocrito', unidad: '%', esPrincipal: true },
'hemoglobina corpuscular media': { nombre: 'HCM', unidad: 'pg', esPrincipal: false },
'hemoglobina': { nombre: 'Hemoglobina', unidad: 'g/dL', esPrincipal: true },
'volumen corpuscular medio': { nombre: 'VCM', unidad: 'fL', esPrincipal: false },
'rdw': { nombre: 'RDW', unidad: '%', esPrincipal: false },
'eritroblastos': { nombre: 'Eritroblastos', unidad: '%', esPrincipal: false },
'neutrófilos': { nombre: 'Neutrófilos', unidad: '%', esPrincipal: false },
'linfocitos': { nombre: 'Linfocitos', unidad: '%', esPrincipal: false },
'monocitos': { nombre: 'Monocitos', unidad: '%', esPrincipal: false },
'eosinófilos': { nombre: 'Eosinófilos', unidad: '%', esPrincipal: false },
'basófilos': { nombre: 'Basófilos', unidad: '%', esPrincipal: false },
'leucocitos': { nombre: 'Leucocitos', unidad: '10³/µl', esPrincipal: true },
'recuento de leucocitos': { nombre: 'Leucocitos', unidad: '10³/µl', esPrincipal: true },
'recuento de plaquetas': { nombre: 'Plaquetas', unidad: '10³/µl', esPrincipal: true },
'plaquetas': { nombre: 'Plaquetas', unidad: '10³/µl', esPrincipal: true },
'volumen plaquetario medio': { nombre: 'VPM', unidad: 'fL', esPrincipal: false },
'procalcitonina': { nombre: 'Procalcitonina', unidad: 'ng/mL', esPrincipal: false },
'glucosa': { nombre: 'Glucemia', unidad: 'mg/dL', esPrincipal: true },
'urea': { nombre: 'Urea', unidad: 'mg/dL', esPrincipal: true },
'creatinina': { nombre: 'Creatinina', unidad: 'mg/dL', esPrincipal: true },
'mdrd': { nombre: 'Filtrado Glomerular (MDRD)', unidad: 'ml/min', esPrincipal: false },
'sodio': { nombre: 'Sodio', unidad: 'mEq/L', esPrincipal: true },
'potasio': { nombre: 'Potasio', unidad: 'mEq/L', esPrincipal: true },
'cloro': { nombre: 'Cloro', unidad: 'mEq/L', esPrincipal: true },
'bilirrubina total': { nombre: 'Bilirrubina Total', unidad: 'mg/dL', esPrincipal: true },
'bilirrubina directa': { nombre: 'Bilirrubina Directa', unidad: 'mg/dL', esPrincipal: true },
'got': { nombre: 'GOT', unidad: 'U/L', esPrincipal: true },
'gpt': { nombre: 'GPT', unidad: 'U/L', esPrincipal: true },
'fosfatasa alcalina': { nombre: 'Fosfatasa Alcalina', unidad: 'U/L', esPrincipal: false },
'proteínas totales': { nombre: 'Proteínas Totales', unidad: 'g/dL', esPrincipal: false },
'albúmina': { nombre: 'Albúmina', unidad: 'g/dL', esPrincipal: false },
'tiempo de protrombina': { nombre: 'Tiempo de Protrombina', unidad: '%', esPrincipal: true },
'rin': { nombre: 'INR', unidad: '', esPrincipal: true },
'aptt': { nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
'kptt': { nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
};
const lineas = texto.split('\n');
for (const linea of lineas) {
const lineaLower = linea.toLowerCase().trim();
for (const [clave, info] of Object.entries(mapeoParametros)) {
if (lineaLower.includes(clave)) {
// buscar primer valor numérico después de cualquier texto (letra o palabra)
const match = linea.match(/[a-zA-ZáéíóúñÁÉÍÓÚÑ]+.*?(\d+\.?\d*)/);
if (match && match[1]) {
const valor = parseFloat(match[1].replace(',', '.'));
if (!isNaN(valor) && valor > 0 && valor < 1000) {
const nombreNormalizado = info.nombre;
let valorFinal = valor;
if (nombreNormalizado === 'Leucocitos' || nombreNormalizado === 'Plaquetas') {
valorFinal = valor * 1000;
}
if (info.esPrincipal) {
resultados.push({
parametro: nombreNormalizado,
valor: valorFinal,
unidad: info.unidad,
estado: calcularEstadoLaboratorio(nombreNormalizado, match[1])
});
}
break;
}
}
break;
}
}
}
return { resultados, observaciones: observacionesExtra.join('\n') };
};
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;
let 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 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, fecha: importFecha, hora: importHora });
}
add({
pacienteId: patientId,
fecha: importFecha,
hora: importHora,
resultados: importResultados,
observaciones: importObservaciones
});
setImportDialog(false);
setImportTexto('');
setImportResultados([]);
setImportObservaciones('');
setImportAcidoBase(null);
setImportFecha(new Date().toISOString().split('T')[0]);
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 || '', 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;
});
return (
<div className="space-y-4 w-full max-w-full min-w-0">
<div className="flex justify-between">
<div className="flex gap-2">
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}><Plus className="h-4 w-4 mr-2" />Nuevo</Button>
<Button variant="outline" onClick={() => { setSelectedLab(lab[0] || null); setParametroEvolucion('Hematocrito'); setEvolDialog(true); }}><TrendingUp className="h-4 w-4 mr-2" />Evolución</Button>
<Button variant="outline" onClick={() => { setImportFecha(new Date().toISOString().split('T')[0]); 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={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">
<Table>
<TableHeader>
<TableRow>
<TableHead>Parámetro</TableHead>
<TableHead>Valor</TableHead>
<TableHead>Unidad</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{importResultados.map((r, idx) => (
<TableRow key={idx}>
<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>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
);
}
function SeccionEvoluciones({ evos, internacionId, add, update, del }: {
evos: Evolucion[];
internacionId: string;
add: (e: Omit<Evolucion, 'id'>) => void;
update: (id: string, data: Partial<Evolucion>) => void;
del: (id: string) => void;
}) {
const [dialog, setDialog] = useState(false);
const [edit, setEdit] = useState<Evolucion | null>(null);
const [expanded, setExpanded] = useState<string | null>(null);
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
const [hora, setHora] = useState(new Date().toTimeString().slice(0, 5));
const [medico, setMedico] = useState('');
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(new Date().toISOString().split('T')[0]);
setHora(new Date().toTimeString().slice(0, 5));
setMedico('');
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);
setMedico(e.medico);
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 = () => {
if (!medico) return;
const signosVitales = temperatura || presionSistolica || frecuenciaCardiaca
? { temperatura: temperatura ? parseFloat(temperatura) : undefined, presionSistolica: presionSistolica ? parseInt(presionSistolica) : undefined, presionDiastolica: presionDiastolica ? parseInt(presionDiastolica) : undefined, frecuenciaCardiaca: frecuenciaCardiaca ? parseInt(frecuenciaCardiaca) : undefined, frecuenciaRespiratoria: frecuenciaRespiratoria ? parseInt(frecuenciaRespiratoria) : undefined, saturacionO2: saturacionO2 ? parseInt(saturacionO2) : undefined }
: undefined;
const examenFisico = snc || cardiovascular || respiratorio || abdominal || genitourinario || pielAnexos || soma
? { SNC: snc || undefined, Cardiovascular: cardiovascular || undefined, Respiratorio: respiratorio || undefined, Abdominal: abdominal || undefined, Genitourinario: genitourinario || undefined, PielAnexos: pielAnexos || undefined, SOMA: soma || undefined }
: undefined;
const data = { fecha, hora, medico, signosVitales, examenFisico, novedades: novedades || undefined, comentario: comentario || undefined, pendientes: pendientes || undefined };
if (edit) update(edit.id, data);
else add({ internacionId, ...data, signosVitales, examenFisico });
setDialog(false);
reset();
};
return (
<div className="space-y-4">
<div className="flex">
<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={medico} onChange={e => setMedico(e.target.value)} 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={!medico}><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> :
evosFiltered.map(e => {
const isExpanded = expanded === e.id;
const sv = e.signosVitales;
const svText = sv ? [sv.presionSistolica && sv.presionDiastolica ? `PA: ${sv.presionSistolica}/${sv.presionDiastolica}` : null, sv.frecuenciaCardiaca ? `FC: ${sv.frecuenciaCardiaca}` : null, sv.frecuenciaRespiratoria ? `FR: ${sv.frecuenciaRespiratoria}` : null, sv.temperatura ? `T: ${sv.temperatura}°C` : null, sv.saturacionO2 ? `SatO2: ${sv.saturacionO2}%` : null].filter(Boolean).join(' | ') : null;
return (
<Card key={e.id} className="mb-2"><CardContent className="p-4">
<div className="flex flex-col gap-3">
<div className="flex items-start justify-between">
<div><p className="font-medium">{e.fecha} {e.hora}</p><p className="text-sm text-gray-600">Médico: {e.medico}</p></div>
<div className="flex gap-1">
<Button size="sm" variant="outline" onClick={() => setExpanded(isExpanded ? null : e.id)}>{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}</Button>
<Button size="sm" variant="outline" onClick={() => openEdit(e)}><Pencil /></Button>
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(e.id)}><Trash2 /></Button>
</div>
</div>
{svText && <div className="bg-blue-50 p-2 rounded-lg"><p className="text-sm font-medium text-blue-800 flex items-center gap-2"><Activity className="h-4 w-4" />Signos Vitales: {svText}</p></div>}
{isExpanded && <>
{e.examenFisico && <div className="bg-green-50 dark:bg-green-900 p-3 rounded-lg border border-green-200 dark:border-green-700"><p className="text-sm font-medium text-green-800 mb-2">Examen Físico:</p><div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs">
{e.examenFisico.SNC && <div><span className="font-medium">SNC:</span> {e.examenFisico.SNC}</div>}
{e.examenFisico.Cardiovascular && <div><span className="font-medium">CV:</span> {e.examenFisico.Cardiovascular}</div>}
{e.examenFisico.Respiratorio && <div><span className="font-medium">Resp:</span> {e.examenFisico.Respiratorio}</div>}
{e.examenFisico.Abdominal && <div><span className="font-medium">Abd:</span> {e.examenFisico.Abdominal}</div>}
{e.examenFisico.Genitourinario && <div><span className="font-medium">GU:</span> {e.examenFisico.Genitourinario}</div>}
{e.examenFisico.PielAnexos && <div><span className="font-medium">Piel:</span> {e.examenFisico.PielAnexos}</div>}
{e.examenFisico.SOMA && <div><span className="font-medium">SOMA:</span> {e.examenFisico.SOMA}</div>}
</div></div>}
{e.novedades && <div className="bg-red-50 p-3 rounded-lg border border-red-200 mt-3"><p className="text-sm font-medium text-red-800 mb-1">Novedades:</p><p className="text-sm text-gray-800 whitespace-pre-wrap">{e.novedades}</p></div>}
{e.comentario && <div><p className="text-sm font-medium text-gray-700">Comentario:</p><p className="text-sm text-gray-600 whitespace-pre-wrap">{e.comentario}</p></div>}
{e.pendientes && <div className="bg-amber-50 p-3 rounded-lg border border-amber-200"><p className="text-sm font-medium text-amber-800 mb-1">Pendientes:</p><p className="text-sm text-amber-700 whitespace-pre-wrap">{e.pendientes}</p></div>}
</>}
</div>
</CardContent></Card>
);
})
}
</div>
);
}
function SeccionAcidosBase({ ab, patientId, add, update, del }: {
ab: AcidoBase[];
patientId: string;
add: (a: Omit<AcidoBase, 'id'>) => void;
update: (id: string, datos: Partial<AcidoBase>) => void;
del: (id: string) => void;
}) {
const [dialog, setDialog] = useState(false);
const [editDialog, setEditDialog] = useState(false);
const [selected, setSelected] = useState<AcidoBase | null>(null);
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
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(new Date().toISOString().split('T')[0]);
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 getEstadoParametro = (param: string, val: number) => {
const refs: Record<string, { min: number; max: number }> = { ph: { min: 7.35, max: 7.45 }, pco2: { min: 35, max: 45 }, po2: { min: 80, max: 100 }, hco3: { min: 22, max: 26 }, be: { min: -2, max: 2 }, sato2: { min: 95, max: 100 } };
const r = refs[param];
if (!r) return { estado: 'Normal', color: 'text-gray-800' };
if (val < r.min) return { estado: 'Bajo', color: 'text-blue-600' };
if (val > r.max) return { estado: 'Alto', color: 'text-red-600' };
return { estado: 'Normal', color: 'text-green-600' };
};
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 getColorPh = (ph: number) => {
if (ph < 7.2 || ph > 7.6) return 'bg-red-100 text-red-800';
if (ph < 7.35 || ph > 7.45) return 'bg-amber-100 text-amber-800';
return 'bg-green-100 text-green-800';
};
const handle = () => {
if (!ph || !pco2 || !po2 || !hco3 || !be || !sato2) return;
const interpretacionAuto = interpretarGasometria();
add({ pacienteId: patientId, 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">
<div className="flex">
<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 p-3 rounded-lg border border-blue-200"><p className="text-sm font-medium text-blue-800 flex items-center gap-2"><Activity className="h-4 w-4" />Interpretación automática: {interpretarGasometria()}</p></div>}
<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 p-3 rounded-lg border border-blue-200"><p className="text-sm font-medium text-blue-800 flex items-center gap-2"><Activity className="h-4 w-4" />Interpretación automática: {interpretarGasometria()}</p></div>}
<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="badge"></span>;
const pf = a.po2 / a.fio2;
const color =
pf > 300 ? "green" :
pf > 200 ? "yellow" :
pf > 100 ? "orange" :
"red";
return (
<Badge className={`bg-${color}-100 text-${color}-900 border-${color}-100`}>
{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, add, update, del }: {
cults: Cultivo[];
patient: Paciente;
add: (c: Omit<Cultivo, 'id'>) => void;
update: (id: string, data: Partial<Cultivo>) => void;
del: (id: string) => void;
}) {
const [dialog, setDialog] = useState(false);
const [resDialog, setResDialog] = useState(false);
const [editDialog, setEditDialog] = useState(false);
const [selected, setSelected] = useState<Cultivo | null>(null);
const [isParcialMode, setIsParcialMode] = useState(false);
const [fechaToma, setFechaToma] = useState(new Date().toISOString().split('T')[0]);
const [protocolo, setProtocolo] = useState('');
const [tipoMuestra, setTipoMuestra] = useState<Cultivo['tipoMuestra']>('HMCx2');
const [observaciones, setObservaciones] = useState('');
const [fechaResultado, setFechaResultado] = useState(new Date().toISOString().split('T')[0]);
const [estadoResultado, setEstadoResultado] = useState<Cultivo['estado']>('Parcial');
const [germen, setGermen] = useState('');
const [sensible, setSensible] = useState('');
const [resistente, setResistente] = useState('');
const reset = () => {
setFechaToma(new Date().toISOString().split('T')[0]);
setProtocolo('');
setTipoMuestra('HMCx2');
setObservaciones('');
setSelected(null);
};
const resetRes = () => {
setFechaResultado(new Date().toISOString().split('T')[0]);
setEstadoResultado('Positivo');
setGermen('');
setSensible('');
setResistente('');
setSelected(null);
};
const abrirParcial = (c: Cultivo) => {
setSelected(c);
setProtocolo(c.protocolo || '');
setGermen(c.germen || '');
setSensible(c.sensible || '');
setResistente(c.resistente || '');
setResDialog(true);
};
const abrirDefinitivo = (c: Cultivo) => {
setSelected(c);
setProtocolo(c.protocolo || '');
setFechaResultado(c.fechaResultado || new Date().toISOString().split('T')[0]);
setEstadoResultado(c.estado === 'Parcial' || c.estado === 'Positivo' ? 'Positivo' : c.estado);
setGermen(c.germen || '');
setSensible(c.sensible || '');
setResistente(c.resistente || '');
setEditDialog(true);
};
const openParcial = (c: Cultivo) => {
setSelected(c);
setIsParcialMode(true);
setProtocolo(c.protocolo || '');
setGermen(c.germen || '');
setSensible(c.sensible || '');
setResistente(c.resistente || '');
setResDialog(true);
};
const openDefinitivo = (c: Cultivo) => {
setSelected(c);
setIsParcialMode(false);
setProtocolo(c.protocolo || '');
setFechaResultado(c.fechaResultado || new Date().toISOString().split('T')[0]);
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, fechaToma, protocolo: protocolo || undefined, tipoMuestra, observaciones: observaciones || undefined, estado: 'NAF/Pendiente' }); setDialog(false); reset(); };
const handleParcial = () => {
if (!selected) return;
update(selected.id, { protocolo: protocolo || undefined, estado: 'Parcial', germen: germen || undefined, sensible: sensible || undefined, resistente: resistente || undefined });
setResDialog(false);
resetRes();
};
const handleDefinitivo = () => {
if (!selected) return;
update(selected.id, { protocolo: protocolo || undefined, fechaResultado, estado: estadoResultado, germen: germen || undefined, sensible: sensible || undefined, resistente: resistente || undefined });
setEditDialog(false);
resetRes();
};
return (
<div className="space-y-4">
<div className="flex">
<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>
</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">{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); setIsParcialMode(false); }}><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">Protocolo: {c.protocolo}</p>}
{c.observaciones && <p className="text-sm text-gray-600 bg-gray-50 p-2 rounded">{c.observaciones}</p>}
{c.estado === 'Parcial' && c.germen && (
<div className="bg-orange-50 p-3 rounded-lg border border-orange-200">
<p className="text-sm font-medium text-orange-800 flex items-center gap-2"><AlertCircle className="h-4 w-4" />PARCIAL - Germen: {c.germen}</p>
{(c.sensible || c.resistente) && (
<div className="mt-2 space-y-1">
{c.sensible && <p className="text-xs text-orange-700">Sensible: {c.sensible}</p>}
{c.resistente && <p className="text-xs text-orange-700">Resistente: {c.resistente}</p>}
</div>
)}
</div>
)}
{c.estado === 'Positivo' && c.germen && (
<div className="bg-red-50 p-3 rounded-lg border border-red-200">
<p className="text-sm font-medium text-red-800 flex items-center gap-2"><AlertCircle className="h-4 w-4" />Germen: {c.germen}</p>
{c.fechaResultado && <p className="text-xs text-red-600 mt-1">Resultado: {c.fechaResultado}</p>}
{(c.sensible || c.resistente) && (
<div className="mt-2 space-y-2">
{c.sensible && <div><p className="text-xs font-medium text-green-700 mb-1">Sensible a:</p><p className="text-sm text-green-800">{c.sensible}</p></div>}
{c.resistente && <div><p className="text-xs font-medium text-red-700 mb-1">Resistente a:</p><p className="text-sm text-red-800">{c.resistente}</p></div>}
</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">
<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>
{c.fechaResultado && <p className="text-xs text-green-600 mt-1">Resultado: {c.fechaResultado}</p>}
</div>
)}
</div>
</CardContent></Card>
))}
</div>
</div>
</div>
);
}
function SeccionEstudiosComplementarios({ estudios, internacionId, pacienteId, add, update, del }: {
estudios: EstudioComplementario[];
internacionId: string;
pacienteId: string;
add: (e: Omit<EstudioComplementario, 'id'>) => void;
update: (id: string, datos: Partial<EstudioComplementario>) => void;
del: (id: string) => void;
}) {
const [dialog, setDialog] = useState(false);
const [filtro, setFiltro] = useState<'todos' | 'internacion'>('internacion');
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
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(new Date().toISOString().split('T')[0]);
setTipo('');
setResultado('');
setEdit(null);
};
const loadEdit = (e: EstudioComplementario) => {
setEdit(e);
setFecha(e.fecha);
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">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="flex gap-2">
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />
Nuevo Estudio
</Button>
</div>
<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">
<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 }: {
interconsultas: Interconsulta[];
pacienteId: string;
internacionId: string;
add: (ic: Omit<Interconsulta, 'id'>) => void;
update: (id: string, datos: Partial<Interconsulta>) => void;
del: (id: string) => void;
}) {
const [dialog, setDialog] = useState(false);
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
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(new Date().toISOString().split('T')[0]);
setServicioInterconsultado('');
setMotivo('');
setRespuestaInterconsulta('');
setRespuestaFecha('');
setEdit(null);
};
const handleGuardar = () => {
if (!servicioInterconsultado) return;
const datos: any = { fecha, servicioInterconsultado, motivo, respuestaInterconsulta };
if (respuestaInterconsulta) {
datos.respuestaFecha = respuestaFecha || new Date().toISOString().split('T')[0];
}
if (edit) {
update(edit.id, datos);
} else {
add({ pacienteId, internacionId, ...datos });
}
setDialog(false);
reset();
};
const loadEdit = (ic: Interconsulta) => {
setEdit(ic);
setFecha(ic.fecha);
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">
<div className="flex justify-between items-center">
<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-600 text-white hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600">Respondida</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" />
Respondido: {ic.respuestaFecha}
</Badge>
)}
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2 whitespace-pre-wrap">{ic.respuestaInterconsulta}</p>
</>
)}
</div>
<div className="flex gap-1 self-start">
<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 }: {
atb: ATB[];
internacionId: string;
pacienteId: string;
add: (a: Omit<ATB, 'id'>) => void;
update: (id: string, data: Partial<ATB>) => void;
del: (id: string) => void;
}) {
const [dialog, setDialog] = useState(false);
const [edit, setEdit] = useState<ATB | null>(null);
const [antibiotico, setAntibiotico] = useState('');
const [fechaInicio, setFechaInicio] = useState(new Date().toISOString().split('T')[0]);
const [fechaFinalizacion, setFechaFinalizacion] = useState('');
const calcularDias = (inicio: string, fin?: string): number => {
const ini = new Date(inicio);
const finDate = fin ? new Date(fin) : new Date();
const diff = finDate.getTime() - ini.getTime();
return Math.ceil(diff / (1000 * 60 * 60 * 24));
};
const reset = () => {
setEdit(null);
setAntibiotico('');
setFechaInicio(new Date().toISOString().split('T')[0]);
setFechaFinalizacion('');
};
const loadEdit = (a: ATB) => {
setEdit(a);
setAntibiotico(a.antibiotico);
setFechaInicio(a.fechaInicio);
setFechaFinalizacion(a.fechaFinalizacion || '');
};
const handleGuardar = () => {
if (!antibiotico.trim() || !fechaInicio) return;
if (edit) {
update(edit.id, { antibiotico, fechaInicio, fechaFinalizacion: fechaFinalizacion || undefined });
} else {
add({ pacienteId, internacionId, antibiotico, fechaInicio, fechaFinalizacion: fechaFinalizacion || undefined });
}
setDialog(false);
reset();
};
const atbOrdenado = atb.sort((a, b) => new Date(b.fechaInicio).getTime() - new Date(a.fechaInicio).getTime());
return (
<div className="space-y-4">
<div className="flex justify-between">
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />Nuevo
</Button>
</div>
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{edit ? 'Editar' : 'Nuevo'} Antibiótico</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Antibiótico</Label>
<Input value={antibiotico} onChange={e => setAntibiotico(e.target.value)} placeholder="Nombre del antibiótico" />
</div>
<div>
<Label>Fecha de Inicio</Label>
<Input type="date" value={fechaInicio} onChange={e => setFechaInicio(e.target.value)} />
</div>
<div>
<Label>Fecha de Finalización (opcional)</Label>
<Input type="date" value={fechaFinalizacion} onChange={e => setFechaFinalizacion(e.target.value)} />
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setDialog(false)}>
<X className="h-4 w-4 mr-2" />Cancelar
</Button>
<Button onClick={handleGuardar}>
<Save className="h-4 w-4 mr-2" />Guardar
</Button>
</div>
</DialogContent>
</Dialog>
{atbOrdenado.length === 0 ? (
<div className="text-center py-8 text-gray-500">No hay antibiotics registrados</div>
) : (
<div className="space-y-2">
{atbOrdenado.map(a => (
<Card key={a.id}>
<CardContent className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex-1">
<div className="font-medium">{a.antibiotico}</div>
<div className="text-sm text-gray-500 flex flex-wrap gap-2 mt-1">
<span>Inicio: {formatDateDDMMYYYY(a.fechaInicio)}</span>
{a.fechaFinalizacion && (
<span>Fin: {formatDateDDMMYYYY(a.fechaFinalizacion)}</span>
)}
<Badge className="bg-green-100 text-green-800">
{calcularDias(a.fechaInicio, a.fechaFinalizacion)} días
</Badge>
</div>
</div>
<div className="flex gap-1">
<Button size="sm" variant="outline" onClick={() => loadEdit(a)}>
<Pencil className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(a.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}