Actualizacion de sistema: mejoras en seleccion de categorias de cultivos, optimizacion responsive de listado de cultivos e historia clinica, y correccion de componentes
This commit is contained in:
@@ -12,6 +12,7 @@ import { initDb,
|
|||||||
getAllGlucemias, createGlucemia, updateGlucemia, deleteGlucemia,
|
getAllGlucemias, createGlucemia, updateGlucemia, deleteGlucemia,
|
||||||
getAllAcidosBase, createAcidoBase, updateAcidoBase, deleteAcidoBase,
|
getAllAcidosBase, createAcidoBase, updateAcidoBase, deleteAcidoBase,
|
||||||
getAllCultivos, createCultivo, updateCultivo, deleteCultivo,
|
getAllCultivos, createCultivo, updateCultivo, deleteCultivo,
|
||||||
|
getAllTiposCultivo, createTipoCultivo, updateTipoCultivo, deleteTipoCultivo, restablecerTiposCultivo,
|
||||||
getAllEstudiosComplementarios, createEstudioComplementario, updateEstudioComplementario, deleteEstudioComplementario,
|
getAllEstudiosComplementarios, createEstudioComplementario, updateEstudioComplementario, deleteEstudioComplementario,
|
||||||
getAllInterconsultas, createInterconsulta, updateInterconsulta, deleteInterconsulta,
|
getAllInterconsultas, createInterconsulta, updateInterconsulta, deleteInterconsulta,
|
||||||
getAllAtb, createAtb, updateAtb, deleteAtb,
|
getAllAtb, createAtb, updateAtb, deleteAtb,
|
||||||
@@ -95,6 +96,7 @@ app.get('/api/state', async (req, res) => {
|
|||||||
glucemias: await getAllGlucemias(),
|
glucemias: await getAllGlucemias(),
|
||||||
acidosBase: await getAllAcidosBase(),
|
acidosBase: await getAllAcidosBase(),
|
||||||
cultivos: await getAllCultivos(),
|
cultivos: await getAllCultivos(),
|
||||||
|
tiposCultivo: await getAllTiposCultivo(),
|
||||||
estudiosComplementarios: await getAllEstudiosComplementarios(),
|
estudiosComplementarios: await getAllEstudiosComplementarios(),
|
||||||
interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })),
|
interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })),
|
||||||
atb: await getAllAtb(),
|
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 ==========
|
// ========== ESTUDIOS COMPLEMENTARIOS ==========
|
||||||
app.get('/api/estudios-complementarios', async (req, res) => {
|
app.get('/api/estudios-complementarios', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+158
-2
@@ -66,6 +66,8 @@ const memStore = {
|
|||||||
indicaciones: [],
|
indicaciones: [],
|
||||||
movimientos_indicaciones: [],
|
movimientos_indicaciones: [],
|
||||||
pendientes: [],
|
pendientes: [],
|
||||||
|
tipos_cultivo: [],
|
||||||
|
otrosLaboratorios: [],
|
||||||
kv: {}
|
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 ==========
|
// ========== ESTUDIOS COMPLEMENTARIOS ==========
|
||||||
export async function getAllEstudiosComplementarios() {
|
export async function getAllEstudiosComplementarios() {
|
||||||
if (db) {
|
if (db) {
|
||||||
@@ -1003,7 +1159,7 @@ export async function exportAllData() {
|
|||||||
const collections = [
|
const collections = [
|
||||||
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
||||||
'evoluciones', 'laboratorios', 'glucemias', 'acidosbase',
|
'evoluciones', 'laboratorios', 'glucemias', 'acidosbase',
|
||||||
'cultivos', 'estudiosComplementarios', 'interconsultas', 'atb',
|
'cultivos', 'tipos_cultivo', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||||
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
||||||
];
|
];
|
||||||
const dump = {};
|
const dump = {};
|
||||||
@@ -1024,7 +1180,7 @@ export async function importAllData(dump) {
|
|||||||
const collections = [
|
const collections = [
|
||||||
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
||||||
'evoluciones', 'laboratorios', 'glucemias', 'acidosbase',
|
'evoluciones', 'laboratorios', 'glucemias', 'acidosbase',
|
||||||
'cultivos', 'estudiosComplementarios', 'interconsultas', 'atb',
|
'cultivos', 'tipos_cultivo', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||||
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
||||||
];
|
];
|
||||||
if (db) {
|
if (db) {
|
||||||
|
|||||||
@@ -6,28 +6,33 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
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:
|
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:
|
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:
|
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:
|
ghost:
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"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-primary underline-offset-4 hover:underline",
|
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: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
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",
|
sm: "h-8 rounded-md gap-1.5 px-3 text-xs has-[>svg]:px-2.5",
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
lg: "h-10 rounded-lg px-6 text-base has-[>svg]:px-4",
|
||||||
icon: "size-9",
|
icon: "size-9 rounded-lg",
|
||||||
"icon-sm": "size-8",
|
"icon-sm": "size-8 rounded-md",
|
||||||
"icon-lg": "size-10",
|
"icon-lg": "size-10 rounded-lg",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
Glucemia,
|
Glucemia,
|
||||||
AcidoBase,
|
AcidoBase,
|
||||||
Cultivo,
|
Cultivo,
|
||||||
|
TipoCultivo,
|
||||||
EstudioComplementario,
|
EstudioComplementario,
|
||||||
Interconsulta,
|
Interconsulta,
|
||||||
ATB,
|
ATB,
|
||||||
@@ -45,6 +46,7 @@ interface HospitalState {
|
|||||||
glucemias: Glucemia[];
|
glucemias: Glucemia[];
|
||||||
acidosBase: AcidoBase[];
|
acidosBase: AcidoBase[];
|
||||||
cultivos: Cultivo[];
|
cultivos: Cultivo[];
|
||||||
|
tiposCultivo: TipoCultivo[];
|
||||||
estudiosComplementarios: EstudioComplementario[];
|
estudiosComplementarios: EstudioComplementario[];
|
||||||
interconsultas: Interconsulta[];
|
interconsultas: Interconsulta[];
|
||||||
atb: ATB[];
|
atb: ATB[];
|
||||||
@@ -71,6 +73,7 @@ const defaultState = (): HospitalState => ({
|
|||||||
glucemias: [],
|
glucemias: [],
|
||||||
acidosBase: [],
|
acidosBase: [],
|
||||||
cultivos: [],
|
cultivos: [],
|
||||||
|
tiposCultivo: [],
|
||||||
estudiosComplementarios: [],
|
estudiosComplementarios: [],
|
||||||
interconsultas: [],
|
interconsultas: [],
|
||||||
atb: [],
|
atb: [],
|
||||||
@@ -104,6 +107,7 @@ export function useHospitalStore() {
|
|||||||
const normalized = {
|
const normalized = {
|
||||||
...defaults,
|
...defaults,
|
||||||
...body,
|
...body,
|
||||||
|
tiposCultivo: body.tiposCultivo || [],
|
||||||
estudiosComplementarios: body.estudiosComplementarios || [],
|
estudiosComplementarios: body.estudiosComplementarios || [],
|
||||||
interconsultas: body.interconsultas || [],
|
interconsultas: body.interconsultas || [],
|
||||||
atb: body.atb || [],
|
atb: body.atb || [],
|
||||||
@@ -992,6 +996,75 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
|||||||
}
|
}
|
||||||
}, [state, checkGrupoPermission, apiCall]);
|
}, [state, checkGrupoPermission, apiCall]);
|
||||||
|
|
||||||
|
// Acciones de tipos de cultivo
|
||||||
|
const agregarTipoCultivo = useCallback(async (tipo: Omit<TipoCultivo, 'id'>) => {
|
||||||
|
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<TipoCultivo>) => {
|
||||||
|
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
|
// Acciones de estudios complementarios
|
||||||
const agregarEstudioComplementario = useCallback(async (estudio: Omit<EstudioComplementario, 'id'>) => {
|
const agregarEstudioComplementario = useCallback(async (estudio: Omit<EstudioComplementario, 'id'>) => {
|
||||||
const internacion = state.internaciones.find(i => i.id === estudio.internacionId);
|
const internacion = state.internaciones.find(i => i.id === estudio.internacionId);
|
||||||
@@ -1581,6 +1654,10 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
|||||||
agregarCultivo,
|
agregarCultivo,
|
||||||
actualizarCultivo,
|
actualizarCultivo,
|
||||||
eliminarCultivo,
|
eliminarCultivo,
|
||||||
|
agregarTipoCultivo,
|
||||||
|
actualizarTipoCultivo,
|
||||||
|
eliminarTipoCultivo,
|
||||||
|
restablecerTiposCultivo,
|
||||||
agregarEstudioComplementario,
|
agregarEstudioComplementario,
|
||||||
actualizarEstudioComplementario,
|
actualizarEstudioComplementario,
|
||||||
eliminarEstudioComplementario,
|
eliminarEstudioComplementario,
|
||||||
|
|||||||
+21
-21
@@ -10,28 +10,28 @@
|
|||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--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%;
|
--primary-foreground: 210 40% 98%;
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 214 90% 96%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 221.2 83.2% 40%;
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 214 32% 95%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 214 90% 96%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 221.2 83.2% 40%;
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 214.3 31.8% 90%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 31.8% 90%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 221.2 83.2% 53.3%;
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar-background: 0 0% 98%;
|
--sidebar-background: 0 0% 99%;
|
||||||
--sidebar-foreground: 222.2 84% 4.9%;
|
--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-primary-foreground: 210 40% 98%;
|
||||||
--sidebar-accent: 210 40% 96.1%;
|
--sidebar-accent: 214 90% 96%;
|
||||||
--sidebar-accent-foreground: 222.2 47.4% 11.2%;
|
--sidebar-accent-foreground: 221.2 83.2% 40%;
|
||||||
--sidebar-border: 214.3 31.8% 91.4%;
|
--sidebar-border: 214.3 31.8% 90%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 221.2 83.2% 53.3%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -43,12 +43,12 @@
|
|||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
--primary: 217.2 91.2% 59.8%;
|
--primary: 217.2 91.2% 59.8%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 217.2 45% 15%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 213 94% 85%;
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: 217.2 32.6% 17.5%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: 217.2 45% 15%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 213 94% 85%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
--border: 217.2 32.6% 20%;
|
--border: 217.2 32.6% 20%;
|
||||||
@@ -58,8 +58,8 @@
|
|||||||
--sidebar-foreground: 210 40% 98%;
|
--sidebar-foreground: 210 40% 98%;
|
||||||
--sidebar-primary: 217.2 91.2% 59.8%;
|
--sidebar-primary: 217.2 91.2% 59.8%;
|
||||||
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
|
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
|
||||||
--sidebar-accent: 217.2 32.6% 17.5%;
|
--sidebar-accent: 217.2 45% 15%;
|
||||||
--sidebar-accent-foreground: 210 40% 98%;
|
--sidebar-accent-foreground: 213 94% 85%;
|
||||||
--sidebar-border: 217.2 32.6% 20%;
|
--sidebar-border: 217.2 32.6% 20%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-73
@@ -94,24 +94,24 @@ export function Cultivos({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between">
|
||||||
<div className="flex-1 flex gap-2">
|
<div className="flex-1 flex flex-col sm:flex-row gap-2 w-full">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1 w-full">
|
||||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Buscar por protocolo, germen, paciente..."
|
placeholder="Buscar por protocolo, germen, paciente, DNI..."
|
||||||
value={busqueda}
|
value={busqueda}
|
||||||
onChange={(e) => setBusqueda(e.target.value)}
|
onChange={(e) => setBusqueda(e.target.value)}
|
||||||
className="pl-8"
|
className="pl-9 w-full bg-white dark:bg-gray-800"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select value={filtroEstado} onValueChange={(v) => setFiltroEstado(v as Cultivo['estado'] | 'todos')}>
|
<Select value={filtroEstado} onValueChange={(v) => setFiltroEstado(v as Cultivo['estado'] | 'todos')}>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-full sm:w-[190px] bg-white dark:bg-gray-800 shrink-0">
|
||||||
<SelectValue />
|
<SelectValue placeholder="Filtrar estado" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="todos">Todos</SelectItem>
|
<SelectItem value="todos">Todos los estados</SelectItem>
|
||||||
<SelectItem value="NAF/Pendiente">NAF/Pendiente</SelectItem>
|
<SelectItem value="NAF/Pendiente">NAF / Pendiente</SelectItem>
|
||||||
<SelectItem value="Parcial">Parcial</SelectItem>
|
<SelectItem value="Parcial">Parcial</SelectItem>
|
||||||
<SelectItem value="Positivo">Positivo</SelectItem>
|
<SelectItem value="Positivo">Positivo</SelectItem>
|
||||||
<SelectItem value="Negativo">Negativo</SelectItem>
|
<SelectItem value="Negativo">Negativo</SelectItem>
|
||||||
@@ -126,40 +126,42 @@ export function Cultivos({
|
|||||||
const camaNombre = getCamaNombreForPaciente(cultivo.pacienteId);
|
const camaNombre = getCamaNombreForPaciente(cultivo.pacienteId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={cultivo.id} className="hover:shadow-md transition-shadow">
|
<Card key={cultivo.id} className="hover:shadow-md transition-shadow overflow-hidden">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-3.5 sm:p-5">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3 min-w-0">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2">
|
{/* Card Header Info */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
|
||||||
<div className={`h-10 w-10 rounded-full flex items-center justify-center shrink-0 ${
|
<div className="flex items-start gap-3 min-w-0 flex-1">
|
||||||
cultivo.estado === 'NAF/Pendiente' ? 'bg-amber-100 dark:bg-amber-950/80' :
|
<div className={`h-10 w-10 rounded-full flex items-center justify-center shrink-0 mt-0.5 ${
|
||||||
cultivo.estado === 'Parcial' ? 'bg-orange-100 dark:bg-orange-950/80' :
|
cultivo.estado === 'NAF/Pendiente' ? 'bg-amber-100 text-amber-700 dark:bg-amber-950/80 dark:text-amber-300' :
|
||||||
cultivo.estado === 'Positivo' ? 'bg-red-100 dark:bg-red-950/80' : 'bg-green-100 dark:bg-green-950/80'
|
cultivo.estado === 'Parcial' ? 'bg-orange-100 text-orange-700 dark:bg-orange-950/80 dark:text-orange-300' :
|
||||||
|
cultivo.estado === 'Positivo' ? 'bg-red-100 text-red-700 dark:bg-red-950/80 dark:text-red-300' : 'bg-green-100 text-green-700 dark:bg-green-950/80 dark:text-green-300'
|
||||||
}`}>
|
}`}>
|
||||||
<Microscope className={`h-5 w-5 ${
|
<Microscope className="h-5 w-5 shrink-0" />
|
||||||
cultivo.estado === 'NAF/Pendiente' ? 'text-amber-600 dark:text-amber-400' :
|
|
||||||
cultivo.estado === 'Parcial' ? 'text-orange-600 dark:text-orange-400' :
|
|
||||||
cultivo.estado === 'Positivo' ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'
|
|
||||||
}`} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{camaNombre && (
|
{camaNombre && (
|
||||||
<Badge variant="outline" className="text-xs shrink-0 font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/80 dark:text-blue-300 dark:border-blue-800">
|
<Badge variant="outline" className="text-xs shrink-0 font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/80 dark:text-blue-300 dark:border-blue-800">
|
||||||
{camaNombre}
|
{camaNombre}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<h3 className="font-bold text-sm sm:text-base truncate">
|
<h3 className="font-bold text-sm sm:text-base text-gray-900 dark:text-white break-words">
|
||||||
{pac ? `${pac.apellido}, ${pac.nombre}` : 'Paciente no encontrado'}
|
{pac ? `${pac.apellido}, ${pac.nombre}` : 'Paciente no encontrado'}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm text-gray-500 mt-1">
|
|
||||||
<span className="flex items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2 text-xs sm:text-sm text-gray-500 dark:text-gray-400 mt-1.5">
|
||||||
<Calendar className="h-3 w-3" />
|
<span className="inline-flex items-center gap-1 shrink-0">
|
||||||
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
{cultivo.fechaToma}
|
{cultivo.fechaToma}
|
||||||
</span>
|
</span>
|
||||||
<Badge variant="outline" className="text-xs">{cultivo.tipoMuestra}</Badge>
|
<span className="text-gray-300 dark:text-gray-600 hidden xs:inline">•</span>
|
||||||
<Badge className={`text-xs ${getEstadoColor(cultivo.estado)}`}>
|
<Badge variant="outline" className="text-xs font-normal max-w-full truncate">
|
||||||
|
{cultivo.tipoMuestra}
|
||||||
|
</Badge>
|
||||||
|
<Badge className={`text-xs ${getEstadoColor(cultivo.estado)} font-medium`}>
|
||||||
{getEstadoLabel(cultivo.estado)}
|
{getEstadoLabel(cultivo.estado)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,92 +169,102 @@ export function Cultivos({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Protocolo */}
|
||||||
{cultivo.protocolo && (
|
{cultivo.protocolo && (
|
||||||
<p className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500 dark:text-gray-400 font-mono bg-gray-50 dark:bg-gray-800/60 px-2.5 py-1 rounded w-fit max-w-full break-all">
|
||||||
Protocolo: {cultivo.protocolo}
|
Protocolo: {cultivo.protocolo}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Estado NAF / Pendiente */}
|
||||||
{cultivo.estado === 'NAF/Pendiente' && cultivo.observaciones && (
|
{cultivo.estado === 'NAF/Pendiente' && cultivo.observaciones && (
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 p-2.5 rounded border border-gray-200 dark:border-gray-700">
|
<p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800/80 p-2.5 rounded-lg border border-gray-200 dark:border-gray-700/70 break-words">
|
||||||
<span className="font-semibold">Observaciones:</span> {cultivo.observaciones}
|
<span className="font-semibold text-gray-700 dark:text-gray-200">Observaciones:</span> {cultivo.observaciones}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Estado Parcial */}
|
||||||
{cultivo.estado === 'Parcial' && (
|
{cultivo.estado === 'Parcial' && (
|
||||||
<div className="bg-orange-50 dark:bg-orange-950/60 p-3 rounded-lg border border-orange-200 dark:border-orange-800/50 space-y-2">
|
<div className="bg-orange-50 dark:bg-orange-950/50 p-3 rounded-lg border border-orange-200 dark:border-orange-800/60 space-y-2 text-xs sm:text-sm">
|
||||||
{cultivo.germen && (
|
{cultivo.germen && (
|
||||||
<p className="text-sm font-medium text-orange-800 dark:text-orange-300 flex items-center gap-2">
|
<p className="font-semibold text-orange-800 dark:text-orange-300 flex items-start gap-1.5 break-words">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4 shrink-0 mt-0.5" />
|
||||||
PARCIAL - Germen: {cultivo.germen}
|
<span>PARCIAL - Germen: {cultivo.germen}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{(cultivo.sensible || cultivo.resistente) && (
|
{(cultivo.sensible || cultivo.resistente) && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1.5 pt-1">
|
||||||
{cultivo.sensible && (
|
{cultivo.sensible && (
|
||||||
<p className="text-xs text-orange-700 dark:text-orange-300">Sensible: {cultivo.sensible}</p>
|
<div className="text-orange-900 dark:text-orange-200 bg-orange-100/60 dark:bg-orange-900/40 p-2 rounded break-words">
|
||||||
|
<span className="font-semibold text-orange-800 dark:text-orange-300">Sensible:</span> {cultivo.sensible}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{cultivo.resistente && (
|
{cultivo.resistente && (
|
||||||
<p className="text-xs text-orange-700 dark:text-orange-300">Resistente: {cultivo.resistente}</p>
|
<div className="text-orange-900 dark:text-orange-200 bg-orange-100/60 dark:bg-orange-900/40 p-2 rounded break-words">
|
||||||
|
<span className="font-semibold text-orange-800 dark:text-orange-300">Resistente:</span> {cultivo.resistente}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{cultivo.observaciones && (
|
{cultivo.observaciones && (
|
||||||
<p className="text-xs text-orange-800 dark:text-orange-200 border-t border-orange-200 dark:border-orange-800/60 pt-2 mt-1">
|
<p className="text-xs text-orange-800 dark:text-orange-200 border-t border-orange-200/80 dark:border-orange-800/60 pt-2 break-words">
|
||||||
<span className="font-semibold">Observaciones:</span> {cultivo.observaciones}
|
<span className="font-semibold">Observaciones:</span> {cultivo.observaciones}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Estado Positivo */}
|
||||||
{cultivo.estado === 'Positivo' && (
|
{cultivo.estado === 'Positivo' && (
|
||||||
<div className="flex flex-col gap-2 mt-2">
|
<div className="flex flex-col gap-2 mt-1">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{cultivo.germen && (
|
{cultivo.germen && (
|
||||||
<Badge
|
<div className="border border-red-300 dark:border-red-800/80 bg-red-50 dark:bg-red-950/60 text-red-900 dark:text-red-200 rounded-lg p-2.5 text-xs sm:text-sm flex items-start gap-2 break-words">
|
||||||
variant="outline"
|
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 shrink-0 mt-0.5" />
|
||||||
className="border-red-600 border bg-red-50 text-red-800 dark:bg-red-950/60 dark:text-red-300 dark:border-red-500 rounded px-2.5 py-1 font-medium text-sm inline-flex items-center gap-1.5"
|
<div className="min-w-0 flex-1">
|
||||||
>
|
<span className="font-bold text-red-800 dark:text-red-300">Germen: </span>
|
||||||
<AlertCircle className="h-4 w-4 text-red-600 shrink-0" />
|
<span className="font-medium">{cultivo.germen}</span>
|
||||||
<span>Germen: {cultivo.germen} {cultivo.fechaResultado ? `(Definitivo: ${cultivo.fechaResultado})` : ''}</span>
|
{cultivo.fechaResultado && (
|
||||||
</Badge>
|
<span className="text-xs text-red-700/80 dark:text-red-300/80 block sm:inline sm:ml-2">
|
||||||
|
(Definitivo: {cultivo.fechaResultado})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{cultivo.sensible && (
|
{cultivo.sensible && (
|
||||||
<Badge
|
<div className="border border-green-300 dark:border-green-800/80 bg-green-50 dark:bg-green-950/60 text-green-900 dark:text-green-200 rounded-lg p-2.5 text-xs sm:text-sm break-words">
|
||||||
variant="outline"
|
<span className="font-bold text-green-800 dark:text-green-300">Sensibilidad: </span>
|
||||||
className="border-green-600 border bg-green-50 text-green-800 dark:bg-green-950/60 dark:text-green-300 dark:border-green-500 rounded px-2.5 py-1 font-medium text-sm inline-flex items-center gap-1.5"
|
<span>{cultivo.sensible}</span>
|
||||||
>
|
</div>
|
||||||
<span>Sensibilidad: {cultivo.sensible}</span>
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{cultivo.resistente && (
|
{cultivo.resistente && (
|
||||||
<Badge
|
<div className="border border-red-300 dark:border-red-800/80 bg-red-50/70 dark:bg-red-950/50 text-red-900 dark:text-red-200 rounded-lg p-2.5 text-xs sm:text-sm break-words">
|
||||||
variant="outline"
|
<span className="font-bold text-red-800 dark:text-red-300">Resistencia: </span>
|
||||||
className="border-red-600 border bg-red-50 text-red-800 dark:bg-red-950/60 dark:text-red-300 dark:border-red-500 rounded px-2.5 py-1 font-medium text-sm inline-flex items-center gap-1.5"
|
<span>{cultivo.resistente}</span>
|
||||||
>
|
</div>
|
||||||
<span>Resistencia: {cultivo.resistente}</span>
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cultivo.observaciones && (
|
{cultivo.observaciones && (
|
||||||
<div className="bg-red-50/60 dark:bg-red-950/40 p-2.5 rounded-md border border-red-200/80 dark:border-red-900/50 text-xs text-red-900 dark:text-red-200">
|
<div className="bg-red-50/50 dark:bg-red-950/30 p-2.5 rounded-lg border border-red-200/80 dark:border-red-900/40 text-xs text-red-900 dark:text-red-200 break-words">
|
||||||
<span className="font-semibold">Observaciones:</span> {cultivo.observaciones}
|
<span className="font-semibold">Observaciones:</span> {cultivo.observaciones}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Estado Negativo */}
|
||||||
{cultivo.estado === 'Negativo' && (
|
{cultivo.estado === 'Negativo' && (
|
||||||
<div className="bg-green-50 dark:bg-green-900/60 p-3 rounded-lg border border-green-200 dark:border-green-700 mt-2 space-y-2">
|
<div className="bg-green-50 dark:bg-green-950/50 p-3 rounded-lg border border-green-200 dark:border-green-800/60 mt-1 space-y-1.5 text-xs sm:text-sm">
|
||||||
<p className="text-sm font-medium text-green-800 dark:text-green-200 flex items-center gap-2">
|
<p className="font-semibold text-green-800 dark:text-green-300 flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
<CheckCircle2 className="h-4 w-4 shrink-0" />
|
||||||
Cultivo Negativo
|
Cultivo Negativo Final
|
||||||
</p>
|
</p>
|
||||||
{cultivo.observaciones && (
|
{cultivo.observaciones && (
|
||||||
<p className="text-xs text-green-800 dark:text-green-200 border-t border-green-200 dark:border-green-700/80 pt-2 mt-1">
|
<p className="text-xs text-green-800 dark:text-green-200 border-t border-green-200/80 dark:border-green-800/60 pt-2 break-words">
|
||||||
<span className="font-semibold">Observaciones:</span> {cultivo.observaciones}
|
<span className="font-semibold">Observaciones:</span> {cultivo.observaciones}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -265,9 +277,11 @@ export function Cultivos({
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{cultivosFiltrados.length === 0 && (
|
{cultivosFiltrados.length === 0 && (
|
||||||
<p className="text-center text-gray-500 py-8">
|
<div className="text-center py-12 bg-white dark:bg-gray-800/50 rounded-xl border border-dashed border-gray-300 dark:border-gray-700">
|
||||||
No se encontraron cultivos con los filtros seleccionados
|
<Microscope className="h-10 w-10 text-gray-400 mx-auto mb-2 opacity-60" />
|
||||||
</p>
|
<p className="text-gray-600 dark:text-gray-300 font-medium">No se encontraron cultivos</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">Pruebe ajustando los filtros o el término de búsqueda</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+13
-14
@@ -3,7 +3,6 @@ import {
|
|||||||
Bed,
|
Bed,
|
||||||
Users,
|
Users,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
FlaskConical,
|
|
||||||
Activity,
|
Activity,
|
||||||
Microscope,
|
Microscope,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
@@ -240,35 +239,35 @@ export function Dashboard({
|
|||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-auto py-4 flex flex-col items-center gap-2"
|
className="h-auto py-4 flex flex-col items-center gap-2 group hover:border-blue-400 hover:bg-blue-50/80 dark:hover:bg-blue-950/40"
|
||||||
onClick={() => onCambiarVista('pacientes')}
|
onClick={() => onCambiarVista('pacientes')}
|
||||||
>
|
>
|
||||||
<Users className="h-6 w-6 text-blue-600" />
|
<Users className="h-6 w-6 text-blue-600 dark:text-blue-400 group-hover:scale-110 transition-transform" />
|
||||||
<span className="text-sm">Nuevo Paciente</span>
|
<span className="text-sm font-medium">Nuevo Paciente</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-auto py-4 flex flex-col items-center gap-2"
|
className="h-auto py-4 flex flex-col items-center gap-2 group hover:border-blue-400 hover:bg-blue-50/80 dark:hover:bg-blue-950/40"
|
||||||
onClick={() => onCambiarVista('internaciones')}
|
onClick={() => onCambiarVista('internaciones')}
|
||||||
>
|
>
|
||||||
<ClipboardList className="h-6 w-6 text-purple-600" />
|
<ClipboardList className="h-6 w-6 text-blue-600 dark:text-blue-400 group-hover:scale-110 transition-transform" />
|
||||||
<span className="text-sm">Nueva Internación</span>
|
<span className="text-sm font-medium">Nueva Internación</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-auto py-4 flex flex-col items-center gap-2"
|
className="h-auto py-4 flex flex-col items-center gap-2 group hover:border-blue-400 hover:bg-blue-50/80 dark:hover:bg-blue-950/40"
|
||||||
onClick={() => onCambiarVista('laboratorios')}
|
onClick={() => onCambiarVista('camas')}
|
||||||
>
|
>
|
||||||
<FlaskConical className="h-6 w-6 text-amber-600" />
|
<Bed className="h-6 w-6 text-blue-600 dark:text-blue-400 group-hover:scale-110 transition-transform" />
|
||||||
<span className="text-sm">Laboratorio</span>
|
<span className="text-sm font-medium">Mapa de Camas</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-auto py-4 flex flex-col items-center gap-2"
|
className="h-auto py-4 flex flex-col items-center gap-2 group hover:border-blue-400 hover:bg-blue-50/80 dark:hover:bg-blue-950/40"
|
||||||
onClick={() => onCambiarVista('cultivos')}
|
onClick={() => onCambiarVista('cultivos')}
|
||||||
>
|
>
|
||||||
<Microscope className="h-6 w-6 text-teal-600" />
|
<Microscope className="h-6 w-6 text-blue-600 dark:text-blue-400 group-hover:scale-110 transition-transform" />
|
||||||
<span className="text-sm">Cultivo</span>
|
<span className="text-sm font-medium">Cultivos</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -2840,13 +2840,35 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE
|
|||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
portalNode?: HTMLDivElement | null;
|
portalNode?: HTMLDivElement | null;
|
||||||
}) {
|
}) {
|
||||||
|
const { tiposCultivo } = useHospitalStore();
|
||||||
const [dialog, setDialog] = useState(false);
|
const [dialog, setDialog] = useState(false);
|
||||||
const [resDialog, setResDialog] = useState(false);
|
const [resDialog, setResDialog] = useState(false);
|
||||||
const [editDialog, setEditDialog] = useState(false);
|
const [editDialog, setEditDialog] = useState(false);
|
||||||
const [selected, setSelected] = useState<Cultivo | null>(null);
|
const [selected, setSelected] = useState<Cultivo | null>(null);
|
||||||
const [fechaToma, setFechaToma] = useState(getLocalToday());
|
const [fechaToma, setFechaToma] = useState(getLocalToday());
|
||||||
const [protocolo, setProtocolo] = useState('');
|
const [protocolo, setProtocolo] = useState('');
|
||||||
const [tipoMuestra, setTipoMuestra] = useState<Cultivo['tipoMuestra']>('HMCx2');
|
|
||||||
|
const defaultTipos = [
|
||||||
|
{ id: '1', nombre: 'HMCx2', categoria: 'Hemocultivos' },
|
||||||
|
{ id: '2', nombre: 'RC', categoria: 'Catéter' },
|
||||||
|
{ id: '3', nombre: 'PC', categoria: 'Punción' },
|
||||||
|
{ id: '4', nombre: 'UC', categoria: 'Urocultivo' },
|
||||||
|
{ id: '5', nombre: 'LCR', categoria: 'Líquidos' },
|
||||||
|
{ id: '6', nombre: 'LP', categoria: 'Líquidos' },
|
||||||
|
{ id: '7', nombre: 'LAsc', categoria: 'Líquidos' },
|
||||||
|
{ id: '8', nombre: 'LAbd', categoria: 'Líquidos' },
|
||||||
|
{ id: '9', nombre: 'Coleccion', categoria: 'Líquidos y Colecciones' },
|
||||||
|
{ id: '10', nombre: 'Partes Blandas', categoria: 'Tejidos' },
|
||||||
|
{ id: '11', nombre: 'Esputo GC', categoria: 'Respiratorio' },
|
||||||
|
{ id: '12', nombre: 'Esputo TBC', categoria: 'Respiratorio' },
|
||||||
|
{ id: '13', nombre: 'Baciloscopia', categoria: 'Respiratorio' },
|
||||||
|
{ id: '14', nombre: 'HNF Test Rápido', categoria: 'Hisopado Nasofaríngeo' },
|
||||||
|
{ id: '15', nombre: 'HNF Panel PCR', categoria: 'Hisopado Nasofaríngeo' },
|
||||||
|
{ id: '16', nombre: 'Hisopado Rectal KPC', categoria: 'Vigilancia Epidemiológica' },
|
||||||
|
];
|
||||||
|
const listaTipos = (tiposCultivo && tiposCultivo.length > 0) ? tiposCultivo : defaultTipos;
|
||||||
|
|
||||||
|
const [tipoMuestra, setTipoMuestra] = useState<string>('HMCx2');
|
||||||
const [observaciones, setObservaciones] = useState('');
|
const [observaciones, setObservaciones] = useState('');
|
||||||
const [fechaResultado, setFechaResultado] = useState(getLocalToday());
|
const [fechaResultado, setFechaResultado] = useState(getLocalToday());
|
||||||
const [estadoResultado, setEstadoResultado] = useState<Cultivo['estado']>('Parcial');
|
const [estadoResultado, setEstadoResultado] = useState<Cultivo['estado']>('Parcial');
|
||||||
@@ -2857,7 +2879,7 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE
|
|||||||
const reset = () => {
|
const reset = () => {
|
||||||
setFechaToma(getLocalToday());
|
setFechaToma(getLocalToday());
|
||||||
setProtocolo('');
|
setProtocolo('');
|
||||||
setTipoMuestra('HMCx2');
|
setTipoMuestra(listaTipos[0]?.nombre || 'HMCx2');
|
||||||
setObservaciones('');
|
setObservaciones('');
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
};
|
};
|
||||||
@@ -2960,34 +2982,24 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE
|
|||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader><DialogTitle>Nuevo Cultivo</DialogTitle></DialogHeader>
|
<DialogHeader><DialogTitle>Nuevo Cultivo</DialogTitle></DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||||
<div><Label>Fecha de Toma *</Label><Input type="date" value={fechaToma} onChange={e => setFechaToma(e.target.value)} /></div>
|
<div><Label>Fecha de Toma *</Label><Input type="date" value={fechaToma} onChange={e => setFechaToma(e.target.value)} /></div>
|
||||||
<div><Label>Protocolo</Label><Input value={protocolo} onChange={e => setProtocolo(e.target.value)} placeholder="N° Protocolo" /></div>
|
<div><Label>Protocolo</Label><Input value={protocolo} onChange={e => setProtocolo(e.target.value)} placeholder="N° Protocolo" /></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Tipo de Muestra *</Label>
|
<Label>Tipo de Muestra *</Label>
|
||||||
<Select value={tipoMuestra} onValueChange={(v: Cultivo['tipoMuestra']) => setTipoMuestra(v)}>
|
<Select value={tipoMuestra} onValueChange={(v: string) => setTipoMuestra(v)}>
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
<SelectTrigger><SelectValue placeholder="Seleccione tipo de muestra" /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent className="max-h-72">
|
||||||
<SelectItem value="HMCx2">HMCx2</SelectItem>
|
{listaTipos.map((t) => (
|
||||||
<SelectItem value="RC">RC</SelectItem>
|
<SelectItem key={t.id} value={t.nombre}>
|
||||||
<SelectItem value="PC">PC</SelectItem>
|
<span className="font-medium">{t.nombre}</span>
|
||||||
<SelectItem value="UC">UC</SelectItem>
|
{t.categoria && <span className="text-xs text-gray-500 dark:text-gray-400 ml-2">({t.categoria})</span>}
|
||||||
<SelectItem value="LCR">LCR</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="LP">LP</SelectItem>
|
))}
|
||||||
<SelectItem value="LAsc">LAsc</SelectItem>
|
{tipoMuestra && !listaTipos.some(t => t.nombre === tipoMuestra) && (
|
||||||
<SelectItem value="LAbd">LAbd</SelectItem>
|
<SelectItem value={tipoMuestra}>{tipoMuestra}</SelectItem>
|
||||||
<SelectItem value="Coleccion">Coleccion</SelectItem>
|
)}
|
||||||
<SelectItem value="Partes Blandas">Partes Blandas</SelectItem>
|
|
||||||
<SelectItem value="Esputo GC">Esputo GC</SelectItem>
|
|
||||||
<SelectItem value="Esputo TBC">Esputo TBC</SelectItem>
|
|
||||||
<SelectItem value="Baciloscopia">Baciloscopia</SelectItem>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>HNF</SelectLabel>
|
|
||||||
<SelectItem value="HNF Test Rápido">Test Rápido</SelectItem>
|
|
||||||
<SelectItem value="HNF Panel PCR">Panel PCR</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
<SelectItem value="Hisopado Rectal KPC">Hisopado Rectal KPC</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -3019,7 +3031,7 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE
|
|||||||
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader><DialogTitle>Cargar Resultado Definitivo</DialogTitle></DialogHeader>
|
<DialogHeader><DialogTitle>Cargar Resultado Definitivo</DialogTitle></DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||||
<div><Label>Fecha de Resultado *</Label><Input type="date" value={fechaResultado} onChange={e => setFechaResultado(e.target.value)} /></div>
|
<div><Label>Fecha de Resultado *</Label><Input type="date" value={fechaResultado} onChange={e => setFechaResultado(e.target.value)} /></div>
|
||||||
<div><Label>Resultado *</Label>
|
<div><Label>Resultado *</Label>
|
||||||
<Select value={estadoResultado} onValueChange={(v: Cultivo['estado']) => setEstadoResultado(v)}>
|
<Select value={estadoResultado} onValueChange={(v: Cultivo['estado']) => setEstadoResultado(v)}>
|
||||||
@@ -3049,22 +3061,22 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE
|
|||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
{cs.length === 0 ? <div className="text-center py-8 text-gray-400"><Microscope className="h-12 w-12 mx-auto mb-2 opacity-50" /><p>No hay cultivos</p></div> :
|
{cs.length === 0 ? <div className="text-center py-8 text-gray-400"><Microscope className="h-12 w-12 mx-auto mb-2 opacity-50" /><p>No hay cultivos</p></div> :
|
||||||
cs.map(c => (
|
cs.map(c => (
|
||||||
<Card key={c.id} className="hover:shadow-md transition-shadow"><CardContent className="p-4">
|
<Card key={c.id} className="hover:shadow-md transition-shadow overflow-hidden"><CardContent className="p-3.5 sm:p-4">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3 min-w-0">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-start gap-3 min-w-0 flex-1">
|
||||||
<div className={`h-10 w-10 rounded-full flex items-center justify-center flex-shrink-0 ${c.estado === 'NAF/Pendiente' ? 'bg-amber-100' : c.estado === 'Parcial' ? 'bg-orange-100' : c.estado === 'Positivo' ? 'bg-red-100' : 'bg-green-100'}`}>
|
<div className={`h-10 w-10 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5 ${c.estado === 'NAF/Pendiente' ? 'bg-amber-100' : c.estado === 'Parcial' ? 'bg-orange-100' : c.estado === 'Positivo' ? 'bg-red-100' : 'bg-green-100'}`}>
|
||||||
<Microscope className={`h-5 w-5 ${c.estado === 'NAF/Pendiente' ? 'text-amber-600' : c.estado === 'Parcial' ? 'text-orange-600' : c.estado === 'Positivo' ? 'text-red-600' : 'text-green-600'}`} />
|
<Microscope className={`h-5 w-5 shrink-0 ${c.estado === 'NAF/Pendiente' ? 'text-amber-600' : c.estado === 'Parcial' ? 'text-orange-600' : c.estado === 'Positivo' ? 'text-red-600' : 'text-green-600'}`} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<p className="font-medium">{formatDateDDMMYYYY(c.fechaToma)}</p>
|
<p className="font-semibold text-gray-900 dark:text-white">{formatDateDDMMYYYY(c.fechaToma)}</p>
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2 text-xs sm:text-sm text-gray-500 mt-1">
|
||||||
<Badge variant="outline">{c.tipoMuestra}</Badge>
|
<Badge variant="outline" className="max-w-full truncate">{c.tipoMuestra}</Badge>
|
||||||
<Badge className={getEstadoColor(c.estado)}>{getEstadoLabel(c.estado)}</Badge>
|
<Badge className={getEstadoColor(c.estado)}>{getEstadoLabel(c.estado)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
|
||||||
{c.estado === 'NAF/Pendiente' && (
|
{c.estado === 'NAF/Pendiente' && (
|
||||||
<>
|
<>
|
||||||
<Button size="sm" variant="outline" onClick={() => openParcial(c)}><AlertCircle className="h-4 w-4 mr-1" />Parcial</Button>
|
<Button size="sm" variant="outline" onClick={() => openParcial(c)}><AlertCircle className="h-4 w-4 mr-1" />Parcial</Button>
|
||||||
@@ -3077,79 +3089,68 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE
|
|||||||
<Button size="sm" variant="outline" onClick={() => openDefinitivo(c)}><CheckCircle2 className="h-4 w-4 mr-1" />Definitivo</Button>
|
<Button size="sm" variant="outline" onClick={() => openDefinitivo(c)}><CheckCircle2 className="h-4 w-4 mr-1" />Definitivo</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(c.estado === 'Positivo' || c.estado === 'Negativo') && <Button size="sm" variant="outline" onClick={() => openDefinitivo(c)}><Pencil className="h-4 w-4" /></Button>}
|
{(c.estado === 'Positivo' || c.estado === 'Negativo') && <Button size="sm" variant="outline" onClick={() => openDefinitivo(c)}><Pencil className="h-4 w-4 mr-1" />Editar</Button>}
|
||||||
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(c.id)}><Trash2 /></Button>
|
<Button size="sm" variant="outline" className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/40" onClick={() => del(c.id)}><Trash2 className="h-4 w-4" /></Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{c.protocolo && <p className="text-xs text-gray-500 dark:text-gray-400">Protocolo: {c.protocolo}</p>}
|
{c.protocolo && <p className="text-xs text-gray-500 dark:text-gray-400 font-mono break-all">Protocolo: {c.protocolo}</p>}
|
||||||
{c.fechaResultado && (c.estado === 'Positivo' || c.estado === 'Negativo' || c.estado === 'Parcial') && (
|
{c.fechaResultado && (c.estado === 'Positivo' || c.estado === 'Negativo' || c.estado === 'Parcial') && (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{c.estado === 'Parcial' ? 'Fecha Resultado Parcial' : 'Fecha Definitivo'}: {formatDateDDMMYYYY(c.fechaResultado)}
|
{c.estado === 'Parcial' ? 'Fecha Resultado Parcial' : 'Fecha Definitivo'}: {formatDateDDMMYYYY(c.fechaResultado)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{c.estado === 'NAF/Pendiente' && c.observaciones && (
|
{c.estado === 'NAF/Pendiente' && c.observaciones && (
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 p-2.5 rounded border border-gray-200 dark:border-gray-700">
|
<p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 p-2.5 rounded border border-gray-200 dark:border-gray-700 break-words">
|
||||||
<span className="font-semibold">Observaciones:</span> {c.observaciones}
|
<span className="font-semibold">Observaciones:</span> {c.observaciones}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{c.estado === 'Parcial' && (
|
{c.estado === 'Parcial' && (
|
||||||
<div className="bg-orange-50 dark:bg-orange-950/60 p-3 rounded-lg border border-orange-200 dark:border-orange-800/50 space-y-2">
|
<div className="bg-orange-50 dark:bg-orange-950/60 p-3 rounded-lg border border-orange-200 dark:border-orange-800/50 space-y-2 text-xs sm:text-sm">
|
||||||
{c.germen && (
|
{c.germen && (
|
||||||
<p className="text-sm font-medium text-orange-800 dark:text-orange-300 flex items-center gap-2">
|
<p className="font-semibold text-orange-800 dark:text-orange-300 flex items-start gap-1.5 break-words">
|
||||||
<AlertCircle className="h-4 w-4" />PARCIAL - Germen: {c.germen}
|
<AlertCircle className="h-4 w-4 shrink-0 mt-0.5" /><span>PARCIAL - Germen: {c.germen}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{(c.sensible || c.resistente) && (
|
{(c.sensible || c.resistente) && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{c.sensible && <p className="text-xs text-orange-700 dark:text-orange-300">Sensible: {c.sensible}</p>}
|
{c.sensible && <p className="text-xs text-orange-700 dark:text-orange-300 break-words">Sensible: {c.sensible}</p>}
|
||||||
{c.resistente && <p className="text-xs text-orange-700 dark:text-orange-300">Resistente: {c.resistente}</p>}
|
{c.resistente && <p className="text-xs text-orange-700 dark:text-orange-300 break-words">Resistente: {c.resistente}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{c.observaciones && (
|
{c.observaciones && (
|
||||||
<p className="text-xs text-orange-800 dark:text-orange-200 border-t border-orange-200 dark:border-orange-800/60 pt-2 mt-1">
|
<p className="text-xs text-orange-800 dark:text-orange-200 border-t border-orange-200 dark:border-orange-800/60 pt-2 mt-1 break-words">
|
||||||
<span className="font-semibold">Observaciones:</span> {c.observaciones}
|
<span className="font-semibold">Observaciones:</span> {c.observaciones}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{c.estado === 'Positivo' && (
|
{c.estado === 'Positivo' && (
|
||||||
<div className="flex flex-col gap-2 mt-2">
|
<div className="flex flex-col gap-2 mt-1">
|
||||||
{c.germen && (
|
{c.germen && (
|
||||||
<div>
|
<div className="border-2 border-red-600 bg-red-50 text-red-800 dark:bg-red-950/60 dark:text-red-300 dark:border-red-500 rounded-lg p-2.5 font-medium text-xs sm:text-sm flex items-start gap-1.5 break-words">
|
||||||
<Badge
|
<AlertCircle className="h-4 w-4 text-red-600 shrink-0 mt-0.5" />
|
||||||
variant="outline"
|
|
||||||
className="border-solid border-2 border-red-600 bg-red-50 text-red-800 dark:bg-red-950/60 dark:text-red-300 dark:border-red-500 rounded px-2.5 py-1 font-semibold text-sm inline-flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<AlertCircle className="h-4 w-4 text-red-600 shrink-0" />
|
|
||||||
<span>Germen: {c.germen}</span>
|
<span>Germen: {c.germen}</span>
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(c.sensible || c.resistente) && (
|
{(c.sensible || c.resistente) && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-col gap-1.5">
|
||||||
{c.sensible && (
|
{c.sensible && (
|
||||||
<Badge
|
<div className="border border-green-600 bg-green-50 text-green-800 dark:bg-green-950/60 dark:text-green-300 dark:border-green-500 rounded-lg p-2 text-xs sm:text-sm break-words">
|
||||||
variant="outline"
|
<span className="font-semibold">Sensibilidad:</span> {c.sensible}
|
||||||
className="border-green-600 border bg-green-50 text-green-800 dark:bg-green-950/60 dark:text-green-300 dark:border-green-500 rounded px-2.5 py-1 font-medium text-sm inline-flex items-center gap-1.5"
|
</div>
|
||||||
>
|
|
||||||
<span>Sensibilidad: {c.sensible}</span>
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{c.resistente && (
|
{c.resistente && (
|
||||||
<Badge
|
<div className="border border-red-600 bg-red-50 text-red-800 dark:bg-red-950/60 dark:text-red-300 dark:border-red-500 rounded-lg p-2 text-xs sm:text-sm break-words">
|
||||||
variant="outline"
|
<span className="font-semibold">Resistencia:</span> {c.resistente}
|
||||||
className="border-red-600 border bg-red-50 text-red-800 dark:bg-red-950/60 dark:text-red-300 dark:border-red-500 rounded px-2.5 py-1 font-medium text-sm inline-flex items-center gap-1.5"
|
</div>
|
||||||
>
|
|
||||||
<span>Resistencia: {c.resistente}</span>
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{c.observaciones && (
|
{c.observaciones && (
|
||||||
<div className="bg-red-50/60 dark:bg-red-950/40 p-2.5 rounded-md border border-red-200/80 dark:border-red-900/50 text-xs text-red-900 dark:text-red-200">
|
<div className="bg-red-50/60 dark:bg-red-950/40 p-2.5 rounded-md border border-red-200/80 dark:border-red-900/50 text-xs text-red-900 dark:text-red-200 break-words">
|
||||||
<span className="font-semibold">Observaciones:</span> {c.observaciones}
|
<span className="font-semibold">Observaciones:</span> {c.observaciones}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export function Internaciones({
|
|||||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||||
<Dialog open={dialogoNuevaAbierto} onOpenChange={setDialogoNuevaAbierto}>
|
<Dialog open={dialogoNuevaAbierto} onOpenChange={setDialogoNuevaAbierto}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" onClick={() => onNuevoIngreso ? onNuevoIngreso() : setDialogoNuevaAbierto(true)}>
|
<Button onClick={() => onNuevoIngreso ? onNuevoIngreso() : setDialogoNuevaAbierto(true)}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Nuevo Ingreso
|
Nuevo Ingreso
|
||||||
</Button>
|
</Button>
|
||||||
@@ -361,7 +361,7 @@ export function Internaciones({
|
|||||||
placeholder="Nombre del médico ingresante"
|
placeholder="Nombre del médico ingresante"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline"
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={handleIniciarInternacion}
|
onClick={handleIniciarInternacion}
|
||||||
disabled={!pacienteSeleccionado || !camaSeleccionada || !motivoConsulta || !enfermedadActual || !effectiveMedico}
|
disabled={!pacienteSeleccionado || !camaSeleccionada || !motivoConsulta || !enfermedadActual || !effectiveMedico}
|
||||||
@@ -693,7 +693,7 @@ export function Internaciones({
|
|||||||
placeholder="Ingrese el diagnóstico de egreso..."
|
placeholder="Ingrese el diagnóstico de egreso..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline"
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={handleFinalizarInternacion}
|
onClick={handleFinalizarInternacion}
|
||||||
disabled={!fechaEgreso || !diagnosticoEgreso || !motivoEgreso || (motivoEgreso === 'Pase servicio' && !servicioAlQuePasa.trim())}
|
disabled={!fechaEgreso || !diagnosticoEgreso || !motivoEgreso || (motivoEgreso === 'Pase servicio' && !servicioAlQuePasa.trim())}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderNavContent = (onItemClick?: () => void) => (
|
const renderNavContent = (onItemClick?: () => void) => (
|
||||||
<nav className="flex flex-col gap-2">
|
<nav className="flex flex-col gap-1.5">
|
||||||
{filteredMenuItems.map((item) => {
|
{filteredMenuItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = vistaActual === item.vista;
|
const isActive = vistaActual === item.vista;
|
||||||
@@ -72,13 +72,17 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
|
|||||||
<Button
|
<Button
|
||||||
key={item.vista}
|
key={item.vista}
|
||||||
variant={isActive ? 'default' : 'ghost'}
|
variant={isActive ? 'default' : 'ghost'}
|
||||||
className={`justify-start gap-3 ${isActive ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-200'}`}
|
className={`justify-start gap-3 h-10 px-3.5 rounded-lg transition-all duration-150 ${
|
||||||
|
isActive
|
||||||
|
? 'shadow-xs font-semibold'
|
||||||
|
: 'text-slate-600 dark:text-slate-300 hover:text-blue-700 dark:hover:text-blue-300 hover:bg-blue-50/80 dark:hover:bg-blue-950/40 font-medium'
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onCambiarVista(item.vista);
|
onCambiarVista(item.vista);
|
||||||
if (onItemClick) onItemClick();
|
if (onItemClick) onItemClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className={`h-5 w-5 ${isActive ? 'text-white' : 'text-blue-600/70 dark:text-blue-400'}`} />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
+18
-12
@@ -158,24 +158,24 @@ export function MapaCamas({
|
|||||||
</Badge>
|
</Badge>
|
||||||
<div className="ml-0 sm:ml-4 flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
<div className="ml-0 sm:ml-4 flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
||||||
{currentUser?.rol === 'admin' && (
|
{currentUser?.rol === 'admin' && (
|
||||||
<Button variant="outline" onClick={() => {
|
<Button variant="secondary" onClick={() => {
|
||||||
setEditingGrupoId(null);
|
setEditingGrupoId(null);
|
||||||
setGrupoNombre('');
|
setGrupoNombre('');
|
||||||
setGrupoDialogOpen(true);
|
setGrupoDialogOpen(true);
|
||||||
}} className="text-xs px-2 py-1 sm:text-sm sm:px-4 sm:py-2">
|
}} className="text-xs px-2.5 py-1.5 sm:text-sm sm:px-4 sm:py-2">
|
||||||
Administrar Grupos
|
Administrar Grupos
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{currentUser?.rol === 'admin' && (
|
{currentUser?.rol === 'admin' && (
|
||||||
<Button variant="outline" onClick={() => {
|
<Button onClick={() => {
|
||||||
setEditingBed(null);
|
setEditingBed(null);
|
||||||
setBedNumero('');
|
setBedNumero('');
|
||||||
setBedTipo('General');
|
setBedTipo('General');
|
||||||
setBedGrupoId(grupos?.[0]?.id);
|
setBedGrupoId(grupos?.[0]?.id);
|
||||||
setBedDialogOpen(true);
|
setBedDialogOpen(true);
|
||||||
}} className="text-xs px-2 py-1 sm:text-sm sm:px-4 sm:py-2">
|
}} className="text-xs px-2.5 py-1.5 sm:text-sm sm:px-4 sm:py-2">
|
||||||
<Plus className="h-3 w-3 sm:h-4 sm:w-4 sm:mr-1" />
|
<Plus className="h-3.5 w-3.5 sm:h-4 sm:w-4 mr-1" />
|
||||||
<span className="hidden sm:inline">Agregar Cama</span>
|
<span>Agregar Cama</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -480,15 +480,15 @@ export function MapaCamas({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button size="sm" variant="outline" onClick={() => setGrupoDialogOpen(false)}>
|
<Button size="sm" variant="outline" onClick={() => setGrupoDialogOpen(false)}>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4 mr-1" /> Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="outline" onClick={() => {
|
<Button size="sm" onClick={() => {
|
||||||
if (!grupoNombre.trim()) return alert('Nombre requerido');
|
if (!grupoNombre.trim()) return alert('Nombre requerido');
|
||||||
if (editingGrupoId) onActualizarGrupo(editingGrupoId, { nombre: grupoNombre });
|
if (editingGrupoId) onActualizarGrupo(editingGrupoId, { nombre: grupoNombre });
|
||||||
else onAgregarGrupo({ nombre: grupoNombre });
|
else onAgregarGrupo({ nombre: grupoNombre });
|
||||||
setGrupoDialogOpen(false);
|
setGrupoDialogOpen(false);
|
||||||
}}>
|
}}>
|
||||||
<Save className="h-4 w-4" />
|
<Save className="h-4 w-4 mr-1" /> Guardar
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -569,8 +569,11 @@ export function MapaCamas({
|
|||||||
Eliminar
|
Eliminar
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button variant="outline" onClick={() => setBedDialogOpen(false)}><X className="h-4 w-4 mr-2" />Cancelar</Button>
|
<Button variant="outline" onClick={() => setBedDialogOpen(false)}>
|
||||||
<Button variant="outline" onClick={() => {
|
<X className="h-4 w-4 mr-2" />
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
if (!bedNumero.trim()) return alert('Número requerido');
|
if (!bedNumero.trim()) return alert('Número requerido');
|
||||||
if (editingBed) {
|
if (editingBed) {
|
||||||
onActualizarCama(editingBed.id, { numero: bedNumero, tipo: bedTipo, grupoId: bedGrupoId });
|
onActualizarCama(editingBed.id, { numero: bedNumero, tipo: bedTipo, grupoId: bedGrupoId });
|
||||||
@@ -578,7 +581,10 @@ export function MapaCamas({
|
|||||||
onAgregarCama({ numero: bedNumero, tipo: bedTipo, estado: 'Disponible', grupoId: bedGrupoId });
|
onAgregarCama({ numero: bedNumero, tipo: bedTipo, estado: 'Disponible', grupoId: bedGrupoId });
|
||||||
}
|
}
|
||||||
setBedDialogOpen(false);
|
setBedDialogOpen(false);
|
||||||
}}>Guardar</Button>
|
}}>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export function Pacientes({
|
|||||||
</div>
|
</div>
|
||||||
<Dialog open={dialogoAbierto} onOpenChange={setDialogoAbierto}>
|
<Dialog open={dialogoAbierto} onOpenChange={setDialogoAbierto}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" onClick={resetFormulario}>
|
<Button onClick={resetFormulario}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Nuevo Paciente
|
Nuevo Paciente
|
||||||
</Button>
|
</Button>
|
||||||
@@ -307,7 +307,7 @@ export function Pacientes({
|
|||||||
<X className="h-4 w-4 mr-2" />
|
<X className="h-4 w-4 mr-2" />
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline"
|
<Button
|
||||||
onClick={handleGuardar}
|
onClick={handleGuardar}
|
||||||
disabled={!nombre || !apellido || !dni || !fechaNacimiento}
|
disabled={!nombre || !apellido || !dni || !fechaNacimiento}
|
||||||
>
|
>
|
||||||
@@ -489,7 +489,7 @@ export function Pacientes({
|
|||||||
{busqueda ? 'No se encontraron pacientes con esa búsqueda' : 'No hay pacientes registrados'}
|
{busqueda ? 'No se encontraron pacientes con esa búsqueda' : 'No hay pacientes registrados'}
|
||||||
</p>
|
</p>
|
||||||
{!busqueda && (
|
{!busqueda && (
|
||||||
<Button variant="outline" className="mt-4" onClick={() => setDialogoAbierto(true)}>
|
<Button className="mt-4" onClick={() => setDialogoAbierto(true)}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Agregar primer paciente
|
Agregar primer paciente
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,10 +1,65 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Terminal, Download, Upload, FileText, AlertCircle, RefreshCw } from 'lucide-react';
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGroup, SelectLabel } from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Terminal,
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
FileText,
|
||||||
|
AlertCircle,
|
||||||
|
RefreshCw,
|
||||||
|
Microscope,
|
||||||
|
Plus,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
Search,
|
||||||
|
CheckCircle2,
|
||||||
|
Tag,
|
||||||
|
Info,
|
||||||
|
RotateCcw
|
||||||
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||||
|
import type { TipoCultivo } from '@/types';
|
||||||
|
|
||||||
export function SeccionSistema() {
|
export function SeccionSistema() {
|
||||||
|
const {
|
||||||
|
tiposCultivo,
|
||||||
|
cultivos,
|
||||||
|
agregarTipoCultivo,
|
||||||
|
actualizarTipoCultivo,
|
||||||
|
eliminarTipoCultivo,
|
||||||
|
restablecerTiposCultivo,
|
||||||
|
refreshState
|
||||||
|
} = useHospitalStore();
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState('cultivos');
|
||||||
|
|
||||||
|
// Tipos de Cultivo Management State
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [selectedCategoria, setSelectedCategoria] = useState<string>('todas');
|
||||||
|
const [isNewDialogOpen, setIsNewDialogOpen] = useState(false);
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
|
||||||
|
const [selectedTipo, setSelectedTipo] = useState<TipoCultivo | null>(null);
|
||||||
|
|
||||||
|
// Form states
|
||||||
|
const [formNombre, setFormNombre] = useState('');
|
||||||
|
const [formCategoria, setFormCategoria] = useState('General');
|
||||||
|
const [formDescripcion, setFormDescripcion] = useState('');
|
||||||
|
const [isCustomCategoriaNew, setIsCustomCategoriaNew] = useState(false);
|
||||||
|
const [isCustomCategoriaEdit, setIsCustomCategoriaEdit] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Mongosh console states
|
||||||
const [mongoshCommand, setMongoshCommand] = useState('');
|
const [mongoshCommand, setMongoshCommand] = useState('');
|
||||||
const [consoleHistory, setConsoleHistory] = useState<{ id: number; type: 'input' | 'output' | 'error'; content: string }[]>([]);
|
const [consoleHistory, setConsoleHistory] = useState<{ id: number; type: 'input' | 'output' | 'error'; content: string }[]>([]);
|
||||||
const [logs, setLogs] = useState<string>('');
|
const [logs, setLogs] = useState<string>('');
|
||||||
@@ -27,6 +82,178 @@ export function SeccionSistema() {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [consoleHistory, mongoshCommand]);
|
}, [consoleHistory, mongoshCommand]);
|
||||||
|
|
||||||
|
// Categories list
|
||||||
|
const categoriasDisponibles = useMemo(() => {
|
||||||
|
const set = new Set<string>();
|
||||||
|
const defaultCats = [
|
||||||
|
'Hemocultivos',
|
||||||
|
'Catéter',
|
||||||
|
'Punción',
|
||||||
|
'Urocultivo',
|
||||||
|
'Líquidos y Colecciones',
|
||||||
|
'Tejidos',
|
||||||
|
'Respiratorio',
|
||||||
|
'Hisopado Nasofaríngeo',
|
||||||
|
'Vigilancia Epidemiológica',
|
||||||
|
'General'
|
||||||
|
];
|
||||||
|
defaultCats.forEach(c => set.add(c));
|
||||||
|
(tiposCultivo || []).forEach(t => {
|
||||||
|
if (t.categoria && t.categoria.trim()) {
|
||||||
|
set.add(t.categoria.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(set).sort((a, b) => a.localeCompare(b));
|
||||||
|
}, [tiposCultivo]);
|
||||||
|
|
||||||
|
// Count usage of each tipo in registered cultivos
|
||||||
|
const usageCountMap = useMemo(() => {
|
||||||
|
const map: Record<string, number> = {};
|
||||||
|
(cultivos || []).forEach(c => {
|
||||||
|
if (c.tipoMuestra) {
|
||||||
|
const key = c.tipoMuestra.trim();
|
||||||
|
map[key] = (map[key] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [cultivos]);
|
||||||
|
|
||||||
|
// Filtered Tipos
|
||||||
|
const tiposFiltrados = useMemo(() => {
|
||||||
|
const list = tiposCultivo || [];
|
||||||
|
return list.filter(tipo => {
|
||||||
|
const matchesSearch =
|
||||||
|
tipo.nombre.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(tipo.categoria && tipo.categoria.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||||
|
(tipo.descripcion && tipo.descripcion.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
|
|
||||||
|
const matchesCategory =
|
||||||
|
selectedCategoria === 'todas' || (tipo.categoria || 'General') === selectedCategoria;
|
||||||
|
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
}).sort((a, b) => {
|
||||||
|
const catCompare = (a.categoria || 'General').localeCompare(b.categoria || 'General');
|
||||||
|
if (catCompare !== 0) return catCompare;
|
||||||
|
return a.nombre.localeCompare(b.nombre);
|
||||||
|
});
|
||||||
|
}, [tiposCultivo, searchTerm, selectedCategoria]);
|
||||||
|
|
||||||
|
// Open Add Dialog
|
||||||
|
const handleOpenNewDialog = () => {
|
||||||
|
setFormNombre('');
|
||||||
|
const defaultCat = selectedCategoria !== 'todas' ? selectedCategoria : (categoriasDisponibles[0] || 'General');
|
||||||
|
setFormCategoria(defaultCat);
|
||||||
|
setFormDescripcion('');
|
||||||
|
setIsCustomCategoriaNew(false);
|
||||||
|
setIsNewDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open Edit Dialog
|
||||||
|
const handleOpenEditDialog = (tipo: TipoCultivo) => {
|
||||||
|
setSelectedTipo(tipo);
|
||||||
|
setFormNombre(tipo.nombre);
|
||||||
|
const cat = tipo.categoria && tipo.categoria.trim() ? tipo.categoria.trim() : 'General';
|
||||||
|
setFormCategoria(cat);
|
||||||
|
setFormDescripcion(tipo.descripcion || '');
|
||||||
|
setIsCustomCategoriaEdit(false);
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open Delete Dialog
|
||||||
|
const handleOpenDeleteDialog = (tipo: TipoCultivo) => {
|
||||||
|
setSelectedTipo(tipo);
|
||||||
|
setIsDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit New Tipo
|
||||||
|
const handleCreateTipo = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!formNombre.trim()) {
|
||||||
|
toast.error('El nombre del tipo de cultivo es obligatorio');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exists = (tiposCultivo || []).some(
|
||||||
|
t => t.nombre.trim().toLowerCase() === formNombre.trim().toLowerCase()
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
|
toast.error(`Ya existe un tipo de cultivo con el nombre "${formNombre.trim()}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await agregarTipoCultivo({
|
||||||
|
nombre: formNombre.trim(),
|
||||||
|
categoria: formCategoria.trim() || 'General',
|
||||||
|
descripcion: formDescripcion.trim()
|
||||||
|
});
|
||||||
|
setIsNewDialogOpen(false);
|
||||||
|
} catch {
|
||||||
|
// Handled in hook
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit Edit Tipo
|
||||||
|
const handleUpdateTipo = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!selectedTipo) return;
|
||||||
|
if (!formNombre.trim()) {
|
||||||
|
toast.error('El nombre del tipo de cultivo es obligatorio');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exists = (tiposCultivo || []).some(
|
||||||
|
t => t.id !== selectedTipo.id && t.nombre.trim().toLowerCase() === formNombre.trim().toLowerCase()
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
|
toast.error(`Ya existe otro tipo de cultivo con el nombre "${formNombre.trim()}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await actualizarTipoCultivo(selectedTipo.id, {
|
||||||
|
nombre: formNombre.trim(),
|
||||||
|
categoria: formCategoria.trim() || 'General',
|
||||||
|
descripcion: formDescripcion.trim()
|
||||||
|
});
|
||||||
|
setIsEditDialogOpen(false);
|
||||||
|
} catch {
|
||||||
|
// Handled in hook
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit Delete Tipo
|
||||||
|
const handleDeleteTipo = async () => {
|
||||||
|
if (!selectedTipo) return;
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await eliminarTipoCultivo(selectedTipo.id);
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
} catch {
|
||||||
|
// Handled in hook
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit Reset Tipos
|
||||||
|
const handleResetTipos = async () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await restablecerTiposCultivo();
|
||||||
|
setIsResetDialogOpen(false);
|
||||||
|
} catch {
|
||||||
|
// Handled in hook
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mongosh console functions
|
||||||
const addHistory = (type: 'input' | 'output' | 'error', content: string) => {
|
const addHistory = (type: 'input' | 'output' | 'error', content: string) => {
|
||||||
setConsoleHistory(prev => [...prev, { id: historyCounter.current++, type, content }]);
|
setConsoleHistory(prev => [...prev, { id: historyCounter.current++, type, content }]);
|
||||||
};
|
};
|
||||||
@@ -132,14 +359,14 @@ export function SeccionSistema() {
|
|||||||
throw new Error(data.error || 'Error al importar base de datos');
|
throw new Error(data.error || 'Error al importar base de datos');
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success('Base de datos importada exitosamente. Recarga la página para ver los cambios.');
|
toast.success('Base de datos importada exitosamente.');
|
||||||
// Opcional: window.location.reload();
|
await refreshState();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Error: ' + String(error));
|
toast.error('Error: ' + String(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
e.target.value = ''; // Reset input
|
e.target.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearConsole = () => {
|
const handleClearConsole = () => {
|
||||||
@@ -148,13 +375,201 @@ export function SeccionSistema() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Sistema y Depuración</h2>
|
<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">Herramientas avanzadas para administradores</p>
|
<p className="text-gray-500 dark:text-gray-400">Administración de parámetros clínicos, base de datos y depuración</p>
|
||||||
</div>
|
</div>
|
||||||
</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
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="mantenimiento" className="flex items-center gap-2">
|
||||||
|
<Terminal className="h-4 w-4" /> Consola & Mantenimiento
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* TAB 1: GESTIÓN DE TIPOS DE CULTIVOS */}
|
||||||
|
<TabsContent value="cultivos" className="space-y-6">
|
||||||
|
{/* Header Card with Metrics */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-5 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">Total Tipos de Muestra</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">{(tiposCultivo || []).length}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-blue-50 dark:bg-blue-950/60 rounded-xl text-blue-600 dark:text-blue-400">
|
||||||
|
<Microscope className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-5 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">Categorías Activas</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">{categoriasDisponibles.length}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-indigo-50 dark:bg-indigo-950/60 rounded-xl text-indigo-600 dark:text-indigo-400">
|
||||||
|
<Tag className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-5 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">Cultivos Registrados</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">{(cultivos || []).length}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-emerald-50 dark:bg-emerald-950/60 rounded-xl text-emerald-600 dark:text-emerald-400">
|
||||||
|
<CheckCircle2 className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Cultivos Management Card */}
|
||||||
|
<Card>
|
||||||
|
<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>
|
||||||
|
<CardDescription>
|
||||||
|
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
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={handleOpenNewDialog}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Nuevo Tipo de Cultivo
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Search and Filters */}
|
||||||
|
<div className="flex flex-col sm:flex-row 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..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 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]">
|
||||||
|
<SelectValue placeholder="Filtrar por categoría" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-60">
|
||||||
|
<SelectItem value="todas">Todas las categorías ({categoriasDisponibles.length})</SelectItem>
|
||||||
|
{categoriasDisponibles.map(cat => (
|
||||||
|
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table of Tipos */}
|
||||||
|
<div className="border rounded-lg overflow-hidden border-gray-200 dark:border-gray-800">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="bg-gray-50 dark:bg-gray-800/50">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="font-semibold">Nombre / Código</TableHead>
|
||||||
|
<TableHead className="font-semibold">Categoría</TableHead>
|
||||||
|
<TableHead className="font-semibold">Descripción</TableHead>
|
||||||
|
<TableHead className="font-semibold text-center">Uso en Cultivos</TableHead>
|
||||||
|
<TableHead className="font-semibold text-right">Acciones</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{tiposFiltrados.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
|
<Microscope className="h-8 w-8 mx-auto mb-2 opacity-40 text-gray-400" />
|
||||||
|
<p className="font-medium">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>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
tiposFiltrados.map((tipo) => {
|
||||||
|
const count = usageCountMap[tipo.nombre.trim()] || 0;
|
||||||
|
return (
|
||||||
|
<TableRow key={tipo.id} className="hover:bg-gray-50/60 dark:hover:bg-gray-800/40">
|
||||||
|
<TableCell className="font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
<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 inline-block">
|
||||||
|
{tipo.nombre}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline" className="text-xs bg-gray-50 dark:bg-gray-800">
|
||||||
|
{tipo.categoria || 'General'}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm text-gray-600 dark:text-gray-300 max-w-md">
|
||||||
|
{tipo.descripcion || <span className="text-gray-400 italic text-xs">Sin descripción</span>}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{count > 0 ? (
|
||||||
|
<Badge className="bg-emerald-100 text-emerald-800 dark:bg-emerald-950/80 dark:text-emerald-300 text-xs font-semibold hover:bg-emerald-200">
|
||||||
|
{count} {count === 1 ? 'cultivo' : 'cultivos'}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-400">0 usos</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<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>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* TAB 2: CONSOLA MONGOSH & MANTENIMIENTO */}
|
||||||
|
<TabsContent value="mantenimiento" className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<Card className="col-span-1 md:col-span-3">
|
<Card className="col-span-1 md:col-span-3">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -267,7 +682,298 @@ export function SeccionSistema() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* 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
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
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>
|
||||||
|
<Input
|
||||||
|
id="nuevo-nombre"
|
||||||
|
placeholder="Ej: Hemocultivo Central, Líquido Peritoneal, etc."
|
||||||
|
value={formNombre}
|
||||||
|
onChange={(e) => setFormNombre(e.target.value)}
|
||||||
|
required
|
||||||
|
className="mt-1"
|
||||||
|
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>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCustomCategoriaNew(!isCustomCategoriaNew);
|
||||||
|
if (!isCustomCategoriaNew) setFormCategoria('');
|
||||||
|
else setFormCategoria(categoriasDisponibles[0] || 'General');
|
||||||
|
}}
|
||||||
|
className="text-xs text-blue-600 dark:text-blue-400 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
{isCustomCategoriaNew ? '← Elegir de existentes' : '➕ Crear nueva categoría'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isCustomCategoriaNew ? (
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
value={categoriasDisponibles.includes(formCategoria) ? formCategoria : (categoriasDisponibles[0] || 'General')}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
if (val === '__custom__') {
|
||||||
|
setIsCustomCategoriaNew(true);
|
||||||
|
setFormCategoria('');
|
||||||
|
} else {
|
||||||
|
setFormCategoria(val);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="nuevo-categoria" className="w-full">
|
||||||
|
<SelectValue placeholder="Seleccionar categoría" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-60">
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Categorías Existentes ({categoriasDisponibles.length})</SelectLabel>
|
||||||
|
{categoriasDisponibles.map(c => (
|
||||||
|
<SelectItem key={c} value={c}>
|
||||||
|
{c}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="__custom__" className="text-blue-600 dark:text-blue-400 font-medium">
|
||||||
|
➕ 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>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Input
|
||||||
|
id="nuevo-categoria"
|
||||||
|
placeholder="Ej: Hemocultivos, Líquidos, Respiratorio..."
|
||||||
|
value={formCategoria}
|
||||||
|
onChange={(e) => setFormCategoria(e.target.value)}
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<p className="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>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="pt-4 flex gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setIsNewDialogOpen(false)} disabled={isSubmitting}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Guardando...' : 'Crear Tipo de Cultivo'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 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
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
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>
|
||||||
|
<Input
|
||||||
|
id="edit-nombre"
|
||||||
|
value={formNombre}
|
||||||
|
onChange={(e) => setFormNombre(e.target.value)}
|
||||||
|
required
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<Label htmlFor="edit-categoria" className="text-sm font-semibold">Categoría / Grupo</Label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCustomCategoriaEdit(!isCustomCategoriaEdit);
|
||||||
|
if (!isCustomCategoriaEdit) setFormCategoria('');
|
||||||
|
else setFormCategoria(categoriasDisponibles[0] || 'General');
|
||||||
|
}}
|
||||||
|
className="text-xs text-blue-600 dark:text-blue-400 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
{isCustomCategoriaEdit ? '← Elegir de existentes' : '➕ Crear nueva categoría'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isCustomCategoriaEdit ? (
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
value={categoriasDisponibles.includes(formCategoria) ? formCategoria : (categoriasDisponibles[0] || 'General')}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
if (val === '__custom__') {
|
||||||
|
setIsCustomCategoriaEdit(true);
|
||||||
|
setFormCategoria('');
|
||||||
|
} else {
|
||||||
|
setFormCategoria(val);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="edit-categoria" className="w-full">
|
||||||
|
<SelectValue placeholder="Seleccionar categoría" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-60">
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Categorías Existentes ({categoriasDisponibles.length})</SelectLabel>
|
||||||
|
{categoriasDisponibles.map(c => (
|
||||||
|
<SelectItem key={c} value={c}>
|
||||||
|
{c}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="__custom__" className="text-blue-600 dark:text-blue-400 font-medium">
|
||||||
|
➕ 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>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Input
|
||||||
|
id="edit-categoria"
|
||||||
|
placeholder="Ej: Hemocultivos, Líquidos, Respiratorio..."
|
||||||
|
value={formCategoria}
|
||||||
|
onChange={(e) => setFormCategoria(e.target.value)}
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<p className="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>
|
||||||
|
<Input
|
||||||
|
id="edit-desc"
|
||||||
|
value={formDescripcion}
|
||||||
|
onChange={(e) => setFormDescripcion(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="pt-4 flex gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setIsEditDialogOpen(false)} disabled={isSubmitting}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Guardando...' : 'Guardar Cambios'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 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
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
¿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">
|
||||||
|
<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.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter className="pt-2 flex gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setIsDeleteDialogOpen(false)} disabled={isSubmitting}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="destructive" onClick={handleDeleteTipo} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Eliminando...' : 'Sí, Eliminar'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 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
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
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">
|
||||||
|
¿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}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={handleResetTipos} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Restableciendo...' : 'Restablecer Catálogo'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* LOGS MODAL */}
|
||||||
{isLogsModalOpen && (
|
{isLogsModalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
<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">
|
<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">
|
||||||
|
|||||||
+8
-1
@@ -159,6 +159,13 @@ export interface AcidoBase {
|
|||||||
interpretacion?: string;
|
interpretacion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TipoCultivo {
|
||||||
|
id: string;
|
||||||
|
nombre: string;
|
||||||
|
categoria?: string;
|
||||||
|
descripcion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Cultivo {
|
export interface Cultivo {
|
||||||
id: string;
|
id: string;
|
||||||
pacienteId: string;
|
pacienteId: string;
|
||||||
@@ -167,7 +174,7 @@ export interface Cultivo {
|
|||||||
fechaToma: string;
|
fechaToma: string;
|
||||||
protocolo?: string;
|
protocolo?: string;
|
||||||
fechaResultado?: 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;
|
germen?: string;
|
||||||
sensible?: string;
|
sensible?: string;
|
||||||
resistente?: string;
|
resistente?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user