Files
administracionHospital/src/sections/HistoriaClinica.tsx
T

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