diff --git a/server/api-mongodb.js b/server/api-mongodb.js index a483a20..94828fc 100644 --- a/server/api-mongodb.js +++ b/server/api-mongodb.js @@ -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 { diff --git a/server/db-mongodb.js b/server/db-mongodb.js index a4354e6..4c0bf05 100644 --- a/server/db-mongodb.js +++ b/server/db-mongodb.js @@ -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) { diff --git a/src/components/sistema/GestorParserLaboratorio.tsx b/src/components/sistema/GestorParserLaboratorio.tsx new file mode 100644 index 0000000..fbed341 --- /dev/null +++ b/src/components/sistema/GestorParserLaboratorio.tsx @@ -0,0 +1,1012 @@ +import { useState, useMemo } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Switch } from '@/components/ui/switch'; +import { Textarea } from '@/components/ui/textarea'; +import { + FlaskConical, + Plus, + Pencil, + Trash2, + Search, + CheckCircle2, + RotateCcw, + Sparkles, + Play, + Layers, + ChevronDown, + ChevronUp, + AlertTriangle, + X, + PlusCircle +} from 'lucide-react'; +import { toast } from 'sonner'; +import { useHospitalStore } from '@/hooks/useHospitalStore'; +import type { GrupoDeterminacionLaboratorio, DeterminacionDefinicion } from '@/types'; +import { parseLaboratorioTextoCompleto } from '@/lib/laboratoryParser'; + +export function GestorParserLaboratorio() { + const { + gruposDeterminacionesLab, + agregarGrupoDeterminacionLab, + actualizarGrupoDeterminacionLab, + eliminarGrupoDeterminacionLab, + restablecerGruposDeterminacionesLab + } = useHospitalStore(); + + // Search & Filter + const [searchTerm, setSearchTerm] = useState(''); + const [expandedGroupId, setExpandedGroupId] = useState(null); + + // Group Modals + const [isGroupModalOpen, setIsGroupModalOpen] = useState(false); + const [editingGroup, setEditingGroup] = useState(null); + const [groupNombre, setGroupNombre] = useState(''); + const [groupDescripcion, setGroupDescripcion] = useState(''); + const [isDeletingGroup, setIsDeletingGroup] = useState(false); + const [groupToDelete, setGroupToDelete] = useState(null); + + // Determination Modals + const [isDetModalOpen, setIsDetModalOpen] = useState(false); + const [targetGroupForDet, setTargetGroupForDet] = useState(null); + const [editingDet, setEditingDet] = useState(null); + const [detNombre, setDetNombre] = useState(''); + const [detUnidad, setDetUnidad] = useState('mg/dL'); + const [detRango, setDetRango] = useState(''); + const [detClaves, setDetClaves] = useState([]); + const [nuevaClaveInput, setNuevaClaveInput] = useState(''); + const [detEsAdicional, setDetEsAdicional] = useState(true); + + // Reset Confirmation Modal + const [isResetConfirmOpen, setIsResetConfirmOpen] = useState(false); + + // Interactive Playground / Training Simulator State + const [rawLabText, setRawLabText] = useState( +`ANALISIS CLINICOS DE CONTROL +HEMATOCRITO: 38.5 % +HEMOGLOBINA: 12.8 g/dL +LEUCOCITOS: 8400 /mm3 +PLAQUETAS: 245000 /mm3 +GLUCEMIA: 98 mg/dL +UREA: 34 mg/dL +CREATININA: 0.9 mg/dL +SODIO: 139 mEq/L +POTASIO: 4.2 mEq/L +COLESTEROL TOTAL: 215 mg/dL +COLESTEROL LDL: 135 mg/dL +COLESTEROL HDL: 48 mg/dL +TRIGLICERIDOS: 160 mg/dL +FERRITINA: 320 ng/mL +TRANSFERRINA: 280 mg/dL +SATURACION DE TRANSFERRINA: 28 % +TSH: 2.45 uUI/mL +T4 LIBRE: 1.15 ng/dL +MAGNESIO: 2.1 mg/dL +CALCIO TOTAL: 9.2 mg/dL +PROTEINA C REACTIVA: 3.2 mg/L +PROCALCITONINA: 0.08 ng/mL +LACTATO: 1.4 mmol/L +DIMERO D: 450 ng/mL` + ); + + // Parse simulator execution + const parseSimulation = useMemo(() => { + if (!rawLabText.trim()) return null; + return parseLaboratorioTextoCompleto(rawLabText, gruposDeterminacionesLab || []); + }, [rawLabText, gruposDeterminacionesLab]); + + // Filtered Groups + const filteredGrupos = useMemo(() => { + if (!gruposDeterminacionesLab) return []; + if (!searchTerm.trim()) return gruposDeterminacionesLab; + const term = searchTerm.toLowerCase().trim(); + return gruposDeterminacionesLab.filter(g => + g.nombreGrupo.toLowerCase().includes(term) || + (g.descripcion && g.descripcion.toLowerCase().includes(term)) || + (g.determinaciones && g.determinaciones.some(d => + d.nombre.toLowerCase().includes(term) || + d.claves.some(c => c.toLowerCase().includes(term)) + )) + ); + }, [gruposDeterminacionesLab, searchTerm]); + + // Statistics + const totalGrupos = gruposDeterminacionesLab?.length || 0; + const totalDeterminaciones = useMemo(() => { + return (gruposDeterminacionesLab || []).reduce((acc, g) => acc + (g.determinaciones?.length || 0), 0); + }, [gruposDeterminacionesLab]); + const totalClaves = useMemo(() => { + return (gruposDeterminacionesLab || []).reduce((acc, g) => + acc + (g.determinaciones?.reduce((dAcc, d) => dAcc + (d.claves?.length || 0), 0) || 0), 0); + }, [gruposDeterminacionesLab]); + + // --- Handlers for Groups --- + const handleOpenNewGroup = () => { + setEditingGroup(null); + setGroupNombre(''); + setGroupDescripcion(''); + setIsGroupModalOpen(true); + }; + + const handleOpenEditGroup = (g: GrupoDeterminacionLaboratorio) => { + setEditingGroup(g); + setGroupNombre(g.nombreGrupo); + setGroupDescripcion(g.descripcion || ''); + setIsGroupModalOpen(true); + }; + + const handleSaveGroup = async () => { + if (!groupNombre.trim()) { + toast.error('El nombre del grupo es obligatorio'); + return; + } + try { + if (editingGroup) { + await actualizarGrupoDeterminacionLab(editingGroup.id, { + nombreGrupo: groupNombre.trim(), + descripcion: groupDescripcion.trim() + }); + } else { + await agregarGrupoDeterminacionLab({ + nombreGrupo: groupNombre.trim(), + descripcion: groupDescripcion.trim(), + activo: true, + orden: (gruposDeterminacionesLab?.length || 0) + 1, + determinaciones: [] + }); + } + setIsGroupModalOpen(false); + } catch { + // Error handled by store + } + }; + + const handleToggleGroupActive = async (g: GrupoDeterminacionLaboratorio, e?: React.MouseEvent | React.SyntheticEvent) => { + if (e) e.stopPropagation(); + try { + await actualizarGrupoDeterminacionLab(g.id, { activo: !g.activo }); + toast.success(`Grupo "${g.nombreGrupo}" ${!g.activo ? 'activado' : 'desactivado'}`); + } catch { + // Error handled in store + } + }; + + const handleDeleteGroupConfirm = async () => { + if (!groupToDelete) return; + try { + await eliminarGrupoDeterminacionLab(groupToDelete.id); + setIsDeletingGroup(false); + setGroupToDelete(null); + } catch { + // Error handled in store + } + }; + + // --- Handlers for Determinations --- + const handleOpenNewDet = (g: GrupoDeterminacionLaboratorio, prefillName?: string, prefillClave?: string, prefillUnit?: string) => { + setTargetGroupForDet(g); + setEditingDet(null); + setDetNombre(prefillName || ''); + setDetUnidad(prefillUnit || 'mg/dL'); + setDetRango(''); + const initialClaves = []; + if (prefillName) initialClaves.push(prefillName.toLowerCase().trim()); + if (prefillClave && !initialClaves.includes(prefillClave.toLowerCase().trim())) { + initialClaves.push(prefillClave.toLowerCase().trim()); + } + setDetClaves(initialClaves); + setNuevaClaveInput(''); + setDetEsAdicional(true); + setIsDetModalOpen(true); + }; + + const handleOpenEditDet = (g: GrupoDeterminacionLaboratorio, d: DeterminacionDefinicion) => { + setTargetGroupForDet(g); + setEditingDet(d); + setDetNombre(d.nombre); + setDetUnidad(d.unidad || ''); + setDetRango(d.rangoReferencia || ''); + setDetClaves([...(d.claves || [])]); + setNuevaClaveInput(''); + setDetEsAdicional(d.esAdicional !== false); + setIsDetModalOpen(true); + }; + + const handleAddClave = () => { + const val = nuevaClaveInput.trim().toLowerCase(); + if (!val) return; + if (detClaves.includes(val)) { + toast.error('Esta palabra clave ya está en la lista'); + return; + } + setDetClaves([...detClaves, val]); + setNuevaClaveInput(''); + }; + + const handleRemoveClave = (c: string) => { + setDetClaves(detClaves.filter(item => item !== c)); + }; + + const handleSaveDet = async () => { + if (!targetGroupForDet) return; + if (!detNombre.trim()) { + toast.error('El nombre de la determinación es requerido'); + return; + } + + const finalClaves = [...detClaves]; + if (finalClaves.length === 0) { + finalClaves.push(detNombre.trim().toLowerCase()); + } + + const newDetObj: DeterminacionDefinicion = { + id: editingDet ? editingDet.id : ('det-' + Date.now()), + nombre: detNombre.trim(), + claves: finalClaves, + unidad: detUnidad.trim(), + rangoReferencia: detRango.trim(), + esAdicional: detEsAdicional, + esPrincipal: false + }; + + let updatedDets: DeterminacionDefinicion[] = []; + if (editingDet) { + updatedDets = (targetGroupForDet.determinaciones || []).map(d => d.id === editingDet.id ? newDetObj : d); + } else { + updatedDets = [...(targetGroupForDet.determinaciones || []), newDetObj]; + } + + try { + await actualizarGrupoDeterminacionLab(targetGroupForDet.id, { + determinaciones: updatedDets + }); + setIsDetModalOpen(false); + toast.success(`Determinación "${detNombre.trim()}" guardada en ${targetGroupForDet.nombreGrupo}`); + } catch { + // Handled in store + } + }; + + const handleDeleteDet = async (g: GrupoDeterminacionLaboratorio, detId: string) => { + const updatedDets = (g.determinaciones || []).filter(d => d.id !== detId); + try { + await actualizarGrupoDeterminacionLab(g.id, { + determinaciones: updatedDets + }); + toast.success('Determinación eliminada'); + } catch { + // Handled in store + } + }; + + // Quick action from training simulator unknown line + const handleQuickTrainUnknown = (nombre: string, unidad?: string) => { + if (!gruposDeterminacionesLab || gruposDeterminacionesLab.length === 0) { + toast.error('Primero debe crear un grupo de laboratorio'); + return; + } + // Pick first active group or first group + const targetGrp = gruposDeterminacionesLab.find(g => g.activo) || gruposDeterminacionesLab[0]; + handleOpenNewDet(targetGrp, nombre, nombre.toLowerCase(), unidad); + }; + + return ( +
+ {/* Metric Overview Cards */} +
+ + +
+

Grupos Configurados

+

{totalGrupos}

+

+ {gruposDeterminacionesLab?.filter(g => g.activo).length || 0} activos en el parser +

+
+
+ +
+
+
+ + + +
+

Determinaciones Extras

+

{totalDeterminaciones}

+

+ Parámetros listos para extracción +

+
+
+ +
+
+
+ + + +
+

Sinónimos & Claves Regex

+

{totalClaves}

+

+ Entrenados para reconocimiento +

+
+
+ +
+
+
+
+ + {/* Main Section Header & Controls */} + + +
+
+ + + Entrenamiento y Configuración del Parser de Laboratorio + + + Configure grupos y sinónimos para que el procesador de texto reconozca automáticamente perfiles lipídicos, tiroideos, férricos, enzimáticos o cualquier nueva determinación clínica. + +
+
+ + +
+
+
+ + + {/* Search bar */} +
+ + setSearchTerm(e.target.value)} + className="pl-10 text-sm bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-800" + /> +
+ + {/* List of Groups */} +
+ {filteredGrupos.length === 0 ? ( +
+ +

No se encontraron grupos de determinaciones

+

Haga clic en "Nuevo Grupo" o restablezca los grupos predeterminados.

+ +
+ ) : ( + filteredGrupos.map((grupo) => { + const isExpanded = expandedGroupId === grupo.id || !!searchTerm.trim(); + const detCount = grupo.determinaciones?.length || 0; + + return ( +
+ {/* Group Header Bar */} +
setExpandedGroupId(isExpanded && !searchTerm ? null : grupo.id)} + className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3 cursor-pointer hover:bg-slate-50/80 dark:hover:bg-slate-900/60 rounded-t-xl select-none" + > +
+
+ +
+
+
+

+ {grupo.nombreGrupo} +

+ + {grupo.activo ? 'Activo en Parser' : 'Inactivo'} + + + {detCount} {detCount === 1 ? 'determinación' : 'determinaciones'} + +
+ {grupo.descripcion && ( +

+ {grupo.descripcion} +

+ )} +
+
+ + {/* Group Action Buttons */} +
e.stopPropagation()}> +
+ + handleToggleGroupActive(grupo)} + /> +
+ +
+ + + + + + + +
+
+
+ + {/* Group Determinations Table & Cards (Expanded) */} + {isExpanded && ( +
+ {(!grupo.determinaciones || grupo.determinaciones.length === 0) ? ( +
+

Este grupo no tiene determinaciones configuradas.

+ +
+ ) : ( + <> + {/* Mobile View: Cards for Determinations */} +
+ {grupo.determinaciones.map((det) => ( +
+
+
+
+ {det.nombre} + {det.esAdicional && ( + Adicional + )} +
+
+ Unidad: {det.unidad || '—'} + Ref: {det.rangoReferencia || '—'} +
+
+
+ + +
+
+
+ Sinónimos & Claves: +
+ {(det.claves || []).map((clave, idx) => ( + + {clave} + + ))} +
+
+
+ ))} +
+ + {/* Desktop View: Table for Determinations */} +
+ + + + Determinación / Nombre + Unidad + Referencia + Sinónimos & Claves de Reconocimiento + Acciones + + + + {grupo.determinaciones.map((det) => ( + + +
+ {det.nombre} + {det.esAdicional && ( + + Adicional + + )} +
+
+ + {det.unidad || '—'} + + + {det.rangoReferencia || '—'} + + +
+ {(det.claves || []).map((clave, idx) => ( + + {clave} + + ))} +
+
+ +
+ + +
+
+
+ ))} +
+
+
+ + )} +
+ )} +
+ ); + }) + )} +
+
+
+ + {/* Simulator & Live Parser Training Playground */} + + +
+
+ + + Simulador y Probador de Parser en Tiempo Real + + + Pegue aquí cualquier texto raw copiado de informes de laboratorio o analizadores para verificar cómo el parser extrae cada determinación según las reglas entrenadas. + +
+ + Reconocimiento Reactivo Activo + +
+
+ + +
+ {/* Input Text Area */} +
+
+ + + {rawLabText.split('\n').filter(Boolean).length} líneas detectadas + +
+