Agregar modal para importar laboratorios con parser de texto

This commit is contained in:
2026-04-15 20:15:16 -03:00
parent 4573e6e3d8
commit 8c4f103f82
+187
View File
@@ -565,6 +565,11 @@ function SeccionLaboratorios({ lab, patientId, add, update, del }: {
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 [importTexto, setImportTexto] = useState('');
const [importResultados, setImportResultados] = useState<ResultadoLaboratorio[]>([]);
const [importObservaciones, setImportObservaciones] = useState('');
const parametrosLaboratorio = [
'Hematocrito', 'Hemoglobina', 'Leucocitos', 'Plaquetas',
@@ -643,6 +648,114 @@ function SeccionLaboratorios({ lab, patientId, add, update, del }: {
setEdit(null);
};
const parseLaboratorioTexto = (texto: string): { resultados: ResultadoLaboratorio[]; observaciones: string } => {
const resultados: ResultadoLaboratorio[] = [];
const observacionesExtra: string[] = [];
const mapeoParametros: Record<string, { nombre: string; unidad: string; esPrincipal: boolean }> = {
'hematíes': { nombre: 'Hematíes', unidad: '10⁶/µl', esPrincipal: false },
'hematocrito': { nombre: 'Hematocrito', unidad: '%', esPrincipal: true },
'hemoglobina corpuscular media': { nombre: 'HCM', unidad: 'pg', esPrincipal: false },
'concentración de hemoglobina media': { nombre: 'CHCM', unidad: 'g/dL', esPrincipal: false },
'hemoglobina': { nombre: 'Hemoglobina', unidad: 'g/dL', esPrincipal: true },
'volumen corpuscular medio': { nombre: 'VCM', unidad: 'fL', esPrincipal: false },
'rdw': { nombre: 'RDW', unidad: '%', esPrincipal: false },
'eritroblastos': { nombre: 'Eritroblastos', unidad: '%', esPrincipal: false },
'neutrófilos': { nombre: 'Neutrófilos', unidad: '%', esPrincipal: false },
'linfocitos': { nombre: 'Linfocitos', unidad: '%', esPrincipal: false },
'monocitos': { nombre: 'Monocitos', unidad: '%', esPrincipal: false },
'eosinófilos': { nombre: 'Eosinófilos', unidad: '%', esPrincipal: false },
'basófilos': { nombre: 'Basófilos', unidad: '%', esPrincipal: false },
'leucocitos': { nombre: 'Leucocitos', unidad: '10³/µl', esPrincipal: true },
'recuento de leucocitos': { nombre: 'Leucocitos', unidad: '10³/µl', esPrincipal: true },
'recuento de plaquetas': { nombre: 'Plaquetas', unidad: '10³/µl', esPrincipal: true },
'plaquetas': { nombre: 'Plaquetas', unidad: '10³/µl', esPrincipal: true },
'volumen plaquetario medio': { nombre: 'VPM', unidad: 'fL', esPrincipal: false },
'procalcitonina': { nombre: 'Procalcitonina', unidad: 'ng/mL', esPrincipal: false },
'glucosa': { nombre: 'Glucemia', unidad: 'mg/dL', esPrincipal: true },
'urea': { nombre: 'Urea', unidad: 'mg/dL', esPrincipal: true },
'creatinina': { nombre: 'Creatinina', unidad: 'mg/dL', esPrincipal: true },
'mdrd': { nombre: 'Filtrado Glomerular (MDRD)', unidad: 'ml/min', esPrincipal: false },
'sodio': { nombre: 'Sodio', unidad: 'mEq/L', esPrincipal: true },
'potasio': { nombre: 'Potasio', unidad: 'mEq/L', esPrincipal: true },
'cloro': { nombre: 'Cloro', unidad: 'mEq/L', esPrincipal: true },
'bilirrubina total': { nombre: 'Bilirrubina Total', unidad: 'mg/dL', esPrincipal: true },
'bilirrubina directa': { nombre: 'Bilirrubina Directa', unidad: 'mg/dL', esPrincipal: true },
'got': { nombre: 'GOT', unidad: 'U/L', esPrincipal: true },
'gpt': { nombre: 'GPT', unidad: 'U/L', esPrincipal: true },
'fosfatasa alcalina': { nombre: 'Fosfatasa Alcalina', unidad: 'U/L', esPrincipal: false },
'proteínas totales': { nombre: 'Proteínas Totales', unidad: 'g/dL', esPrincipal: false },
'albúmina': { nombre: 'Albúmina', unidad: 'g/dL', esPrincipal: false },
'tiempo de protrombina': { nombre: 'Tiempo de Protrombina', unidad: '%', esPrincipal: true },
'rin': { nombre: 'INR', unidad: '', esPrincipal: true },
'aptt': { nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
'kptt': { nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
};
const lineas = texto.split('\n');
for (const linea of lineas) {
const lineaLower = linea.toLowerCase().trim();
for (const [clave, info] of Object.entries(mapeoParametros)) {
if (lineaLower.includes(clave)) {
const cleanLinea = linea.replace(/show_chart|list_alt/g, '').trim();
const partes = cleanLinea.split(/\s+/);
for (let i = 0; i < partes.length; i++) {
const parte = partes[i].replace(',', '.');
const valor = parseFloat(parte);
if (!isNaN(valor) && valor > 0 && valor < 1000) {
const esUnidadSiguiente = partes[i + 1] && /^(10[\^*]?[³6]?[\/]?µl|%|g\/dl|mg\/dl|mEq\/L|u\/l|fL|pg|seg|ng\/ml)?$/i.test(partes[i + 1]);
if (!esUnidadSiguiente) {
const nombreNormalizado = info.nombre === 'Plajetas' ? 'Plaquetas' : info.nombre;
if (info.esPrincipal) {
resultados.push({
parametro: nombreNormalizado,
valor: valor,
unidad: info.unidad,
estado: calcularEstadoLaboratorio(nombreNormalizado, parte)
});
} else if (!['Eritroblastos', 'Neutrófilos', 'Linfocitos', 'Monocitos', 'Eosinófilos', 'Basófilos'].includes(info.nombre)) {
observacionesExtra.push(`${info.nombre}: ${valor} ${info.unidad}`.trim());
}
break;
}
}
}
break;
}
}
}
return { resultados, observaciones: observacionesExtra.join('\n') };
};
const handleProcesarTexto = () => {
const { resultados, observaciones } = parseLaboratorioTexto(importTexto);
setImportResultados(resultados);
setImportObservaciones(observaciones);
};
const handleImportarLaboratorio = () => {
if (importResultados.length === 0 && !importObservaciones) return;
add({
pacienteId: patientId,
fecha: importFecha,
resultados: importResultados,
observaciones: importObservaciones
});
setImportDialog(false);
setImportTexto('');
setImportResultados([]);
setImportObservaciones('');
setImportFecha(new Date().toISOString().split('T')[0]);
};
const loadEdit = (l: Laboratorio) => {
setEdit(l);
setFecha(l.fecha);
@@ -686,6 +799,7 @@ function SeccionLaboratorios({ lab, patientId, add, update, del }: {
<div className="flex gap-2">
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}><Plus className="h-4 w-4 mr-2" />Nuevo Laboratorio</Button>
<Button variant="outline" onClick={() => { setSelectedLab(lab[0] || null); setParametroEvolucion('Hematocrito'); setEvolDialog(true); }}><TrendingUp className="h-4 w-4 mr-2" />Evolución</Button>
<Button variant="outline" onClick={() => { setImportFecha(new Date().toISOString().split('T')[0]); setImportTexto(''); setImportResultados([]); setImportObservaciones(''); setImportDialog(true); }}><FileText className="h-4 w-4 mr-2" />Importar</Button>
</div>
</div>
<Dialog open={dialog} onOpenChange={setDialog}>
@@ -819,6 +933,79 @@ function SeccionLaboratorios({ lab, patientId, add, update, del }: {
</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>
<Label>Fecha del Laboratorio</Label>
<Input type="date" value={importFecha} onChange={e => setImportFecha(e.target.value)} />
</div>
<div>
<Label>Pegar texto del resultado de laboratorio</Label>
<textarea
className="w-full p-2 border rounded-md text-sm min-h-[200px] font-mono"
placeholder="Pegue aquí el texto del resultado de laboratorio..."
value={importTexto}
onChange={e => setImportTexto(e.target.value)}
/>
</div>
<Button variant="outline" onClick={handleProcesarTexto} className="w-full">
<FileText className="h-4 w-4 mr-2" />
Procesar Texto
</Button>
{importResultados.length > 0 && (
<div className="border rounded-md max-h-48 overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Parámetro</TableHead>
<TableHead>Valor</TableHead>
<TableHead>Unidad</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{importResultados.map((r, idx) => (
<TableRow key={idx}>
<TableCell>{r.parametro}</TableCell>
<TableCell className={r.estado === 'Alto' ? 'text-amber-600' : r.estado === 'Bajo' ? 'text-blue-600' : r.estado === 'Crítico' ? 'text-red-600 font-bold' : 'text-green-600'}>{r.valor}</TableCell>
<TableCell>{r.unidad}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{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 && 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}>
<Save className="h-4 w-4 mr-2" />
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="overflow-x-auto">
<Table>