From 5b51e4e8eb86d992a9701d25ba01b48cea36bf27 Mon Sep 17 00:00:00 2001 From: AI Studio Date: Sun, 16 Aug 2026 20:40:26 +0000 Subject: [PATCH] Actualizacion de sistema: mejoras en seleccion de categorias de cultivos, optimizacion responsive de listado de cultivos e historia clinica, y correccion de componentes --- server/api-mongodb.js | 47 ++ server/db-mongodb.js | 160 +++++- src/components/ui/button.tsx | 29 +- src/hooks/useHospitalStore.ts | 77 +++ src/index.css | 42 +- src/sections/Cultivos.tsx | 160 +++--- src/sections/Dashboard.tsx | 29 +- src/sections/HistoriaClinica.tsx | 141 ++--- src/sections/Internaciones.tsx | 6 +- src/sections/Layout.tsx | 10 +- src/sections/MapaCamas.tsx | 30 +- src/sections/Pacientes.tsx | 6 +- src/sections/SeccionSistema.tsx | 930 +++++++++++++++++++++++++++---- src/types/index.ts | 9 +- 14 files changed, 1349 insertions(+), 327 deletions(-) diff --git a/server/api-mongodb.js b/server/api-mongodb.js index 4d77b64..a483a20 100644 --- a/server/api-mongodb.js +++ b/server/api-mongodb.js @@ -12,6 +12,7 @@ import { initDb, getAllGlucemias, createGlucemia, updateGlucemia, deleteGlucemia, getAllAcidosBase, createAcidoBase, updateAcidoBase, deleteAcidoBase, getAllCultivos, createCultivo, updateCultivo, deleteCultivo, + getAllTiposCultivo, createTipoCultivo, updateTipoCultivo, deleteTipoCultivo, restablecerTiposCultivo, getAllEstudiosComplementarios, createEstudioComplementario, updateEstudioComplementario, deleteEstudioComplementario, getAllInterconsultas, createInterconsulta, updateInterconsulta, deleteInterconsulta, getAllAtb, createAtb, updateAtb, deleteAtb, @@ -95,6 +96,7 @@ app.get('/api/state', async (req, res) => { glucemias: await getAllGlucemias(), acidosBase: await getAllAcidosBase(), cultivos: await getAllCultivos(), + tiposCultivo: await getAllTiposCultivo(), estudiosComplementarios: await getAllEstudiosComplementarios(), interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })), atb: await getAllAtb(), @@ -580,6 +582,51 @@ app.delete('/api/cultivos/:id', async (req, res) => { } }); +// ========== TIPOS DE CULTIVO ========== +app.get('/api/tipos-cultivo', async (req, res) => { + try { + res.json(await getAllTiposCultivo()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener tipos de cultivo' }); + } +}); + +app.post('/api/tipos-cultivo', async (req, res) => { + try { + const nuevo = await createTipoCultivo(req.body); + res.json(nuevo); + } catch (err) { + res.status(400).json({ error: err.message || 'Error al crear tipo de cultivo' }); + } +}); + +app.put('/api/tipos-cultivo/:id', async (req, res) => { + try { + const updated = await updateTipoCultivo(req.params.id, req.body); + res.json(updated); + } catch (err) { + res.status(400).json({ error: err.message || 'Error al actualizar tipo de cultivo' }); + } +}); + +app.delete('/api/tipos-cultivo/:id', async (req, res) => { + try { + await deleteTipoCultivo(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar tipo de cultivo' }); + } +}); + +app.post('/api/tipos-cultivo/reset', async (req, res) => { + try { + const list = await restablecerTiposCultivo(); + res.json(list); + } catch (err) { + res.status(500).json({ error: 'Error al restablecer tipos de cultivo' }); + } +}); + // ========== ESTUDIOS COMPLEMENTARIOS ========== app.get('/api/estudios-complementarios', async (req, res) => { try { diff --git a/server/db-mongodb.js b/server/db-mongodb.js index b7fe093..a4354e6 100644 --- a/server/db-mongodb.js +++ b/server/db-mongodb.js @@ -66,6 +66,8 @@ const memStore = { indicaciones: [], movimientos_indicaciones: [], pendientes: [], + tipos_cultivo: [], + otrosLaboratorios: [], kv: {} }; @@ -703,6 +705,160 @@ export async function deleteCultivo(id) { } } +// ========== TIPOS DE CULTIVO ========== +export const DEFAULT_TIPOS_CULTIVO = [ + { id: '1', nombre: 'HMCx2', categoria: 'Hemocultivos', descripcion: 'Hemocultivos seriados x2' }, + { id: '2', nombre: 'RC', categoria: 'Catéter', descripcion: 'Retro cultivo / punta de catéter' }, + { id: '3', nombre: 'PC', categoria: 'Punción', descripcion: 'Punción cultivo' }, + { id: '4', nombre: 'UC', categoria: 'Urocultivo', descripcion: 'Urocultivo / muestra de orina' }, + { id: '5', nombre: 'LCR', categoria: 'Líquidos', descripcion: 'Líquido cefalorraquídeo' }, + { id: '6', nombre: 'LP', categoria: 'Líquidos', descripcion: 'Líquido pleural' }, + { id: '7', nombre: 'LAsc', categoria: 'Líquidos', descripcion: 'Líquido ascítico' }, + { id: '8', nombre: 'LAbd', categoria: 'Líquidos', descripcion: 'Líquido abdominal' }, + { id: '9', nombre: 'Coleccion', categoria: 'Líquidos y Colecciones', descripcion: 'Muestra de colección / absceso' }, + { id: '10', nombre: 'Partes Blandas', categoria: 'Tejidos', descripcion: 'Cultivo de partes blandas / tejido' }, + { id: '11', nombre: 'Esputo GC', categoria: 'Respiratorio', descripcion: 'Esputo Germen Común' }, + { id: '12', nombre: 'Esputo TBC', categoria: 'Respiratorio', descripcion: 'Esputo Tuberculosis' }, + { id: '13', nombre: 'Baciloscopia', categoria: 'Respiratorio', descripcion: 'Baciloscopia directa' }, + { id: '14', nombre: 'HNF Test Rápido', categoria: 'Hisopado Nasofaríngeo', descripcion: 'Hisopado nasofaríngeo test rápido' }, + { id: '15', nombre: 'HNF Panel PCR', categoria: 'Hisopado Nasofaríngeo', descripcion: 'Hisopado nasofaríngeo panel PCR virológico' }, + { id: '16', nombre: 'Hisopado Rectal KPC', categoria: 'Vigilancia Epidemiológica', descripcion: 'Hisopado rectal para screening de KPC/BLEE' } +]; + +export async function getAllTiposCultivo() { + let list = []; + if (db) { + const raw = await db.collection('tipos_cultivo').find().toArray(); + list = cleanDocs(raw); + if (list.length === 0) { + // Seed default + for (const item of DEFAULT_TIPOS_CULTIVO) { + await db.collection('tipos_cultivo').insertOne({ ...item }); + } + list = [...DEFAULT_TIPOS_CULTIVO]; + } + // Also read any existing custom tipoMuestra from registered cultivos that might not be in the list + const allCultivos = await db.collection('cultivos').find().toArray(); + const existingNombres = new Set(list.map(t => (t.nombre || '').trim().toLowerCase())); + for (const c of allCultivos) { + if (c.tipoMuestra && c.tipoMuestra.trim() && !existingNombres.has(c.tipoMuestra.trim().toLowerCase())) { + const nuevoTipo = { + id: generateUUID(), + nombre: c.tipoMuestra.trim(), + categoria: 'Personalizado', + descripcion: 'Importado automáticamente desde registro existente de cultivo' + }; + await db.collection('tipos_cultivo').insertOne(nuevoTipo); + list.push(nuevoTipo); + existingNombres.add(c.tipoMuestra.trim().toLowerCase()); + } + } + return list; + } else { + if (!memStore.tipos_cultivo || memStore.tipos_cultivo.length === 0) { + memStore.tipos_cultivo = DEFAULT_TIPOS_CULTIVO.map(item => ({ ...item })); + } + list = [...memStore.tipos_cultivo]; + const existingNombres = new Set(list.map(t => (t.nombre || '').trim().toLowerCase())); + for (const c of (memStore.cultivos || [])) { + if (c.tipoMuestra && c.tipoMuestra.trim() && !existingNombres.has(c.tipoMuestra.trim().toLowerCase())) { + const nuevoTipo = { + id: generateUUID(), + nombre: c.tipoMuestra.trim(), + categoria: 'Personalizado', + descripcion: 'Importado automáticamente desde registro existente de cultivo' + }; + memStore.tipos_cultivo.push(nuevoTipo); + list.push(nuevoTipo); + existingNombres.add(c.tipoMuestra.trim().toLowerCase()); + } + } + return list; + } +} + +export async function createTipoCultivo(tipo) { + const doc = { + id: tipo.id || generateUUID(), + nombre: (tipo.nombre || '').trim(), + categoria: (tipo.categoria || 'General').trim(), + descripcion: (tipo.descripcion || '').trim() + }; + if (!doc.nombre) { + throw new Error('El nombre del tipo de cultivo es requerido'); + } + + if (db) { + const existing = await db.collection('tipos_cultivo').findOne({ nombre: doc.nombre }); + if (existing) { + throw new Error(`Ya existe un tipo de cultivo con el nombre "${doc.nombre}"`); + } + await db.collection('tipos_cultivo').insertOne(doc); + } else { + if (!memStore.tipos_cultivo) memStore.tipos_cultivo = []; + const exists = memStore.tipos_cultivo.some(t => t.nombre.toLowerCase() === doc.nombre.toLowerCase()); + if (exists) { + throw new Error(`Ya existe un tipo de cultivo con el nombre "${doc.nombre}"`); + } + memStore.tipos_cultivo.push(doc); + } + return doc; +} + +export async function updateTipoCultivo(id, datos) { + const updateDoc = {}; + if (datos.nombre !== undefined) updateDoc.nombre = datos.nombre.trim(); + if (datos.categoria !== undefined) updateDoc.categoria = datos.categoria.trim(); + if (datos.descripcion !== undefined) updateDoc.descripcion = datos.descripcion.trim(); + + if (db) { + if (updateDoc.nombre) { + const existing = await db.collection('tipos_cultivo').findOne({ nombre: updateDoc.nombre, id: { $ne: id } }); + if (existing) { + throw new Error(`Ya existe otro tipo de cultivo con el nombre "${updateDoc.nombre}"`); + } + } + await db.collection('tipos_cultivo').updateOne({ id }, { $set: updateDoc }); + } else { + if (!memStore.tipos_cultivo) memStore.tipos_cultivo = []; + if (updateDoc.nombre) { + const exists = memStore.tipos_cultivo.some(t => t.id !== id && t.nombre.toLowerCase() === updateDoc.nombre.toLowerCase()); + if (exists) { + throw new Error(`Ya existe otro tipo de cultivo con el nombre "${updateDoc.nombre}"`); + } + } + const idx = memStore.tipos_cultivo.findIndex(t => t.id === id); + if (idx !== -1) { + memStore.tipos_cultivo[idx] = { ...memStore.tipos_cultivo[idx], ...updateDoc }; + } + } + return { id, ...datos }; +} + +export async function deleteTipoCultivo(id) { + if (db) { + await db.collection('tipos_cultivo').deleteOne({ id }); + } else { + if (memStore.tipos_cultivo) { + memStore.tipos_cultivo = memStore.tipos_cultivo.filter(t => t.id !== id); + } + } + return { success: true }; +} + +export async function restablecerTiposCultivo() { + if (db) { + await db.collection('tipos_cultivo').deleteMany({}); + for (const item of DEFAULT_TIPOS_CULTIVO) { + await db.collection('tipos_cultivo').insertOne({ ...item }); + } + return await getAllTiposCultivo(); + } else { + memStore.tipos_cultivo = DEFAULT_TIPOS_CULTIVO.map(item => ({ ...item })); + return [...memStore.tipos_cultivo]; + } +} + // ========== ESTUDIOS COMPLEMENTARIOS ========== export async function getAllEstudiosComplementarios() { if (db) { @@ -1003,7 +1159,7 @@ export async function exportAllData() { const collections = [ 'usuarios', 'pacientes', 'areas', 'camas', 'internaciones', 'evoluciones', 'laboratorios', 'glucemias', 'acidosbase', - 'cultivos', 'estudiosComplementarios', 'interconsultas', 'atb', + 'cultivos', 'tipos_cultivo', 'estudiosComplementarios', 'interconsultas', 'atb', 'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv' ]; const dump = {}; @@ -1024,7 +1180,7 @@ export async function importAllData(dump) { const collections = [ 'usuarios', 'pacientes', 'areas', 'camas', 'internaciones', 'evoluciones', 'laboratorios', 'glucemias', 'acidosbase', - 'cultivos', 'estudiosComplementarios', 'interconsultas', 'atb', + 'cultivos', 'tipos_cultivo', 'estudiosComplementarios', 'interconsultas', 'atb', 'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv' ]; if (db) { diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 3f5bac6..941fdb3 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -6,28 +6,33 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-150 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-blue-500/40 focus-visible:ring-offset-1 select-none", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: + "bg-gradient-to-b from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 active:from-blue-700 active:to-blue-800 text-white shadow-xs shadow-blue-500/25 border border-blue-600/40 dark:from-blue-600 dark:to-blue-700 dark:hover:from-blue-500 dark:hover:to-blue-600 dark:border-blue-500/50", destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + "bg-rose-600 text-white hover:bg-rose-700 active:bg-rose-800 shadow-xs shadow-rose-600/20 border border-rose-600/30 focus-visible:ring-rose-500/30 dark:bg-rose-600/90 dark:hover:bg-rose-600", outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + "border border-blue-200/90 bg-white/90 text-blue-900 hover:bg-blue-50/90 hover:text-blue-700 hover:border-blue-300 dark:border-blue-800/80 dark:bg-gray-900/80 dark:text-blue-100 dark:hover:bg-blue-950/70 dark:hover:border-blue-700 shadow-2xs", secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", + "bg-blue-50 text-blue-700 hover:bg-blue-100 active:bg-blue-200/80 border border-blue-200/70 dark:bg-blue-950/60 dark:text-blue-200 dark:border-blue-800/80 dark:hover:bg-blue-900/60 shadow-2xs", ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", + "text-slate-700 dark:text-slate-200 hover:bg-blue-50 hover:text-blue-700 dark:hover:bg-blue-950/50 dark:hover:text-blue-300", + link: "text-blue-600 underline-offset-4 hover:underline hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300", + gradient: + "bg-gradient-to-r from-blue-600 via-indigo-600 to-blue-700 hover:from-blue-700 hover:via-indigo-700 hover:to-blue-800 text-white shadow-sm shadow-blue-600/30 border border-blue-400/30", + soft: + "bg-blue-50/80 text-blue-700 hover:bg-blue-100 border border-transparent hover:border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-900/50", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-sm": "size-8", - "icon-lg": "size-10", + sm: "h-8 rounded-md gap-1.5 px-3 text-xs has-[>svg]:px-2.5", + lg: "h-10 rounded-lg px-6 text-base has-[>svg]:px-4", + icon: "size-9 rounded-lg", + "icon-sm": "size-8 rounded-md", + "icon-lg": "size-10 rounded-lg", }, }, defaultVariants: { diff --git a/src/hooks/useHospitalStore.ts b/src/hooks/useHospitalStore.ts index 88c3bed..5060d70 100644 --- a/src/hooks/useHospitalStore.ts +++ b/src/hooks/useHospitalStore.ts @@ -11,6 +11,7 @@ import type { Glucemia, AcidoBase, Cultivo, + TipoCultivo, EstudioComplementario, Interconsulta, ATB, @@ -45,6 +46,7 @@ interface HospitalState { glucemias: Glucemia[]; acidosBase: AcidoBase[]; cultivos: Cultivo[]; + tiposCultivo: TipoCultivo[]; estudiosComplementarios: EstudioComplementario[]; interconsultas: Interconsulta[]; atb: ATB[]; @@ -71,6 +73,7 @@ const defaultState = (): HospitalState => ({ glucemias: [], acidosBase: [], cultivos: [], + tiposCultivo: [], estudiosComplementarios: [], interconsultas: [], atb: [], @@ -104,6 +107,7 @@ export function useHospitalStore() { const normalized = { ...defaults, ...body, + tiposCultivo: body.tiposCultivo || [], estudiosComplementarios: body.estudiosComplementarios || [], interconsultas: body.interconsultas || [], atb: body.atb || [], @@ -992,6 +996,75 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P } }, [state, checkGrupoPermission, apiCall]); + // Acciones de tipos de cultivo + const agregarTipoCultivo = useCallback(async (tipo: Omit) => { + const nuevoTipo: TipoCultivo = { + ...tipo, + id: generateUUID(), + }; + try { + const res = await apiCall('POST', '/tipos-cultivo', nuevoTipo); + const saved = res || nuevoTipo; + setState(prev => ({ + ...prev, + tiposCultivo: [...prev.tiposCultivo.filter(t => t.id !== saved.id), saved], + })); + toast.success(`Tipo de cultivo "${saved.nombre}" creado exitosamente`); + return saved; + } catch (err) { + console.error('Error al agregar tipo de cultivo:', err); + toast.error(err instanceof Error ? err.message : 'Error al agregar tipo de cultivo'); + throw err; + } + }, [apiCall]); + + const actualizarTipoCultivo = useCallback(async (id: string, datos: Partial) => { + try { + await apiCall('PUT', `/tipos-cultivo/${id}`, datos); + setState(prev => ({ + ...prev, + tiposCultivo: prev.tiposCultivo.map(t => t.id === id ? { ...t, ...datos } : t), + })); + toast.success('Tipo de cultivo actualizado exitosamente'); + } catch (err) { + console.error('Error al actualizar tipo de cultivo:', err); + toast.error(err instanceof Error ? err.message : 'Error al actualizar tipo de cultivo'); + throw err; + } + }, [apiCall]); + + const eliminarTipoCultivo = useCallback(async (id: string) => { + try { + await apiCall('DELETE', `/tipos-cultivo/${id}`); + setState(prev => ({ + ...prev, + tiposCultivo: prev.tiposCultivo.filter(t => t.id !== id), + })); + toast.success('Tipo de cultivo eliminado'); + } catch (err) { + console.error('Error al eliminar tipo de cultivo:', err); + toast.error(err instanceof Error ? err.message : 'Error al eliminar tipo de cultivo'); + throw err; + } + }, [apiCall]); + + const restablecerTiposCultivo = useCallback(async () => { + try { + const res = await apiCall('POST', '/tipos-cultivo/reset'); + if (Array.isArray(res)) { + setState(prev => ({ + ...prev, + tiposCultivo: res, + })); + } + toast.success('Tipos de cultivo restablecidos a valores predeterminados'); + } catch (err) { + console.error('Error al restablecer tipos de cultivo:', err); + toast.error('Error al restablecer tipos de cultivo'); + throw err; + } + }, [apiCall]); + // Acciones de estudios complementarios const agregarEstudioComplementario = useCallback(async (estudio: Omit) => { const internacion = state.internaciones.find(i => i.id === estudio.internacionId); @@ -1581,6 +1654,10 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P agregarCultivo, actualizarCultivo, eliminarCultivo, + agregarTipoCultivo, + actualizarTipoCultivo, + eliminarTipoCultivo, + restablecerTiposCultivo, agregarEstudioComplementario, actualizarEstudioComplementario, eliminarEstudioComplementario, diff --git a/src/index.css b/src/index.css index e95e11b..dd76ba6 100644 --- a/src/index.css +++ b/src/index.css @@ -10,28 +10,28 @@ --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; + --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; + --secondary: 214 90% 96%; + --secondary-foreground: 221.2 83.2% 40%; + --muted: 214 32% 95%; --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: 214 90% 96%; + --accent-foreground: 221.2 83.2% 40%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: 214.3 31.8% 90%; + --input: 214.3 31.8% 90%; + --ring: 221.2 83.2% 53.3%; --radius: 0.625rem; - --sidebar-background: 0 0% 98%; + --sidebar-background: 0 0% 99%; --sidebar-foreground: 222.2 84% 4.9%; - --sidebar-primary: 222.2 47.4% 11.2%; + --sidebar-primary: 221.2 83.2% 53.3%; --sidebar-primary-foreground: 210 40% 98%; - --sidebar-accent: 210 40% 96.1%; - --sidebar-accent-foreground: 222.2 47.4% 11.2%; - --sidebar-border: 214.3 31.8% 91.4%; - --sidebar-ring: 217.2 91.2% 59.8%; + --sidebar-accent: 214 90% 96%; + --sidebar-accent-foreground: 221.2 83.2% 40%; + --sidebar-border: 214.3 31.8% 90%; + --sidebar-ring: 221.2 83.2% 53.3%; } .dark { @@ -43,12 +43,12 @@ --popover-foreground: 210 40% 98%; --primary: 217.2 91.2% 59.8%; --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; + --secondary: 217.2 45% 15%; + --secondary-foreground: 213 94% 85%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: 217.2 45% 15%; + --accent-foreground: 213 94% 85%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 20%; @@ -58,8 +58,8 @@ --sidebar-foreground: 210 40% 98%; --sidebar-primary: 217.2 91.2% 59.8%; --sidebar-primary-foreground: 222.2 47.4% 11.2%; - --sidebar-accent: 217.2 32.6% 17.5%; - --sidebar-accent-foreground: 210 40% 98%; + --sidebar-accent: 217.2 45% 15%; + --sidebar-accent-foreground: 213 94% 85%; --sidebar-border: 217.2 32.6% 20%; --sidebar-ring: 217.2 91.2% 59.8%; } diff --git a/src/sections/Cultivos.tsx b/src/sections/Cultivos.tsx index 69a7452..e15df6e 100644 --- a/src/sections/Cultivos.tsx +++ b/src/sections/Cultivos.tsx @@ -94,24 +94,24 @@ export function Cultivos({ -
-
-
- +
+
+
+ setBusqueda(e.target.value)} - className="pl-8" + className="pl-9 w-full bg-white dark:bg-gray-800" />
setFechaToma(e.target.value)} />
setProtocolo(e.target.value)} placeholder="N° Protocolo" />
- setTipoMuestra(v)}> + + + {listaTipos.map((t) => ( + + {t.nombre} + {t.categoria && ({t.categoria})} + + ))} + {tipoMuestra && !listaTipos.some(t => t.nombre === tipoMuestra) && ( + {tipoMuestra} + )}
@@ -3019,7 +3031,7 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE Cargar Resultado Definitivo
-
+
setFechaResultado(e.target.value)} />
setMongoshCommand(e.target.value)} - onKeyDown={handleKeyDown} - disabled={isExecuting} - autoComplete="off" - spellCheck="false" + + + + {/* Search and Filters */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-9" + /> +
+
+ Categoría: + +
+
+ + {/* Table of Tipos */} +
+ + + + Nombre / Código + Categoría + Descripción + Uso en Cultivos + Acciones + + + + {tiposFiltrados.length === 0 ? ( + + + +

No se encontraron tipos de cultivo

+

Pruebe ajustando los filtros de búsqueda o agregue uno nuevo.

+
+
+ ) : ( + tiposFiltrados.map((tipo) => { + const count = usageCountMap[tipo.nombre.trim()] || 0; + return ( + + + + {tipo.nombre} + + + + + {tipo.categoria || 'General'} + + + + {tipo.descripcion || Sin descripción} + + + {count > 0 ? ( + + {count} {count === 1 ? 'cultivo' : 'cultivos'} + + ) : ( + 0 usos + )} + + +
+ + +
+
+
+ ); + }) + )} +
+
+
+
+ + + + {/* TAB 2: CONSOLA MONGOSH & MANTENIMIENTO */} + +
+ + +
+
+ + Consola mongosh + + Ejecuta comandos de consulta directamente en la base de datos. +
+ +
+
+ +
inputRef.current?.focus({ preventScroll: true })} + > + {consoleHistory.length === 0 && ( +
Conectado. Esperando comandos... Ej: db.pacientes.find()
+ )} + {consoleHistory.map((item) => ( +
+ {item.type === 'input' && {'>'}} + {item.type === 'output' && {'<-'}} + {item.type === 'error' && {'!'}} +
{item.content}
+
+ ))} + +
+ {'>'} + {mongoshCommand} + +
+ +
+ + setMongoshCommand(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isExecuting} + autoComplete="off" + spellCheck="false" + /> +
+ +
+ + Soporta asincronía. Variables globales: `db`, `ObjectId`. Flechas arriba/abajo para navegar el historial. +
+ + + + + + + Logs del Servidor + + Visualiza los registros recientes del servidor. + + + + + + + + + + Exportar Datos + + Descarga una copia completa de la base de datos (JSON). + + + + + + + + + + Importar Datos + + Restaura la base de datos desde un archivo (JSON). + + +
+ + +
+

Advertencia: Esto sobreescribirá todos los datos actuales.

+
+
+
+ + + + {/* DIALOG: NUEVO TIPO DE CULTIVO */} + + + + + + Nuevo Tipo de Cultivo / Muestra + + + Ingrese los detalles del nuevo tipo de muestra para registrar cultivos. + + + +
+
+ + setFormNombre(e.target.value)} + required + className="mt-1" + autoFocus />
- -
- - Soporta asincronía. Variables globales: `db`, `ObjectId`. Flechas arriba/abajo para navegar el historial. + +
+
+ + +
+ + {!isCustomCategoriaNew ? ( +
+ +

Agrupa los tipos en el selector de creación de cultivo.

+
+ ) : ( +
+ setFormCategoria(e.target.value)} + required + autoFocus + /> +

Escriba el nombre para crear una nueva categoría.

+
+ )}
- - - - - - Logs del Servidor - - Visualiza los registros recientes del servidor. - - - - - - - - - - Exportar Datos - - Descarga una copia completa de la base de datos (JSON). - - - - - - - - - - Importar Datos - - Restaura la base de datos desde un archivo (JSON). - - -
- + + setFormDescripcion(e.target.value)} + className="mt-1" /> -
+ + + -
-

Advertencia: Esto sobreescribirá todos los datos actuales.

- - -
+ + + + + + {/* DIALOG: EDITAR TIPO DE CULTIVO */} + + + + + + Modificar Tipo de Cultivo + + + Edite el nombre, categoría o descripción de este tipo de cultivo. + + + +
+
+ + setFormNombre(e.target.value)} + required + className="mt-1" + /> +
+ +
+
+ + +
+ + {!isCustomCategoriaEdit ? ( +
+ +

Categoría a la que pertenece este tipo de cultivo.

+
+ ) : ( +
+ setFormCategoria(e.target.value)} + required + autoFocus + /> +

Escriba el nombre de la nueva categoría.

+
+ )} +
+ +
+ + setFormDescripcion(e.target.value)} + className="mt-1" + /> +
+ + + + + +
+
+
+ + {/* DIALOG: ELIMINAR TIPO DE CULTIVO */} + + + + + + Eliminar Tipo de Cultivo + + + ¿Está seguro de que desea eliminar el tipo de cultivo "{selectedTipo?.nombre}"? + + + + {selectedTipo && (usageCountMap[selectedTipo.nombre.trim()] || 0) > 0 && ( +
+ + + Aviso: 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. + +
+ )} + + + + + +
+
+ + {/* DIALOG: RESTABLECER PREDETERMINADOS */} + + + + + + Restablecer Valores Predeterminados + + + Esta acción restaurará el catálogo estándar de tipos de cultivo (HMCx2, RC, UC, LCR, Esputos, Hisopados, etc.). + + + +

+ ¿Desea restablecer los tipos de muestra a los valores predeterminados del hospital? +

+ + + + + +
+
+ + {/* LOGS MODAL */} {isLogsModalOpen && (
diff --git a/src/types/index.ts b/src/types/index.ts index d138d8d..d1cf444 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -159,6 +159,13 @@ export interface AcidoBase { interpretacion?: string; } +export interface TipoCultivo { + id: string; + nombre: string; + categoria?: string; + descripcion?: string; +} + export interface Cultivo { id: string; pacienteId: string; @@ -167,7 +174,7 @@ export interface Cultivo { fechaToma: string; protocolo?: string; fechaResultado?: string; - tipoMuestra: 'HMCx2' | 'RC' | 'PC' | 'UC' | 'LCR' | 'LP' | 'LAsc' | 'LAbd' | 'Coleccion' | 'Partes Blandas' | 'Esputo GC' | 'Esputo TBC' | 'Baciloscopia' | 'HNF Test Rápido' | 'HNF Panel PCR' | 'Hisopado Rectal KPC'; + tipoMuestra: string; germen?: string; sensible?: string; resistente?: string;