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 [kptt, setKptt] = useState('');
|
||||||
const [rin, setRin] = useState('');
|
const [rin, setRin] = useState('');
|
||||||
const [parametroEvolucion, setParametroEvolucion] = 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 = [
|
const parametrosLaboratorio = [
|
||||||
'Hematocrito', 'Hemoglobina', 'Leucocitos', 'Plaquetas',
|
'Hematocrito', 'Hemoglobina', 'Leucocitos', 'Plaquetas',
|
||||||
@@ -629,7 +634,7 @@ function SeccionLaboratorios({ lab, patientId, add, update, del }: {
|
|||||||
if (tp) res.push({ parametro: 'Tiempo de Protrombina', valor: tp, unidad: 'seg', estado: calcularEstadoLaboratorio('Tiempo de Protrombina', tp) });
|
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 (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) });
|
if (rin) res.push({ parametro: 'INR', valor: rin, unidad: '', estado: calcularEstadoLaboratorio('INR', rin) });
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
@@ -643,6 +648,114 @@ function SeccionLaboratorios({ lab, patientId, add, update, del }: {
|
|||||||
setEdit(null);
|
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) => {
|
const loadEdit = (l: Laboratorio) => {
|
||||||
setEdit(l);
|
setEdit(l);
|
||||||
setFecha(l.fecha);
|
setFecha(l.fecha);
|
||||||
@@ -686,6 +799,7 @@ function SeccionLaboratorios({ lab, patientId, add, update, del }: {
|
|||||||
<div className="flex gap-2">
|
<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={() => { 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={() => { 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>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={dialog} onOpenChange={setDialog}>
|
<Dialog open={dialog} onOpenChange={setDialog}>
|
||||||
@@ -819,6 +933,79 @@ function SeccionLaboratorios({ lab, patientId, add, update, del }: {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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> :
|
{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">
|
<div className="overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
Reference in New Issue
Block a user