Agregar modal para importar laboratorios con parser de texto
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user