Mejoras de diseño responsive en Sistema y Configuración, gestor de parser de laboratorio y tipos de cultivo
This commit is contained in:
@@ -13,6 +13,7 @@ import { initDb,
|
||||
getAllAcidosBase, createAcidoBase, updateAcidoBase, deleteAcidoBase,
|
||||
getAllCultivos, createCultivo, updateCultivo, deleteCultivo,
|
||||
getAllTiposCultivo, createTipoCultivo, updateTipoCultivo, deleteTipoCultivo, restablecerTiposCultivo,
|
||||
getAllGruposLaboratorio, createGrupoLaboratorio, updateGrupoLaboratorio, deleteGrupoLaboratorio, restablecerGruposLaboratorio,
|
||||
getAllEstudiosComplementarios, createEstudioComplementario, updateEstudioComplementario, deleteEstudioComplementario,
|
||||
getAllInterconsultas, createInterconsulta, updateInterconsulta, deleteInterconsulta,
|
||||
getAllAtb, createAtb, updateAtb, deleteAtb,
|
||||
@@ -97,6 +98,7 @@ app.get('/api/state', async (req, res) => {
|
||||
acidosBase: await getAllAcidosBase(),
|
||||
cultivos: await getAllCultivos(),
|
||||
tiposCultivo: await getAllTiposCultivo(),
|
||||
gruposDeterminacionesLab: await getAllGruposLaboratorio(),
|
||||
estudiosComplementarios: await getAllEstudiosComplementarios(),
|
||||
interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })),
|
||||
atb: await getAllAtb(),
|
||||
@@ -627,6 +629,51 @@ app.post('/api/tipos-cultivo/reset', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== GRUPOS DE DETERMINACIONES DE LABORATORIO ==========
|
||||
app.get('/api/grupos-laboratorio', async (req, res) => {
|
||||
try {
|
||||
res.json(await getAllGruposLaboratorio());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al obtener grupos de determinaciones de laboratorio' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/grupos-laboratorio', async (req, res) => {
|
||||
try {
|
||||
const nuevo = await createGrupoLaboratorio(req.body);
|
||||
res.json(nuevo);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message || 'Error al crear grupo de determinaciones' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/grupos-laboratorio/:id', async (req, res) => {
|
||||
try {
|
||||
const updated = await updateGrupoLaboratorio(req.params.id, req.body);
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message || 'Error al actualizar grupo de determinaciones' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/grupos-laboratorio/:id', async (req, res) => {
|
||||
try {
|
||||
await deleteGrupoLaboratorio(req.params.id);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al eliminar grupo de determinaciones' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/grupos-laboratorio/reset', async (req, res) => {
|
||||
try {
|
||||
const list = await restablecerGruposLaboratorio();
|
||||
res.json(list);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al restablecer grupos de determinaciones' });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ESTUDIOS COMPLEMENTARIOS ==========
|
||||
app.get('/api/estudios-complementarios', async (req, res) => {
|
||||
try {
|
||||
|
||||
+257
-5
@@ -67,7 +67,7 @@ const memStore = {
|
||||
movimientos_indicaciones: [],
|
||||
pendientes: [],
|
||||
tipos_cultivo: [],
|
||||
otrosLaboratorios: [],
|
||||
grupos_laboratorio: [],
|
||||
kv: {}
|
||||
};
|
||||
|
||||
@@ -859,6 +859,258 @@ export async function restablecerTiposCultivo() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== GRUPOS DE DETERMINACIONES DE LABORATORIO ==========
|
||||
export const DEFAULT_GRUPOS_LABORATORIO = [
|
||||
{
|
||||
id: 'grp-lipidos',
|
||||
nombreGrupo: 'Perfil Lipídico',
|
||||
descripcion: 'Determinaciones del metabolismo lipídico y riesgo aterogénico',
|
||||
activo: true,
|
||||
orden: 1,
|
||||
determinaciones: [
|
||||
{ id: 'lip-1', nombre: 'Colesterol Total', claves: ['colesterol total', 'colesterol', 'colest. total', 'col. total', 'colest total'], unidad: 'mg/dL', rangoReferencia: '< 200 mg/dL', esAdicional: true },
|
||||
{ id: 'lip-2', nombre: 'Colesterol LDL', claves: ['colesterol ldl', 'ldl-c', 'ldl', 'colest. ldl', 'colest ldl'], unidad: 'mg/dL', rangoReferencia: '< 100 mg/dL', esAdicional: true },
|
||||
{ id: 'lip-3', nombre: 'Colesterol No HDL', claves: ['colesterol no-hdl', 'colesterol no hdl', 'no-hdl', 'no hdl', 'no_hdl'], unidad: 'mg/dL', rangoReferencia: '< 130 mg/dL', esAdicional: true },
|
||||
{ id: 'lip-4', nombre: 'Colesterol HDL', claves: ['colesterol hdl', 'hdl-c', 'hdl', 'colest. hdl', 'colest hdl'], unidad: 'mg/dL', rangoReferencia: '> 40 mg/dL', esAdicional: true },
|
||||
{ id: 'lip-5', nombre: 'Triglicéridos', claves: ['triglicéridos', 'trigliceridos', 'tg', 'triglicérido', 'triglicerido'], unidad: 'mg/dL', rangoReferencia: '< 150 mg/dL', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-ferrico',
|
||||
nombreGrupo: 'Perfil Férrico',
|
||||
descripcion: 'Metabolismo del hierro, transferrina, ferritina y vitaminas hematopoyéticas',
|
||||
activo: true,
|
||||
orden: 2,
|
||||
determinaciones: [
|
||||
{ id: 'fer-1', nombre: 'Hierro', claves: ['hierro', 'sideremia', 'fe'], unidad: 'µg/dL', rangoReferencia: '60 - 170 µg/dL', esAdicional: true },
|
||||
{ id: 'fer-2', nombre: 'Transferrina', claves: ['transferrina', 'transferrin'], unidad: 'mg/dL', rangoReferencia: '200 - 360 mg/dL', esAdicional: true },
|
||||
{ id: 'fer-3', nombre: 'Porcentaje de Saturación de 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'], unidad: '%', rangoReferencia: '20 - 50 %', esAdicional: true },
|
||||
{ id: 'fer-4', nombre: 'Ferritina', claves: ['ferritina', 'ferritin'], unidad: 'ng/mL', rangoReferencia: '30 - 400 ng/mL', esAdicional: true },
|
||||
{ id: 'fer-5', nombre: 'Ácido Fólico', claves: ['acido folico', 'ácido fólico', 'folato', 'folatos', 'folico', 'fólico'], unidad: 'ng/mL', rangoReferencia: '3.0 - 17.0 ng/mL', esAdicional: true },
|
||||
{ id: 'fer-6', nombre: 'Vitamina B12', claves: ['vitamina b12', 'b12', 'vit. b12'], unidad: 'pg/mL', rangoReferencia: '200 - 900 pg/mL', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-fosfocalcico',
|
||||
nombreGrupo: 'Metabolismo Fosfocálcico y Medio Interno Extra',
|
||||
descripcion: 'Calcio total, calcio iónico, fósforo y magnesio sérico',
|
||||
activo: true,
|
||||
orden: 3,
|
||||
determinaciones: [
|
||||
{ id: 'fcal-1', nombre: 'Calcio Total', claves: ['calcio total', 'calcio', 'ca total', 'ca+', 'ca++'], unidad: 'mg/dL', rangoReferencia: '8.5 - 10.5 mg/dL', esAdicional: true },
|
||||
{ id: 'fcal-2', nombre: 'Calcio Iónico', claves: ['calcio ionico', 'calcio iónico', 'ca ionico', 'ca iónico', 'ca++ ionico', 'ca++ iónico', 'calcio_ionico'], unidad: 'mmol/L', rangoReferencia: '1.15 - 1.33 mmol/L', esAdicional: true },
|
||||
{ id: 'fcal-3', nombre: 'Fósforo', claves: ['fósforo', 'fosforo', 'fosfemia'], unidad: 'mg/dL', rangoReferencia: '2.5 - 4.5 mg/dL', esAdicional: true },
|
||||
{ id: 'fcal-4', nombre: 'Magnesio', claves: ['magnesio', 'magnesemia', 'mg++', 'mg+', 'mg2+', 'mg 2+', 'mg.', 'magnesio plasmatico', 'magnesio plasmático', 'magnesio serico', 'magnesio sérico', 'magnesio en sangre', 'mg serico', 'mg sérico', 'mg plasmatico', 'mg plasmático', 'mg'], unidad: 'mg/dL', rangoReferencia: '1.7 - 2.4 mg/dL', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-enzimas-inflamacion',
|
||||
nombreGrupo: 'Enzimas, Proteínas e Inflamación',
|
||||
descripcion: 'Albúmina, FAL, LDH, Procalcitonina, PCR, eritrosedimentación y enzimas',
|
||||
activo: true,
|
||||
orden: 4,
|
||||
determinaciones: [
|
||||
{ id: 'enz-1', nombre: 'Albúmina', claves: ['albúmina', 'albumina'], unidad: 'g/dL', rangoReferencia: '3.5 - 5.0 g/dL', esAdicional: true },
|
||||
{ id: 'enz-2', nombre: 'Fosfatasa Alcalina', claves: ['fosfatasa alcalina', 'fal'], unidad: 'U/L', rangoReferencia: '40 - 130 U/L', esAdicional: true },
|
||||
{ id: 'enz-3', nombre: 'LDH', claves: ['ldh', 'lactato deshidrogenasa', 'lactatodeshidrogenasa'], unidad: 'U/L', rangoReferencia: '135 - 225 U/L', esAdicional: true },
|
||||
{ id: 'enz-4', nombre: 'Procalcitonina', claves: ['procalcitonina', 'pct'], unidad: 'ng/mL', rangoReferencia: '< 0.5 ng/mL', esAdicional: true },
|
||||
{ id: 'enz-5', nombre: 'Proteína C Reactiva', claves: ['proteina c reactiva', 'proteína c reactiva', 'pcr cuantitativa', 'pcr ultrasensible', 'pcr'], unidad: 'mg/L', rangoReferencia: '< 5 mg/L', esAdicional: true },
|
||||
{ id: 'enz-6', nombre: 'Eritrosedimentación', claves: ['eritrosedimentacion', 'eritrosedimentación', 'vsg', 'esr', 'eritro'], unidad: 'mm/h', rangoReferencia: '< 20 mm/h', esAdicional: true },
|
||||
{ id: 'enz-7', nombre: 'CPK', claves: ['cpk', 'creatinfosfoquinasa', 'creatin fosfoquinasa', 'ck total', 'ck'], unidad: 'U/L', rangoReferencia: '20 - 200 U/L', esAdicional: true },
|
||||
{ id: 'enz-8', nombre: 'Amilasa', claves: ['amilasa', 'amilasemia'], unidad: 'U/L', rangoReferencia: '28 - 100 U/L', esAdicional: true },
|
||||
{ id: 'enz-9', nombre: 'Lipasa', claves: ['lipasa', 'lipasemia'], unidad: 'U/L', rangoReferencia: '13 - 60 U/L', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-tiroideo',
|
||||
nombreGrupo: 'Perfil Tiroideo',
|
||||
descripcion: 'Hormonas tiroideas e hipofisarias (TSH, T4L, T4, T3)',
|
||||
activo: true,
|
||||
orden: 5,
|
||||
determinaciones: [
|
||||
{ id: 'tir-1', nombre: 'TSH', claves: ['tsh', 'tirotrofina', 'tirotropina', 'tsh ultrasensible'], unidad: 'uUI/mL', rangoReferencia: '0.4 - 4.0 uUI/mL', esAdicional: true },
|
||||
{ id: 'tir-2', nombre: 'T4 Libre', claves: ['t4 libre', 't4l', 't4-l', 'tiroxina libre'], unidad: 'ng/dL', rangoReferencia: '0.8 - 1.8 ng/dL', esAdicional: true },
|
||||
{ id: 'tir-3', nombre: 'T4 Total', claves: ['t4 total', 't4', 'tiroxina'], unidad: 'µg/dL', rangoReferencia: '4.5 - 12.0 µg/dL', esAdicional: true },
|
||||
{ id: 'tir-4', nombre: 'T3 Total', claves: ['t3 total', 't3', 'triyodotironina'], unidad: 'ng/dL', rangoReferencia: '80 - 200 ng/dL', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-cardiacos',
|
||||
nombreGrupo: 'Biomarcadores Cardíacos',
|
||||
descripcion: 'Péptidos natriuréticos, troponinas y marcadores de isquemia/falla',
|
||||
activo: true,
|
||||
orden: 6,
|
||||
determinaciones: [
|
||||
{ id: 'car-1', nombre: 'NT-proBNP', claves: ['nt-probnp', 'nt probnp', 'nt_probnp', 'probnp', 'pro-bnp', 'pro bnp'], unidad: 'pg/mL', rangoReferencia: '< 125 pg/mL', esAdicional: true },
|
||||
{ id: 'car-2', nombre: 'Troponina T / I', claves: ['troponina t', 'troponina i', 'troponina ultrasensible', 'troponina', 'tn-t', 'tn-i', 'tnt', 'tni'], unidad: 'ng/mL', rangoReferencia: '< 0.014 ng/mL', esAdicional: true },
|
||||
{ id: 'car-3', nombre: 'CK-MB', claves: ['ck-mb', 'ckmb', 'ck mb'], unidad: 'U/L', rangoReferencia: '< 25 U/L', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-hemograma-indices',
|
||||
nombreGrupo: 'Hemograma - Índices y Fórmula',
|
||||
descripcion: 'Constantes corpusculares e índices hematimétricos adicionales',
|
||||
activo: true,
|
||||
orden: 7,
|
||||
determinaciones: [
|
||||
{ id: 'hem-1', nombre: 'VCM', claves: ['volumen corpuscular medio', 'vcm'], unidad: 'fL', rangoReferencia: '80 - 100 fL', esAdicional: true },
|
||||
{ id: 'hem-2', nombre: 'HCM', claves: ['hemoglobina corpuscular media', 'hcm'], unidad: 'pg', rangoReferencia: '27 - 33 pg', esAdicional: true },
|
||||
{ id: 'hem-3', nombre: 'CHCM', claves: ['concentracion de hemoglobina corpuscular media', 'chcm'], unidad: 'g/dL', rangoReferencia: '32 - 36 g/dL', esAdicional: true },
|
||||
{ id: 'hem-4', nombre: 'RDW', claves: ['rdw', 'ide', 'ancho de distribucion eritrocitaria'], unidad: '%', rangoReferencia: '11.5 - 14.5 %', esAdicional: true },
|
||||
{ id: 'hem-5', nombre: 'Neutrófilos', claves: ['neutrófilos', 'neutrofilos', 'neutrofilos segmentados', 'segmentados'], unidad: '%', rangoReferencia: '45 - 70 %', esAdicional: true },
|
||||
{ id: 'hem-6', nombre: 'Linfocitos', claves: ['linfocitos', 'linfo'], unidad: '%', rangoReferencia: '20 - 45 %', esAdicional: true },
|
||||
{ id: 'hem-7', nombre: 'Monocitos', claves: ['monocitos', 'mono'], unidad: '%', rangoReferencia: '2 - 10 %', esAdicional: true },
|
||||
{ id: 'hem-8', nombre: 'Eosinófilos', claves: ['eosinófilos', 'eosinofilos', 'eosino'], unidad: '%', rangoReferencia: '1 - 4 %', esAdicional: true },
|
||||
{ id: 'hem-9', nombre: 'Basófilos', claves: ['basófilos', 'basofilos'], unidad: '%', rangoReferencia: '0 - 1 %', esAdicional: true },
|
||||
{ id: 'hem-10', nombre: 'Eritroblastos', claves: ['eritroblastos'], unidad: '%', rangoReferencia: '0 %', esAdicional: true },
|
||||
{ id: 'hem-11', nombre: 'VPM', claves: ['volumen plaquetario medio', 'vpm'], unidad: 'fL', rangoReferencia: '7.5 - 11.5 fL', esAdicional: true }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export async function getAllGruposLaboratorio() {
|
||||
let list = [];
|
||||
if (db) {
|
||||
const raw = await db.collection('grupos_laboratorio').find().sort({ orden: 1, nombreGrupo: 1 }).toArray();
|
||||
list = cleanDocs(raw);
|
||||
if (list.length === 0) {
|
||||
for (const item of DEFAULT_GRUPOS_LABORATORIO) {
|
||||
await db.collection('grupos_laboratorio').insertOne({ ...item });
|
||||
}
|
||||
list = [...DEFAULT_GRUPOS_LABORATORIO];
|
||||
}
|
||||
return list;
|
||||
} else {
|
||||
if (!memStore.grupos_laboratorio || memStore.grupos_laboratorio.length === 0) {
|
||||
memStore.grupos_laboratorio = DEFAULT_GRUPOS_LABORATORIO.map(item => JSON.parse(JSON.stringify(item)));
|
||||
}
|
||||
return [...memStore.grupos_laboratorio];
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGrupoLaboratorio(grupo) {
|
||||
const doc = {
|
||||
id: grupo.id || generateUUID(),
|
||||
nombreGrupo: (grupo.nombreGrupo || '').trim(),
|
||||
descripcion: (grupo.descripcion || '').trim(),
|
||||
activo: grupo.activo !== false,
|
||||
orden: typeof grupo.orden === 'number' ? grupo.orden : 99,
|
||||
determinaciones: Array.isArray(grupo.determinaciones) ? grupo.determinaciones.map(d => ({
|
||||
id: d.id || generateUUID(),
|
||||
nombre: (d.nombre || '').trim(),
|
||||
claves: Array.isArray(d.claves) ? d.claves.map(c => (c || '').trim().toLowerCase()).filter(Boolean) : [],
|
||||
unidad: (d.unidad || '').trim(),
|
||||
esPrincipal: Boolean(d.esPrincipal),
|
||||
esAdicional: d.esAdicional !== false,
|
||||
rangoReferencia: (d.rangoReferencia || '').trim(),
|
||||
descripcion: (d.descripcion || '').trim()
|
||||
})) : [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (!doc.nombreGrupo) {
|
||||
throw new Error('El nombre del grupo de laboratorio es requerido');
|
||||
}
|
||||
|
||||
if (db) {
|
||||
const existing = await db.collection('grupos_laboratorio').findOne({ nombreGrupo: doc.nombreGrupo });
|
||||
if (existing) {
|
||||
throw new Error(`Ya existe un grupo de determinaciones con el nombre "${doc.nombreGrupo}"`);
|
||||
}
|
||||
await db.collection('grupos_laboratorio').insertOne(doc);
|
||||
} else {
|
||||
if (!memStore.grupos_laboratorio) memStore.grupos_laboratorio = [];
|
||||
const exists = memStore.grupos_laboratorio.some(g => g.nombreGrupo.toLowerCase() === doc.nombreGrupo.toLowerCase());
|
||||
if (exists) {
|
||||
throw new Error(`Ya existe un grupo de determinaciones con el nombre "${doc.nombreGrupo}"`);
|
||||
}
|
||||
memStore.grupos_laboratorio.push(doc);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function updateGrupoLaboratorio(id, datos) {
|
||||
const updateDoc = { ...datos, updatedAt: new Date().toISOString() };
|
||||
delete updateDoc._id;
|
||||
delete updateDoc.id;
|
||||
|
||||
if (datos.nombreGrupo) {
|
||||
updateDoc.nombreGrupo = datos.nombreGrupo.trim();
|
||||
}
|
||||
if (datos.descripcion !== undefined) {
|
||||
updateDoc.descripcion = datos.descripcion.trim();
|
||||
}
|
||||
if (datos.activo !== undefined) {
|
||||
updateDoc.activo = Boolean(datos.activo);
|
||||
}
|
||||
if (datos.orden !== undefined) {
|
||||
updateDoc.orden = Number(datos.orden);
|
||||
}
|
||||
if (Array.isArray(datos.determinaciones)) {
|
||||
updateDoc.determinaciones = datos.determinaciones.map(d => ({
|
||||
id: d.id || generateUUID(),
|
||||
nombre: (d.nombre || '').trim(),
|
||||
claves: Array.isArray(d.claves) ? d.claves.map(c => (c || '').trim().toLowerCase()).filter(Boolean) : [],
|
||||
unidad: (d.unidad || '').trim(),
|
||||
esPrincipal: Boolean(d.esPrincipal),
|
||||
esAdicional: d.esAdicional !== false,
|
||||
rangoReferencia: (d.rangoReferencia || '').trim(),
|
||||
descripcion: (d.descripcion || '').trim()
|
||||
}));
|
||||
}
|
||||
|
||||
if (db) {
|
||||
if (updateDoc.nombreGrupo) {
|
||||
const existing = await db.collection('grupos_laboratorio').findOne({
|
||||
id: { $ne: id },
|
||||
nombreGrupo: updateDoc.nombreGrupo
|
||||
});
|
||||
if (existing) {
|
||||
throw new Error(`Ya existe otro grupo de determinaciones con el nombre "${updateDoc.nombreGrupo}"`);
|
||||
}
|
||||
}
|
||||
await db.collection('grupos_laboratorio').updateOne({ id }, { $set: updateDoc });
|
||||
} else {
|
||||
if (!memStore.grupos_laboratorio) memStore.grupos_laboratorio = [];
|
||||
if (updateDoc.nombreGrupo) {
|
||||
const exists = memStore.grupos_laboratorio.some(g => g.id !== id && g.nombreGrupo.toLowerCase() === updateDoc.nombreGrupo.toLowerCase());
|
||||
if (exists) {
|
||||
throw new Error(`Ya existe otro grupo de determinaciones con el nombre "${updateDoc.nombreGrupo}"`);
|
||||
}
|
||||
}
|
||||
const idx = memStore.grupos_laboratorio.findIndex(g => g.id === id);
|
||||
if (idx !== -1) {
|
||||
memStore.grupos_laboratorio[idx] = { ...memStore.grupos_laboratorio[idx], ...updateDoc };
|
||||
}
|
||||
}
|
||||
return { id, ...updateDoc };
|
||||
}
|
||||
|
||||
export async function deleteGrupoLaboratorio(id) {
|
||||
if (db) {
|
||||
await db.collection('grupos_laboratorio').deleteOne({ id });
|
||||
} else {
|
||||
if (memStore.grupos_laboratorio) {
|
||||
memStore.grupos_laboratorio = memStore.grupos_laboratorio.filter(g => g.id !== id);
|
||||
}
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function restablecerGruposLaboratorio() {
|
||||
if (db) {
|
||||
await db.collection('grupos_laboratorio').deleteMany({});
|
||||
for (const item of DEFAULT_GRUPOS_LABORATORIO) {
|
||||
await db.collection('grupos_laboratorio').insertOne({ ...item });
|
||||
}
|
||||
return await getAllGruposLaboratorio();
|
||||
} else {
|
||||
memStore.grupos_laboratorio = DEFAULT_GRUPOS_LABORATORIO.map(item => JSON.parse(JSON.stringify(item)));
|
||||
return [...memStore.grupos_laboratorio];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== ESTUDIOS COMPLEMENTARIOS ==========
|
||||
export async function getAllEstudiosComplementarios() {
|
||||
if (db) {
|
||||
@@ -1158,8 +1410,8 @@ export function getDb() {
|
||||
export async function exportAllData() {
|
||||
const collections = [
|
||||
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
||||
'evoluciones', 'laboratorios', 'glucemias', 'acidosbase',
|
||||
'cultivos', 'tipos_cultivo', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'evoluciones', 'laboratorios', 'otrosLaboratorios', 'glucemias', 'acidosbase',
|
||||
'cultivos', 'tipos_cultivo', 'grupos_laboratorio', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
||||
];
|
||||
const dump = {};
|
||||
@@ -1179,8 +1431,8 @@ export async function exportAllData() {
|
||||
export async function importAllData(dump) {
|
||||
const collections = [
|
||||
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
||||
'evoluciones', 'laboratorios', 'glucemias', 'acidosbase',
|
||||
'cultivos', 'tipos_cultivo', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'evoluciones', 'laboratorios', 'otrosLaboratorios', 'glucemias', 'acidosbase',
|
||||
'cultivos', 'tipos_cultivo', 'grupos_laboratorio', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
||||
];
|
||||
if (db) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ import type {
|
||||
AcidoBase,
|
||||
Cultivo,
|
||||
TipoCultivo,
|
||||
GrupoDeterminacionLaboratorio,
|
||||
EstudioComplementario,
|
||||
Interconsulta,
|
||||
ATB,
|
||||
@@ -47,6 +48,7 @@ interface HospitalState {
|
||||
acidosBase: AcidoBase[];
|
||||
cultivos: Cultivo[];
|
||||
tiposCultivo: TipoCultivo[];
|
||||
gruposDeterminacionesLab: GrupoDeterminacionLaboratorio[];
|
||||
estudiosComplementarios: EstudioComplementario[];
|
||||
interconsultas: Interconsulta[];
|
||||
atb: ATB[];
|
||||
@@ -74,6 +76,7 @@ const defaultState = (): HospitalState => ({
|
||||
acidosBase: [],
|
||||
cultivos: [],
|
||||
tiposCultivo: [],
|
||||
gruposDeterminacionesLab: [],
|
||||
estudiosComplementarios: [],
|
||||
interconsultas: [],
|
||||
atb: [],
|
||||
@@ -108,6 +111,7 @@ export function useHospitalStore() {
|
||||
...defaults,
|
||||
...body,
|
||||
tiposCultivo: body.tiposCultivo || [],
|
||||
gruposDeterminacionesLab: body.gruposDeterminacionesLab || [],
|
||||
estudiosComplementarios: body.estudiosComplementarios || [],
|
||||
interconsultas: body.interconsultas || [],
|
||||
atb: body.atb || [],
|
||||
@@ -1065,6 +1069,75 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
// Acciones de grupos de determinaciones de laboratorio (Entrenamiento del Parser)
|
||||
const agregarGrupoDeterminacionLab = useCallback(async (grupo: Omit<GrupoDeterminacionLaboratorio, 'id'>) => {
|
||||
const nuevoGrupo: GrupoDeterminacionLaboratorio = {
|
||||
...grupo,
|
||||
id: generateUUID(),
|
||||
};
|
||||
try {
|
||||
const res = await apiCall('POST', '/grupos-laboratorio', nuevoGrupo);
|
||||
const saved = res || nuevoGrupo;
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
gruposDeterminacionesLab: [...(prev.gruposDeterminacionesLab || []).filter(g => g.id !== saved.id), saved],
|
||||
}));
|
||||
toast.success(`Grupo de laboratorio "${saved.nombreGrupo}" guardado exitosamente`);
|
||||
return saved;
|
||||
} catch (err) {
|
||||
console.error('Error al agregar grupo de determinaciones:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Error al agregar grupo de determinaciones');
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const actualizarGrupoDeterminacionLab = useCallback(async (id: string, datos: Partial<GrupoDeterminacionLaboratorio>) => {
|
||||
try {
|
||||
const res = await apiCall('PUT', `/grupos-laboratorio/${id}`, datos);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
gruposDeterminacionesLab: (prev.gruposDeterminacionesLab || []).map(g => g.id === id ? { ...g, ...datos, ...(res || {}) } : g),
|
||||
}));
|
||||
toast.success('Grupo de determinaciones actualizado exitosamente');
|
||||
} catch (err) {
|
||||
console.error('Error al actualizar grupo de determinaciones:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Error al actualizar grupo de determinaciones');
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const eliminarGrupoDeterminacionLab = useCallback(async (id: string) => {
|
||||
try {
|
||||
await apiCall('DELETE', `/grupos-laboratorio/${id}`);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
gruposDeterminacionesLab: (prev.gruposDeterminacionesLab || []).filter(g => g.id !== id),
|
||||
}));
|
||||
toast.success('Grupo de determinaciones eliminado');
|
||||
} catch (err) {
|
||||
console.error('Error al eliminar grupo de determinaciones:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Error al eliminar grupo de determinaciones');
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const restablecerGruposDeterminacionesLab = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiCall('POST', '/grupos-laboratorio/reset');
|
||||
if (Array.isArray(res)) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
gruposDeterminacionesLab: res,
|
||||
}));
|
||||
}
|
||||
toast.success('Grupos de determinaciones de laboratorio restablecidos a valores estándar');
|
||||
} catch (err) {
|
||||
console.error('Error al restablecer grupos de determinaciones:', err);
|
||||
toast.error('Error al restablecer grupos de determinaciones');
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
// Acciones de estudios complementarios
|
||||
const agregarEstudioComplementario = useCallback(async (estudio: Omit<EstudioComplementario, 'id'>) => {
|
||||
const internacion = state.internaciones.find(i => i.id === estudio.internacionId);
|
||||
@@ -1658,6 +1731,11 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
actualizarTipoCultivo,
|
||||
eliminarTipoCultivo,
|
||||
restablecerTiposCultivo,
|
||||
gruposDeterminacionesLab: state.gruposDeterminacionesLab,
|
||||
agregarGrupoDeterminacionLab,
|
||||
actualizarGrupoDeterminacionLab,
|
||||
eliminarGrupoDeterminacionLab,
|
||||
restablecerGruposDeterminacionesLab,
|
||||
agregarEstudioComplementario,
|
||||
actualizarEstudioComplementario,
|
||||
eliminarEstudioComplementario,
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
import type { ResultadoLaboratorio, AcidoBase, GrupoDeterminacionLaboratorio } from '@/types';
|
||||
|
||||
export const RANGOS_LABORATORIO_DEFAULT: Record<string, { min: number; max: number }> = {
|
||||
'Hematocrito': { min: 36, max: 50 },
|
||||
'Hemoglobina': { min: 12, max: 16.5 },
|
||||
'Leucocitos': { min: 4000, max: 10000 },
|
||||
'Plaquetas': { min: 150000, max: 400000 },
|
||||
'Glucemia': { min: 70, max: 110 },
|
||||
'Urea': { min: 15, max: 45 },
|
||||
'Creatinina': { min: 0.6, max: 1.2 },
|
||||
'Sodio': { min: 135, max: 145 },
|
||||
'Potasio': { min: 3.5, max: 5.0 },
|
||||
'Cloro': { min: 96, max: 106 },
|
||||
'Bilirrubina Total': { min: 0.2, max: 1.2 },
|
||||
'Bilirrubina Directa': { min: 0.0, max: 0.3 },
|
||||
'GOT': { min: 0, max: 40 },
|
||||
'GPT': { min: 0, max: 40 },
|
||||
'Tiempo de Protrombina': { min: 70, max: 100 },
|
||||
'KPTT': { min: 25, max: 38 },
|
||||
'INR': { min: 0.8, max: 1.2 },
|
||||
'Colesterol Total': { min: 0, max: 200 },
|
||||
'Colesterol LDL': { min: 0, max: 100 },
|
||||
'Colesterol No HDL': { min: 0, max: 130 },
|
||||
'Colesterol HDL': { min: 40, max: 100 },
|
||||
'Triglicéridos': { min: 0, max: 150 },
|
||||
'Hierro': { min: 60, max: 170 },
|
||||
'Transferrina': { min: 200, max: 360 },
|
||||
'Porcentaje de Saturación de Transferrina': { min: 20, max: 50 },
|
||||
'Ferritina': { min: 30, max: 400 },
|
||||
'Ácido Fólico': { min: 3.0, max: 17.0 },
|
||||
'Vitamina B12': { min: 200, max: 900 },
|
||||
'Albúmina': { min: 3.5, max: 5.0 },
|
||||
'Calcio Total': { min: 8.5, max: 10.5 },
|
||||
'Fosfatasa Alcalina': { min: 40, max: 130 },
|
||||
'LDH': { min: 135, max: 225 },
|
||||
'NT-proBNP': { min: 0, max: 125 },
|
||||
'Procalcitonina': { min: 0, max: 0.5 },
|
||||
'Proteína C Reactiva': { min: 0, max: 5 },
|
||||
'Eritrosedimentación': { min: 0, max: 20 },
|
||||
'Fósforo': { min: 2.5, max: 4.5 },
|
||||
'Magnesio': { min: 1.6, max: 2.6 },
|
||||
'Calcio Iónico': { min: 1.12, max: 1.32 },
|
||||
'TSH': { min: 0.4, max: 4.0 },
|
||||
'T4 Libre': { min: 0.8, max: 1.8 },
|
||||
'T4 Total': { min: 4.5, max: 12.0 },
|
||||
'T3 Total': { min: 80, max: 200 },
|
||||
'Troponina T / I': { min: 0, max: 0.014 },
|
||||
'CK-MB': { min: 0, max: 25 },
|
||||
'CPK': { min: 20, max: 200 },
|
||||
'Amilasa': { min: 28, max: 100 },
|
||||
'Lipasa': { min: 13, max: 60 },
|
||||
'VCM': { min: 80, max: 100 },
|
||||
'HCM': { min: 27, max: 33 },
|
||||
'CHCM': { min: 32, max: 36 },
|
||||
'RDW': { min: 11.5, max: 14.5 },
|
||||
};
|
||||
|
||||
export function calcularEstadoLaboratorioExtendido(parametro: string, valor: string | number, rangoCustom?: string): 'Normal' | 'Alto' | 'Bajo' | 'Crítico' {
|
||||
const num = typeof valor === 'number' ? valor : parseFloat(valor);
|
||||
if (isNaN(num)) return 'Normal';
|
||||
|
||||
// Check custom range format like "0.4 - 4.0" or "< 200" or "> 40"
|
||||
if (rangoCustom) {
|
||||
const rangeMatch = rangoCustom.match(/(\d+(?:\.\d+)?)\s*[-–—]\s*(\d+(?:\.\d+)?)/);
|
||||
if (rangeMatch) {
|
||||
const min = parseFloat(rangeMatch[1]);
|
||||
const max = parseFloat(rangeMatch[2]);
|
||||
if (!isNaN(min) && !isNaN(max)) {
|
||||
if (num < min) return 'Bajo';
|
||||
if (num > max) return 'Alto';
|
||||
return 'Normal';
|
||||
}
|
||||
}
|
||||
const lessMatch = rangoCustom.match(/<\s*(\d+(?:\.\d+)?)/);
|
||||
if (lessMatch) {
|
||||
const max = parseFloat(lessMatch[1]);
|
||||
if (!isNaN(max) && num > max) return 'Alto';
|
||||
return 'Normal';
|
||||
}
|
||||
const greaterMatch = rangoCustom.match(/>\s*(\d+(?:\.\d+)?)/);
|
||||
if (greaterMatch) {
|
||||
const min = parseFloat(greaterMatch[1]);
|
||||
if (!isNaN(min) && num < min) return 'Bajo';
|
||||
return 'Normal';
|
||||
}
|
||||
}
|
||||
|
||||
const rango = RANGOS_LABORATORIO_DEFAULT[parametro];
|
||||
if (!rango) 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';
|
||||
}
|
||||
|
||||
export interface RecognizedDetermination {
|
||||
grupoId: string;
|
||||
grupoNombre: string;
|
||||
parametro: string;
|
||||
valor: number;
|
||||
unidad: string;
|
||||
estado: 'Normal' | 'Alto' | 'Bajo' | 'Crítico';
|
||||
claveCoincidente: string;
|
||||
rangoReferencia?: string;
|
||||
lineaOriginal: string;
|
||||
}
|
||||
|
||||
export interface PotentialUnknownDetermination {
|
||||
linea: string;
|
||||
posibleNombre: string;
|
||||
posibleValor: number;
|
||||
posibleUnidad: string;
|
||||
}
|
||||
|
||||
export interface ParseResult {
|
||||
resultados: ResultadoLaboratorio[];
|
||||
observaciones: string;
|
||||
observacionesPorGrupo: { grupo: string; lineas: string[] }[];
|
||||
reconocidos: RecognizedDetermination[];
|
||||
desconocidosPotenciales: PotentialUnknownDetermination[];
|
||||
acidoBase: Omit<AcidoBase, 'id'> | null;
|
||||
}
|
||||
|
||||
export const CORE_PARAMETERS_MAPPING = [
|
||||
{ claves: ['hematocrito', 'hto'], nombre: 'Hematocrito', unidad: '%', esPrincipal: true },
|
||||
{ claves: ['hemoglobina', 'hb'], nombre: 'Hemoglobina', unidad: 'g/dL', esPrincipal: true },
|
||||
{ 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: ['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: ['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 },
|
||||
];
|
||||
|
||||
export function esParametroCore(param: string): boolean {
|
||||
const coreList = [
|
||||
'hematocrito', 'hto', 'hemoglobina', 'hb', 'leucocitos', 'gb', 'plaquetas', 'plaq',
|
||||
'glucemia', 'glucosa', 'urea', 'creatinina', 'creat', 'sodio', 'na', 'potasio', 'k', 'k+',
|
||||
'cloro', 'cl', 'cl-', 'bilirrubina total', 'bt', 'bilirrubina directa', 'bd', 'got', 'ast', 'gpt', 'alt',
|
||||
'tiempo de protrombina', 'tp', 't.p.', 't.p', 'rin', 'inr', 'aptt', 'kptt', 'proteínas totales', 'proteinas totales'
|
||||
];
|
||||
return coreList.includes(param.toLowerCase().trim());
|
||||
}
|
||||
|
||||
export function matchClaveTexto(lineaLower: string, clave: string): boolean {
|
||||
const c = clave.toLowerCase().trim();
|
||||
if (!c) return false;
|
||||
if (c === 'mg') {
|
||||
const execMatch = /(?:^|[^a-z0-9_])mg(?=[:\s=+\d]|$)(?!\/(?:dl|l|24h|ml)|%)/i.exec(lineaLower);
|
||||
if (!execMatch) return false;
|
||||
const prefix = lineaLower.substring(0, execMatch.index);
|
||||
return !/\d+\s*$/.test(prefix);
|
||||
}
|
||||
if (c.length > 4) {
|
||||
return lineaLower.includes(c);
|
||||
}
|
||||
const escaped = c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(`(?:^|[^a-z0-9_])${escaped}(?:$|[^a-z0-9_])`, 'i');
|
||||
return regex.test(lineaLower);
|
||||
}
|
||||
|
||||
export function parseAcidoBaseTexto(texto: string, fecha?: string, pacienteId?: 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;
|
||||
|
||||
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') || cleanLinea.includes('gases en sangre')) {
|
||||
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: pacienteId || '',
|
||||
fecha: fecha || new Date().toISOString().split('T')[0],
|
||||
hora: '',
|
||||
ph,
|
||||
pco2: pco2 || 40,
|
||||
po2: po2 || 85,
|
||||
hco3: hco3 || 24,
|
||||
be: be || 0,
|
||||
sato2: sato2 || 97,
|
||||
lactato,
|
||||
fio2,
|
||||
interpretacion: ''
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseLaboratorioTextoCompleto(
|
||||
texto: string,
|
||||
gruposConfigurados: GrupoDeterminacionLaboratorio[] = [],
|
||||
fecha?: string,
|
||||
pacienteId?: string
|
||||
): ParseResult {
|
||||
const resultados: ResultadoLaboratorio[] = [];
|
||||
const reconocidos: RecognizedDetermination[] = [];
|
||||
const processedLines = new Set<number>();
|
||||
const lineas = texto.split('\n');
|
||||
|
||||
// 1. Process Core Parameters
|
||||
for (let lIdx = 0; lIdx < lineas.length; lIdx++) {
|
||||
const linea = lineas[lIdx];
|
||||
const lineaLower = linea.toLowerCase().trim();
|
||||
if (!lineaLower) continue;
|
||||
|
||||
for (const core of CORE_PARAMETERS_MAPPING) {
|
||||
if (resultados.some(r => r.parametro === core.nombre)) continue;
|
||||
|
||||
let matchedClave = false;
|
||||
let matchedClaveString = '';
|
||||
|
||||
for (const clave of core.claves) {
|
||||
if (matchClaveTexto(lineaLower, clave)) {
|
||||
matchedClave = true;
|
||||
matchedClaveString = clave;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedClave) {
|
||||
let subLinea = linea;
|
||||
const idxClave = lineaLower.indexOf(matchedClaveString);
|
||||
if (idxClave !== -1) {
|
||||
subLinea = linea.substring(idxClave + matchedClaveString.length);
|
||||
}
|
||||
|
||||
const cleanSubLinea = subLinea.replace(/\([^)]*?(?:-|–|—|:|[0-9]\s*[-–—]\s*[0-9]|ref|vr|val|norm|min|max)[^)]*?\)/gi, ' ');
|
||||
let match = cleanSubLinea.match(/(\d+(?:[.,]\d+)?)/);
|
||||
if (!match) 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) {
|
||||
let valorFinal = valor;
|
||||
if (core.nombre === 'Leucocitos' || core.nombre === 'Plaquetas') {
|
||||
if (valor < 200) valorFinal = valor * 1000;
|
||||
}
|
||||
|
||||
let unidadFinal = core.unidad;
|
||||
if (core.nombre === 'Tiempo de Protrombina') {
|
||||
if (lineaLower.includes('seg') || lineaLower.includes('segundo')) {
|
||||
unidadFinal = 'seg';
|
||||
} else if (lineaLower.includes('%')) {
|
||||
unidadFinal = '%';
|
||||
}
|
||||
}
|
||||
|
||||
resultados.push({
|
||||
parametro: core.nombre,
|
||||
valor: valorFinal,
|
||||
unidad: unidadFinal,
|
||||
estado: calcularEstadoLaboratorioExtendido(core.nombre, String(valorFinal))
|
||||
});
|
||||
|
||||
processedLines.add(lIdx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process Configured Dynamic Groups of Other Determinations
|
||||
const activeGroups = (gruposConfigurados || []).filter(g => g.activo !== false);
|
||||
|
||||
for (const grupo of activeGroups) {
|
||||
for (const det of (grupo.determinaciones || [])) {
|
||||
if (resultados.some(r => r.parametro === det.nombre)) continue;
|
||||
|
||||
for (let lIdx = 0; lIdx < lineas.length; lIdx++) {
|
||||
const linea = lineas[lIdx];
|
||||
const lineaLower = linea.toLowerCase().trim();
|
||||
if (!lineaLower) continue;
|
||||
|
||||
let matchedClave = false;
|
||||
let matchedClaveString = '';
|
||||
|
||||
for (const clave of (det.claves || [])) {
|
||||
if (det.nombre === 'Colesterol Total' && clave === 'colesterol') {
|
||||
if (lineaLower.includes('ldl') || lineaLower.includes('hdl') || lineaLower.includes('no')) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (det.nombre === 'Transferrina' && clave === 'transferrina') {
|
||||
if (lineaLower.includes('saturac') || lineaLower.includes('sat') || lineaLower.includes('%')) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (matchClaveTexto(lineaLower, clave)) {
|
||||
matchedClave = true;
|
||||
matchedClaveString = clave;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedClave) {
|
||||
let subLinea = linea;
|
||||
const idxClave = lineaLower.indexOf(matchedClaveString);
|
||||
if (idxClave !== -1) {
|
||||
subLinea = linea.substring(idxClave + matchedClaveString.length);
|
||||
}
|
||||
|
||||
const cleanSubLinea = subLinea.replace(/\([^)]*?(?:-|–|—|:|[0-9]\s*[-–—]\s*[0-9]|ref|vr|val|norm|min|max)[^)]*?\)/gi, ' ');
|
||||
let match = cleanSubLinea.match(/(\d+(?:[.,]\d+)?)/);
|
||||
if (!match) 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 < 10000000) {
|
||||
const estado = calcularEstadoLaboratorioExtendido(det.nombre, valor, det.rangoReferencia);
|
||||
|
||||
const recItem: RecognizedDetermination = {
|
||||
grupoId: grupo.id,
|
||||
grupoNombre: grupo.nombreGrupo,
|
||||
parametro: det.nombre,
|
||||
valor,
|
||||
unidad: det.unidad,
|
||||
estado,
|
||||
claveCoincidente: matchedClaveString,
|
||||
rangoReferencia: det.rangoReferencia,
|
||||
lineaOriginal: linea.trim()
|
||||
};
|
||||
|
||||
reconocidos.push(recItem);
|
||||
processedLines.add(lIdx);
|
||||
|
||||
resultados.push({
|
||||
parametro: det.nombre,
|
||||
valor,
|
||||
unidad: det.unidad,
|
||||
estado,
|
||||
valorReferencia: det.rangoReferencia
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Detect Potential Unrecognized Determinations (Training Suggestions)
|
||||
const desconocidosPotenciales: PotentialUnknownDetermination[] = [];
|
||||
for (let lIdx = 0; lIdx < lineas.length; lIdx++) {
|
||||
if (processedLines.has(lIdx)) continue;
|
||||
const rawLine = lineas[lIdx].trim();
|
||||
if (!rawLine || rawLine.length < 3 || rawLine.length > 120) continue;
|
||||
|
||||
// Check if line looks like: Name : 12.3 mg/dL OR Name 12.3
|
||||
const pat1 = /^([a-zA-ZáéíóúñÁÉÍÓÚÑ\s/().%+-]{3,40})[:=]\s*(\d+(?:[.,]\d+)?)\s*([a-zA-Z/µ%³0-9-]*)/;
|
||||
const m1 = rawLine.match(pat1);
|
||||
if (m1) {
|
||||
const posNombre = m1[1].trim();
|
||||
const posVal = parseFloat(m1[2].replace(',', '.'));
|
||||
const posUnidad = m1[3].trim();
|
||||
if (posNombre && !isNaN(posVal) && posNombre.toLowerCase() !== 'fecha' && posNombre.toLowerCase() !== 'hora' && posNombre.toLowerCase() !== 'cama' && posNombre.toLowerCase() !== 'dni') {
|
||||
desconocidosPotenciales.push({
|
||||
linea: rawLine,
|
||||
posibleNombre: posNombre,
|
||||
posibleValor: posVal,
|
||||
posibleUnidad: posUnidad
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Build Organized Observaciones per Group
|
||||
const observacionesPorGrupo: { grupo: string; lineas: string[] }[] = [];
|
||||
for (const grupo of activeGroups) {
|
||||
const itemsDelGrupo = reconocidos.filter(r => r.grupoId === grupo.id);
|
||||
if (itemsDelGrupo.length > 0) {
|
||||
const lineasGrupo: string[] = [];
|
||||
for (const item of itemsDelGrupo) {
|
||||
lineasGrupo.push(`- ${item.parametro}: ${item.valor} ${item.unidad}`);
|
||||
}
|
||||
observacionesPorGrupo.push({
|
||||
grupo: grupo.nombreGrupo.toUpperCase(),
|
||||
lineas: lineasGrupo
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build the complete formatted observations string
|
||||
const blocks: string[] = [];
|
||||
for (const g of observacionesPorGrupo) {
|
||||
blocks.push(`${g.grupo}:\n${g.lineas.join('\n')}`);
|
||||
}
|
||||
const observacionesStr = blocks.join('\n\n');
|
||||
|
||||
// 5. Parse Gasometría / Ácido Base
|
||||
const acidoBase = parseAcidoBaseTexto(texto, fecha, pacienteId);
|
||||
|
||||
return {
|
||||
resultados,
|
||||
observaciones: observacionesStr,
|
||||
observacionesPorGrupo,
|
||||
reconocidos,
|
||||
desconocidosPotenciales,
|
||||
acidoBase
|
||||
};
|
||||
}
|
||||
|
||||
export function buildObservacionesFromResults(
|
||||
resultadosArray: ResultadoLaboratorio[],
|
||||
gruposConfigurados: GrupoDeterminacionLaboratorio[] = []
|
||||
): string {
|
||||
if (!resultadosArray || resultadosArray.length === 0) return '';
|
||||
const blocks: string[] = [];
|
||||
const processedParams = new Set<string>();
|
||||
|
||||
const activeGroups = (gruposConfigurados || []).filter(g => g.activo !== false);
|
||||
|
||||
for (const grupo of activeGroups) {
|
||||
const groupItems: string[] = [];
|
||||
for (const det of (grupo.determinaciones || [])) {
|
||||
const match = resultadosArray.find(r => r.parametro === det.nombre);
|
||||
if (match) {
|
||||
groupItems.push(`- ${match.parametro}: ${match.valor} ${match.unidad}`);
|
||||
processedParams.add(match.parametro);
|
||||
}
|
||||
}
|
||||
if (groupItems.length > 0) {
|
||||
blocks.push(`${grupo.nombreGrupo.toUpperCase()}:\n${groupItems.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Any other non-core results not belonging to known groups
|
||||
const uncategorized = resultadosArray.filter(r => !esParametroCore(r.parametro) && !processedParams.has(r.parametro));
|
||||
if (uncategorized.length > 0) {
|
||||
const uncatLines = uncategorized.map(u => `- ${u.parametro}: ${u.valor} ${u.unidad}`);
|
||||
blocks.push(`OTRAS DETERMINACIONES:\n${uncatLines.join('\n')}`);
|
||||
}
|
||||
|
||||
return blocks.join('\n\n');
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { toast } from 'sonner';
|
||||
import { User } from 'lucide-react';
|
||||
import { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||
import { getNombreProfesional } from '@/lib/utils';
|
||||
import { parseLaboratorioTextoCompleto, buildObservacionesFromResults, esParametroCore as esParamCoreHelper } from '@/lib/laboratoryParser';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -1028,6 +1029,7 @@ function SeccionLaboratorios({ lab, otrosLaboratorios = [], patientId, internaci
|
||||
canEdit?: boolean;
|
||||
portalNode?: HTMLDivElement | null;
|
||||
}) {
|
||||
const { gruposDeterminacionesLab } = useHospitalStore();
|
||||
const [dialog, setDialog] = useState(false);
|
||||
const [obsDialog, setObsDialog] = useState(false);
|
||||
|
||||
@@ -1247,345 +1249,21 @@ function SeccionLaboratorios({ lab, otrosLaboratorios = [], patientId, internaci
|
||||
setEdit(null);
|
||||
};
|
||||
|
||||
const buildObservacionesFromResultados = (resultadosArray: ResultadoLaboratorio[]): string => {
|
||||
const observacionesExtra: string[] = [];
|
||||
|
||||
// 1. Lípidos
|
||||
const ordenLipidos = ['Colesterol Total', 'Colesterol LDL', 'Colesterol No HDL', 'Colesterol HDL', 'Triglicéridos'];
|
||||
const lipidosEncontrados = resultadosArray.filter(r => ordenLipidos.includes(r.parametro));
|
||||
if (lipidosEncontrados.length > 0) {
|
||||
observacionesExtra.push('PERFIL LIPIDICO:');
|
||||
for (const nombre of ordenLipidos) {
|
||||
const item = lipidosEncontrados.find(l => l.parametro === nombre);
|
||||
if (item) {
|
||||
observacionesExtra.push(`- ${item.parametro}: ${item.valor} ${item.unidad}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Férricos
|
||||
const ordenFerricos = ['Hierro', 'Transferrina', 'Porcentaje de Saturación de Transferrina', 'Ferritina', 'Ácido Fólico', 'Vitamina B12'];
|
||||
const ferricosEncontrados = resultadosArray.filter(r => ordenFerricos.includes(r.parametro));
|
||||
if (ferricosEncontrados.length > 0) {
|
||||
if (observacionesExtra.length > 0) {
|
||||
observacionesExtra.push('');
|
||||
}
|
||||
observacionesExtra.push('PERFIL FERRICO:');
|
||||
for (const nombre of ordenFerricos) {
|
||||
const item = ferricosEncontrados.find(f => f.parametro === nombre);
|
||||
if (item) {
|
||||
observacionesExtra.push(`- ${item.parametro}: ${item.valor} ${item.unidad}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Independientes
|
||||
const ordenInd = [
|
||||
'Albúmina',
|
||||
'Calcio Total',
|
||||
'Fosfatasa Alcalina',
|
||||
'LDH',
|
||||
'NT-proBNP',
|
||||
'Procalcitonina',
|
||||
'Fósforo',
|
||||
'Magnesio',
|
||||
'Calcio Iónico'
|
||||
];
|
||||
const indEncontrados = resultadosArray.filter(r => ordenInd.includes(r.parametro));
|
||||
if (indEncontrados.length > 0) {
|
||||
if (observacionesExtra.length > 0) {
|
||||
observacionesExtra.push('');
|
||||
}
|
||||
for (const nombre of ordenInd) {
|
||||
const item = indEncontrados.find(i => i.parametro === nombre);
|
||||
if (item) {
|
||||
observacionesExtra.push(`${item.parametro}: ${item.valor} ${item.unidad}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. VCM & HCM
|
||||
const ordenHemogramaAdicionales = ['VCM', 'HCM'];
|
||||
const hemogramaAdicionalesEncontrados = resultadosArray.filter(r => ordenHemogramaAdicionales.includes(r.parametro));
|
||||
if (hemogramaAdicionalesEncontrados.length > 0) {
|
||||
if (observacionesExtra.length > 0) {
|
||||
observacionesExtra.push('');
|
||||
}
|
||||
for (const nombre of ordenHemogramaAdicionales) {
|
||||
const item = hemogramaAdicionalesEncontrados.find(h => h.parametro === nombre);
|
||||
if (item) {
|
||||
observacionesExtra.push(`${item.parametro}: ${item.valor} ${item.unidad}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return observacionesExtra.join('\n');
|
||||
};
|
||||
|
||||
const parseLaboratorioTexto = (texto: string): { resultados: ResultadoLaboratorio[]; observaciones: string } => {
|
||||
const resultados: ResultadoLaboratorio[] = [];
|
||||
|
||||
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, esAdicional: true },
|
||||
{ claves: ['hemoglobina', 'hb'], nombre: 'Hemoglobina', unidad: 'g/dL', esPrincipal: true },
|
||||
{ claves: ['volumen corpuscular medio', 'vcm'], nombre: 'VCM', unidad: 'fL', esPrincipal: false, esAdicional: true },
|
||||
{ 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', 'magnesemia', 'mg++', 'mg+', 'mg2+', 'mg 2+', 'mg.', 'magnesio plasmatico', 'magnesio plasmático', 'magnesio serico', 'magnesio sérico', 'magnesio en sangre', 'mg serico', 'mg sérico', 'mg plasmatico', 'mg plasmático', 'mg'], 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
|
||||
{ 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 === 'mg') {
|
||||
const execMatch = /(?:^|[^a-z0-9_])mg(?=[:\s=+\d]|$)(?!\/(?:dl|l|24h|ml)|%)/i.exec(lineaLower);
|
||||
if (!execMatch) return false;
|
||||
const prefix = lineaLower.substring(0, execMatch.index);
|
||||
return !/\d+\s*$/.test(prefix);
|
||||
}
|
||||
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) {
|
||||
let subLinea = linea;
|
||||
const idxClave = lineaLower.indexOf(matchedClaveString);
|
||||
if (idxClave !== -1) {
|
||||
subLinea = linea.substring(idxClave + matchedClaveString.length);
|
||||
}
|
||||
|
||||
// Clean parenthesized reference values/ranges if present (e.g. "(1.6 - 2.6 mg/dL)")
|
||||
const cleanSubLinea = subLinea.replace(/\([^)]*?(?:-|–|—|:|[0-9]\s*[-–—]\s*[0-9]|ref|vr|val|norm|min|max)[^)]*?\)/gi, ' ');
|
||||
|
||||
let match = cleanSubLinea.match(/(\d+(?:[.,]\d+)?)/);
|
||||
if (!match) {
|
||||
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))
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observacionesStr = buildObservacionesFromResultados(resultados);
|
||||
return { resultados, observaciones: observacionesStr };
|
||||
};
|
||||
|
||||
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',
|
||||
'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',
|
||||
// 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 => {
|
||||
const filtered = prev.filter((_, i) => i !== index);
|
||||
setImportObservaciones(buildObservacionesFromResultados(filtered));
|
||||
setImportObservaciones(buildObservacionesFromResults(filtered, gruposDeterminacionesLab || []));
|
||||
return filtered;
|
||||
});
|
||||
};
|
||||
|
||||
const handleProcesarTexto = () => {
|
||||
const { resultados, observaciones } = parseLaboratorioTexto(importTexto);
|
||||
const acidoBase = parseAcidoBaseTexto(importTexto);
|
||||
const { resultados, observaciones, acidoBase } = parseLaboratorioTextoCompleto(
|
||||
importTexto,
|
||||
gruposDeterminacionesLab || [],
|
||||
importFecha,
|
||||
patientId
|
||||
);
|
||||
|
||||
setImportResultados(resultados);
|
||||
setImportObservaciones(observaciones);
|
||||
@@ -2127,7 +1805,7 @@ function SeccionLaboratorios({ lab, otrosLaboratorios = [], patientId, internaci
|
||||
{importResultados.map((r, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="text-center p-2 w-10">
|
||||
{!esParametroCore(r.parametro) && (
|
||||
{!esParamCoreHelper(r.parametro) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
||||
+183
-102
@@ -23,11 +23,13 @@ import {
|
||||
CheckCircle2,
|
||||
Tag,
|
||||
Info,
|
||||
RotateCcw
|
||||
RotateCcw,
|
||||
FlaskConical
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||
import type { TipoCultivo } from '@/types';
|
||||
import { GestorParserLaboratorio } from '@/components/sistema/GestorParserLaboratorio';
|
||||
|
||||
export function SeccionSistema() {
|
||||
const {
|
||||
@@ -377,21 +379,32 @@ export function SeccionSistema() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Sistema y Configuración</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400">Administración de parámetros clínicos, base de datos y depuración</p>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Sistema y Configuración</h2>
|
||||
<p className="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Administración de parámetros clínicos, base de datos y depuración</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid grid-cols-2 sm:w-96 mb-6">
|
||||
<TabsTrigger value="cultivos" className="flex items-center gap-2">
|
||||
<Microscope className="h-4 w-4" /> Tipos de Cultivos
|
||||
<TabsList className="grid grid-cols-1 sm:grid-cols-3 h-auto w-full max-w-2xl mb-6 p-1 gap-1">
|
||||
<TabsTrigger value="cultivos" className="flex items-center justify-center gap-2 py-2 text-xs sm:text-sm">
|
||||
<Microscope className="h-4 w-4 shrink-0" />
|
||||
<span>Tipos de Cultivos</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mantenimiento" className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4" /> Consola & Mantenimiento
|
||||
<TabsTrigger value="parser-lab" className="flex items-center justify-center gap-2 py-2 text-xs sm:text-sm">
|
||||
<FlaskConical className="h-4 w-4 shrink-0 text-indigo-600 dark:text-indigo-400" />
|
||||
<span>Parser de Laboratorio</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mantenimiento" className="flex items-center justify-center gap-2 py-2 text-xs sm:text-sm">
|
||||
<Terminal className="h-4 w-4 shrink-0" />
|
||||
<span>Consola & Base de Datos</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* TAB: ENTRENAMIENTO DEL PARSER DE LABORATORIO */}
|
||||
<TabsContent value="parser-lab" className="space-y-6">
|
||||
<GestorParserLaboratorio />
|
||||
</TabsContent>
|
||||
|
||||
{/* TAB 1: GESTIÓN DE TIPOS DE CULTIVOS */}
|
||||
<TabsContent value="cultivos" className="space-y-6">
|
||||
{/* Header Card with Metrics */}
|
||||
@@ -438,22 +451,22 @@ export function SeccionSistema() {
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<Microscope className="h-5 w-5 text-blue-600" />
|
||||
Catálogo de Tipos de Cultivo y Muestras
|
||||
<CardTitle className="text-lg sm:text-xl flex items-center gap-2">
|
||||
<Microscope className="h-5 w-5 text-blue-600 shrink-0" />
|
||||
<span>Catálogo de Tipos de Cultivo y Muestras</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs sm:text-sm">
|
||||
Administre las opciones que aparecen al crear un nuevo cultivo en las Historias Clínicas.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Button variant="outline" size="sm" onClick={() => setIsResetDialogOpen(true)}>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Restablecer
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||
<Button variant="outline" size="sm" onClick={() => setIsResetDialogOpen(true)} className="flex-1 sm:flex-none justify-center">
|
||||
<RotateCcw className="h-4 w-4 mr-2 shrink-0" />
|
||||
<span>Restablecer</span>
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleOpenNewDialog}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nuevo Tipo de Cultivo
|
||||
<Button size="sm" onClick={handleOpenNewDialog} className="flex-1 sm:flex-none justify-center">
|
||||
<Plus className="h-4 w-4 mr-2 shrink-0" />
|
||||
<span>Nuevo Tipo</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -461,20 +474,20 @@ export function SeccionSistema() {
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{/* Search and Filters */}
|
||||
<div className="flex flex-col sm:flex-row items-center gap-3">
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
|
||||
<div className="relative flex-1 w-full">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Buscar por nombre, categoría o descripción..."
|
||||
placeholder="Buscar por nombre, categoría..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9"
|
||||
className="pl-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-1.5 w-full sm:w-auto">
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap font-medium">Categoría:</span>
|
||||
<Select value={selectedCategoria} onValueChange={setSelectedCategoria}>
|
||||
<SelectTrigger className="w-full sm:w-[220px]">
|
||||
<SelectTrigger className="w-full sm:w-[220px] text-sm">
|
||||
<SelectValue placeholder="Filtrar por categoría" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-60">
|
||||
@@ -487,8 +500,72 @@ export function SeccionSistema() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table of Tipos */}
|
||||
<div className="border rounded-lg overflow-hidden border-gray-200 dark:border-gray-800">
|
||||
{/* Mobile View: Cards for small screens */}
|
||||
<div className="grid grid-cols-1 gap-3 md:hidden">
|
||||
{tiposFiltrados.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/40 rounded-lg p-4 border border-gray-200 dark:border-gray-800">
|
||||
<Microscope className="h-8 w-8 mx-auto mb-2 opacity-40 text-gray-400" />
|
||||
<p className="font-medium text-sm">No se encontraron tipos de cultivo</p>
|
||||
<p className="text-xs text-gray-400 mt-1">Pruebe ajustando los filtros de búsqueda o agregue uno nuevo.</p>
|
||||
</div>
|
||||
) : (
|
||||
tiposFiltrados.map((tipo) => {
|
||||
const count = usageCountMap[tipo.nombre.trim()] || 0;
|
||||
return (
|
||||
<div key={tipo.id} className="p-3.5 border rounded-lg bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-800 space-y-2.5 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="bg-blue-50 text-blue-700 dark:bg-blue-950/80 dark:text-blue-300 px-2.5 py-1 rounded text-sm font-semibold border border-blue-200 dark:border-blue-800">
|
||||
{tipo.nombre}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs bg-gray-50 dark:bg-gray-800">
|
||||
{tipo.categoria || 'General'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleOpenEditDialog(tipo)}
|
||||
className="h-8 w-8 p-0 text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/50"
|
||||
title="Modificar tipo de cultivo"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleOpenDeleteDialog(tipo)}
|
||||
className="h-8 w-8 p-0 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/50"
|
||||
title="Eliminar tipo de cultivo"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{tipo.descripcion ? (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 leading-relaxed">{tipo.descripcion}</p>
|
||||
) : (
|
||||
<p className="text-xs text-gray-400 italic">Sin descripción</p>
|
||||
)}
|
||||
<div className="pt-2 flex items-center justify-between border-t border-gray-100 dark:border-gray-800 text-xs">
|
||||
<span className="text-gray-500 font-medium">Uso en cultivos:</span>
|
||||
{count > 0 ? (
|
||||
<Badge className="bg-emerald-100 text-emerald-800 dark:bg-emerald-950/80 dark:text-emerald-300 text-xs font-semibold">
|
||||
{count} {count === 1 ? 'cultivo' : 'cultivos'}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-gray-400">0 usos</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop View: Table for medium & large screens */}
|
||||
<div className="hidden md:block border rounded-lg overflow-x-auto border-gray-200 dark:border-gray-800">
|
||||
<Table>
|
||||
<TableHeader className="bg-gray-50 dark:bg-gray-800/50">
|
||||
<TableRow>
|
||||
@@ -573,14 +650,15 @@ export function SeccionSistema() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="col-span-1 md:col-span-3">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5" /> Consola mongosh
|
||||
<CardTitle className="flex items-center gap-2 text-base sm:text-lg">
|
||||
<Terminal className="h-5 w-5 text-green-600 shrink-0" />
|
||||
<span>Consola mongosh</span>
|
||||
</CardTitle>
|
||||
<CardDescription>Ejecuta comandos de consulta directamente en la base de datos.</CardDescription>
|
||||
<CardDescription className="text-xs sm:text-sm">Ejecuta comandos de consulta directamente en la base de datos.</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleClearConsole}>
|
||||
<Button variant="outline" size="sm" onClick={handleClearConsole} className="self-start sm:self-auto">
|
||||
<RefreshCw className="h-4 w-4 mr-2" /> Limpiar
|
||||
</Button>
|
||||
</div>
|
||||
@@ -588,7 +666,7 @@ export function SeccionSistema() {
|
||||
<CardContent>
|
||||
<div
|
||||
ref={terminalContainerRef}
|
||||
className="bg-gray-900 text-green-400 font-mono text-sm p-4 rounded-md h-[400px] overflow-y-auto flex flex-col gap-1 relative cursor-text group"
|
||||
className="bg-gray-950 text-green-400 font-mono text-xs sm:text-sm p-3 sm:p-4 rounded-md h-[320px] sm:h-[400px] overflow-y-auto flex flex-col gap-1 relative cursor-text group border border-gray-800"
|
||||
onClick={() => inputRef.current?.focus({ preventScroll: true })}
|
||||
>
|
||||
{consoleHistory.length === 0 && (
|
||||
@@ -599,7 +677,7 @@ export function SeccionSistema() {
|
||||
{item.type === 'input' && <span className="text-blue-500 mr-2">{'>'}</span>}
|
||||
{item.type === 'output' && <span className="text-gray-500 mr-2">{'<-'}</span>}
|
||||
{item.type === 'error' && <span className="text-red-500 mr-2">{'!'}</span>}
|
||||
<pre className="inline whitespace-pre-wrap font-inherit m-0">{item.content}</pre>
|
||||
<pre className="inline whitespace-pre-wrap break-all font-inherit m-0">{item.content}</pre>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -687,34 +765,34 @@ export function SeccionSistema() {
|
||||
|
||||
{/* DIALOG: NUEVO TIPO DE CULTIVO */}
|
||||
<Dialog open={isNewDialogOpen} onOpenChange={setIsNewDialogOpen}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Plus className="h-5 w-5 text-blue-600" />
|
||||
Nuevo Tipo de Cultivo / Muestra
|
||||
<DialogContent className="w-[95vw] max-w-[calc(100vw-1.5rem)] sm:max-w-lg max-h-[90vh] overflow-y-auto p-4 sm:p-6 rounded-lg">
|
||||
<DialogHeader className="space-y-1">
|
||||
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
|
||||
<Plus className="h-5 w-5 text-blue-600 shrink-0" />
|
||||
<span>Nuevo Tipo de Cultivo / Muestra</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
Ingrese los detalles del nuevo tipo de muestra para registrar cultivos.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleCreateTipo} className="space-y-4 py-2">
|
||||
<div>
|
||||
<Label htmlFor="nuevo-nombre" className="text-sm font-semibold">Nombre / Código de Muestra *</Label>
|
||||
<Label htmlFor="nuevo-nombre" className="text-xs sm:text-sm font-semibold">Nombre / Código de Muestra *</Label>
|
||||
<Input
|
||||
id="nuevo-nombre"
|
||||
placeholder="Ej: Hemocultivo Central, Líquido Peritoneal, etc."
|
||||
value={formNombre}
|
||||
onChange={(e) => setFormNombre(e.target.value)}
|
||||
required
|
||||
className="mt-1"
|
||||
className="mt-1 text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label htmlFor="nuevo-categoria" className="text-sm font-semibold">Categoría / Grupo</Label>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-1 mb-1.5">
|
||||
<Label htmlFor="nuevo-categoria" className="text-xs sm:text-sm font-semibold">Categoría / Grupo</Label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -722,7 +800,7 @@ export function SeccionSistema() {
|
||||
if (!isCustomCategoriaNew) setFormCategoria('');
|
||||
else setFormCategoria(categoriasDisponibles[0] || 'General');
|
||||
}}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline font-medium"
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline font-medium text-left sm:text-right"
|
||||
>
|
||||
{isCustomCategoriaNew ? '← Elegir de existentes' : '➕ Crear nueva categoría'}
|
||||
</button>
|
||||
@@ -741,26 +819,26 @@ export function SeccionSistema() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="nuevo-categoria" className="w-full">
|
||||
<SelectTrigger id="nuevo-categoria" className="w-full text-sm">
|
||||
<SelectValue placeholder="Seleccionar categoría" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-60">
|
||||
<SelectGroup>
|
||||
<SelectLabel>Categorías Existentes ({categoriasDisponibles.length})</SelectLabel>
|
||||
<SelectLabel className="text-xs">Categorías Existentes ({categoriasDisponibles.length})</SelectLabel>
|
||||
{categoriasDisponibles.map(c => (
|
||||
<SelectItem key={c} value={c}>
|
||||
<SelectItem key={c} value={c} className="text-sm">
|
||||
{c}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectItem value="__custom__" className="text-blue-600 dark:text-blue-400 font-medium">
|
||||
<SelectItem value="__custom__" className="text-blue-600 dark:text-blue-400 font-medium text-sm">
|
||||
➕ Escribir nueva categoría...
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500 mt-1">Agrupa los tipos en el selector de creación de cultivo.</p>
|
||||
<p className="text-[11px] sm:text-xs text-gray-500 mt-1">Agrupa los tipos en el selector de creación de cultivo.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
@@ -771,28 +849,29 @@ export function SeccionSistema() {
|
||||
onChange={(e) => setFormCategoria(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
className="text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">Escriba el nombre para crear una nueva categoría.</p>
|
||||
<p className="text-[11px] sm:text-xs text-gray-500">Escriba el nombre para crear una nueva categoría.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="nuevo-desc" className="text-sm font-semibold">Descripción / Indicación (Opcional)</Label>
|
||||
<Label htmlFor="nuevo-desc" className="text-xs sm:text-sm font-semibold">Descripción / Indicación (Opcional)</Label>
|
||||
<Input
|
||||
id="nuevo-desc"
|
||||
placeholder="Ej: Muestra tomada por punción con técnica estéril"
|
||||
value={formDescripcion}
|
||||
onChange={(e) => setFormDescripcion(e.target.value)}
|
||||
className="mt-1"
|
||||
className="mt-1 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="pt-4 flex gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setIsNewDialogOpen(false)} disabled={isSubmitting}>
|
||||
<DialogFooter className="pt-4 flex flex-col-reverse sm:flex-row gap-2 sm:justify-end">
|
||||
<Button type="button" variant="outline" onClick={() => setIsNewDialogOpen(false)} disabled={isSubmitting} className="w-full sm:w-auto">
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
<Button type="submit" disabled={isSubmitting} className="w-full sm:w-auto">
|
||||
{isSubmitting ? 'Guardando...' : 'Crear Tipo de Cultivo'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -802,32 +881,32 @@ export function SeccionSistema() {
|
||||
|
||||
{/* DIALOG: EDITAR TIPO DE CULTIVO */}
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Pencil className="h-5 w-5 text-blue-600" />
|
||||
Modificar Tipo de Cultivo
|
||||
<DialogContent className="w-[95vw] max-w-[calc(100vw-1.5rem)] sm:max-w-lg max-h-[90vh] overflow-y-auto p-4 sm:p-6 rounded-lg">
|
||||
<DialogHeader className="space-y-1">
|
||||
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
|
||||
<Pencil className="h-5 w-5 text-blue-600 shrink-0" />
|
||||
<span>Modificar Tipo de Cultivo</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
Edite el nombre, categoría o descripción de este tipo de cultivo.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleUpdateTipo} className="space-y-4 py-2">
|
||||
<div>
|
||||
<Label htmlFor="edit-nombre" className="text-sm font-semibold">Nombre / Código de Muestra *</Label>
|
||||
<Label htmlFor="edit-nombre" className="text-xs sm:text-sm font-semibold">Nombre / Código de Muestra *</Label>
|
||||
<Input
|
||||
id="edit-nombre"
|
||||
value={formNombre}
|
||||
onChange={(e) => setFormNombre(e.target.value)}
|
||||
required
|
||||
className="mt-1"
|
||||
className="mt-1 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label htmlFor="edit-categoria" className="text-sm font-semibold">Categoría / Grupo</Label>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-1 mb-1.5">
|
||||
<Label htmlFor="edit-categoria" className="text-xs sm:text-sm font-semibold">Categoría / Grupo</Label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -835,7 +914,7 @@ export function SeccionSistema() {
|
||||
if (!isCustomCategoriaEdit) setFormCategoria('');
|
||||
else setFormCategoria(categoriasDisponibles[0] || 'General');
|
||||
}}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline font-medium"
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline font-medium text-left sm:text-right"
|
||||
>
|
||||
{isCustomCategoriaEdit ? '← Elegir de existentes' : '➕ Crear nueva categoría'}
|
||||
</button>
|
||||
@@ -854,26 +933,26 @@ export function SeccionSistema() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="edit-categoria" className="w-full">
|
||||
<SelectTrigger id="edit-categoria" className="w-full text-sm">
|
||||
<SelectValue placeholder="Seleccionar categoría" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-60">
|
||||
<SelectGroup>
|
||||
<SelectLabel>Categorías Existentes ({categoriasDisponibles.length})</SelectLabel>
|
||||
<SelectLabel className="text-xs">Categorías Existentes ({categoriasDisponibles.length})</SelectLabel>
|
||||
{categoriasDisponibles.map(c => (
|
||||
<SelectItem key={c} value={c}>
|
||||
<SelectItem key={c} value={c} className="text-sm">
|
||||
{c}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectItem value="__custom__" className="text-blue-600 dark:text-blue-400 font-medium">
|
||||
<SelectItem value="__custom__" className="text-blue-600 dark:text-blue-400 font-medium text-sm">
|
||||
➕ Escribir nueva categoría...
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500 mt-1">Categoría a la que pertenece este tipo de cultivo.</p>
|
||||
<p className="text-[11px] sm:text-xs text-gray-500 mt-1">Categoría a la que pertenece este tipo de cultivo.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
@@ -884,27 +963,29 @@ export function SeccionSistema() {
|
||||
onChange={(e) => setFormCategoria(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
className="text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">Escriba el nombre de la nueva categoría.</p>
|
||||
<p className="text-[11px] sm:text-xs text-gray-500">Escriba el nombre de la nueva categoría.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-desc" className="text-sm font-semibold">Descripción / Indicación</Label>
|
||||
<Label htmlFor="edit-desc" className="text-xs sm:text-sm font-semibold">Descripción / Indicación</Label>
|
||||
<Input
|
||||
id="edit-desc"
|
||||
value={formDescripcion}
|
||||
onChange={(e) => setFormDescripcion(e.target.value)}
|
||||
className="mt-1"
|
||||
className="mt-1 text-sm"
|
||||
placeholder="Ej: Indicación de toma de muestra o preparación"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="pt-4 flex gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setIsEditDialogOpen(false)} disabled={isSubmitting}>
|
||||
<DialogFooter className="pt-4 flex flex-col-reverse sm:flex-row gap-2 sm:justify-end">
|
||||
<Button type="button" variant="outline" onClick={() => setIsEditDialogOpen(false)} disabled={isSubmitting} className="w-full sm:w-auto">
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
<Button type="submit" disabled={isSubmitting} className="w-full sm:w-auto">
|
||||
{isSubmitting ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -914,19 +995,19 @@ export function SeccionSistema() {
|
||||
|
||||
{/* DIALOG: ELIMINAR TIPO DE CULTIVO */}
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-red-600">
|
||||
<Trash2 className="h-5 w-5" />
|
||||
Eliminar Tipo de Cultivo
|
||||
<DialogContent className="w-[95vw] max-w-[calc(100vw-1.5rem)] sm:max-w-md max-h-[90vh] overflow-y-auto p-4 sm:p-6 rounded-lg">
|
||||
<DialogHeader className="space-y-1">
|
||||
<DialogTitle className="flex items-center gap-2 text-red-600 text-base sm:text-lg">
|
||||
<Trash2 className="h-5 w-5 shrink-0" />
|
||||
<span>Eliminar Tipo de Cultivo</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
¿Está seguro de que desea eliminar el tipo de cultivo "{selectedTipo?.nombre}"?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedTipo && (usageCountMap[selectedTipo.nombre.trim()] || 0) > 0 && (
|
||||
<div className="bg-amber-50 dark:bg-amber-950/50 border border-amber-200 dark:border-amber-800 p-3 rounded-md flex items-start gap-2 text-amber-800 dark:text-amber-200 text-sm">
|
||||
<div className="bg-amber-50 dark:bg-amber-950/50 border border-amber-200 dark:border-amber-800 p-3 rounded-md flex items-start gap-2 text-amber-800 dark:text-amber-200 text-xs sm:text-sm">
|
||||
<Info className="h-4 w-4 mt-0.5 shrink-0 text-amber-600" />
|
||||
<span>
|
||||
<strong>Aviso:</strong> Existen {usageCountMap[selectedTipo.nombre.trim()]} registro(s) de cultivos que actualmente utilizan este tipo de muestra. Los registros existentes mantendrán el texto histórico, pero este tipo ya no aparecerá para nuevos registros.
|
||||
@@ -934,11 +1015,11 @@ export function SeccionSistema() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="pt-2 flex gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setIsDeleteDialogOpen(false)} disabled={isSubmitting}>
|
||||
<DialogFooter className="pt-2 flex flex-col-reverse sm:flex-row gap-2 sm:justify-end">
|
||||
<Button type="button" variant="outline" onClick={() => setIsDeleteDialogOpen(false)} disabled={isSubmitting} className="w-full sm:w-auto">
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="button" variant="destructive" onClick={handleDeleteTipo} disabled={isSubmitting}>
|
||||
<Button type="button" variant="destructive" onClick={handleDeleteTipo} disabled={isSubmitting} className="w-full sm:w-auto">
|
||||
{isSubmitting ? 'Eliminando...' : 'Sí, Eliminar'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -947,26 +1028,26 @@ export function SeccionSistema() {
|
||||
|
||||
{/* DIALOG: RESTABLECER PREDETERMINADOS */}
|
||||
<Dialog open={isResetDialogOpen} onOpenChange={setIsResetDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-blue-600">
|
||||
<RotateCcw className="h-5 w-5" />
|
||||
Restablecer Valores Predeterminados
|
||||
<DialogContent className="w-[95vw] max-w-[calc(100vw-1.5rem)] sm:max-w-md max-h-[90vh] overflow-y-auto p-4 sm:p-6 rounded-lg">
|
||||
<DialogHeader className="space-y-1">
|
||||
<DialogTitle className="flex items-center gap-2 text-blue-600 text-base sm:text-lg">
|
||||
<RotateCcw className="h-5 w-5 shrink-0" />
|
||||
<span>Restablecer Valores Predeterminados</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
Esta acción restaurará el catálogo estándar de tipos de cultivo (HMCx2, RC, UC, LCR, Esputos, Hisopados, etc.).
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<p className="text-xs sm:text-sm text-gray-600 dark:text-gray-300">
|
||||
¿Desea restablecer los tipos de muestra a los valores predeterminados del hospital?
|
||||
</p>
|
||||
|
||||
<DialogFooter className="pt-2 flex gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setIsResetDialogOpen(false)} disabled={isSubmitting}>
|
||||
<DialogFooter className="pt-2 flex flex-col-reverse sm:flex-row gap-2 sm:justify-end">
|
||||
<Button type="button" variant="outline" onClick={() => setIsResetDialogOpen(false)} disabled={isSubmitting} className="w-full sm:w-auto">
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="button" onClick={handleResetTipos} disabled={isSubmitting}>
|
||||
<Button type="button" onClick={handleResetTipos} disabled={isSubmitting} className="w-full sm:w-auto">
|
||||
{isSubmitting ? 'Restableciendo...' : 'Restablecer Catálogo'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -975,14 +1056,14 @@ export function SeccionSistema() {
|
||||
|
||||
{/* LOGS MODAL */}
|
||||
{isLogsModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg p-6 w-full max-w-4xl max-h-[90vh] flex flex-col shadow-xl">
|
||||
<h3 className="text-lg font-bold mb-4 flex justify-between items-center text-gray-900 dark:text-white">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-3">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg p-4 sm:p-6 w-[95vw] max-w-4xl max-h-[90vh] flex flex-col shadow-xl border border-gray-200 dark:border-gray-800">
|
||||
<h3 className="text-base sm:text-lg font-bold mb-3 flex justify-between items-center text-gray-900 dark:text-white">
|
||||
<span>Logs del Servidor</span>
|
||||
<Button variant="ghost" size="sm" onClick={() => setIsLogsModalOpen(false)}>Cerrar</Button>
|
||||
</h3>
|
||||
<div className="flex-1 overflow-auto bg-gray-950 text-green-400 font-mono text-xs p-4 rounded border border-gray-800">
|
||||
<pre className="whitespace-pre-wrap">{logs}</pre>
|
||||
<div className="flex-1 overflow-auto bg-gray-950 text-green-400 font-mono text-xs p-3 sm:p-4 rounded border border-gray-800">
|
||||
<pre className="whitespace-pre-wrap break-all">{logs}</pre>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button onClick={() => setIsLogsModalOpen(false)}>Aceptar</Button>
|
||||
|
||||
@@ -274,6 +274,28 @@ export interface Usuario {
|
||||
fechaCreacion: string;
|
||||
}
|
||||
|
||||
export interface DeterminacionDefinicion {
|
||||
id: string;
|
||||
nombre: string;
|
||||
claves: string[];
|
||||
unidad: string;
|
||||
esPrincipal?: boolean;
|
||||
esAdicional?: boolean;
|
||||
rangoReferencia?: string;
|
||||
descripcion?: string;
|
||||
}
|
||||
|
||||
export interface GrupoDeterminacionLaboratorio {
|
||||
id: string;
|
||||
nombreGrupo: string;
|
||||
descripcion?: string;
|
||||
activo: boolean;
|
||||
orden?: number;
|
||||
determinaciones: DeterminacionDefinicion[];
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Pendiente {
|
||||
id: string;
|
||||
pacienteId: string;
|
||||
|
||||
+1
-2
@@ -1,12 +1,11 @@
|
||||
import path from "path"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
import { inspectAttr } from 'kimi-plugin-inspect-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [inspectAttr(), react()],
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
Reference in New Issue
Block a user