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,
|
||||
getAllAcidosBase, createAcidoBase, updateAcidoBase, deleteAcidoBase,
|
||||
getAllCultivos, createCultivo, updateCultivo, deleteCultivo,
|
||||
getAllTiposCultivo, createTipoCultivo, updateTipoCultivo, deleteTipoCultivo, restablecerTiposCultivo,
|
||||
getAllEstudiosComplementarios, createEstudioComplementario, updateEstudioComplementario, deleteEstudioComplementario,
|
||||
getAllInterconsultas, createInterconsulta, updateInterconsulta, deleteInterconsulta,
|
||||
getAllAtb, createAtb, updateAtb, deleteAtb,
|
||||
@@ -95,6 +96,7 @@ app.get('/api/state', async (req, res) => {
|
||||
glucemias: await getAllGlucemias(),
|
||||
acidosBase: await getAllAcidosBase(),
|
||||
cultivos: await getAllCultivos(),
|
||||
tiposCultivo: await getAllTiposCultivo(),
|
||||
estudiosComplementarios: await getAllEstudiosComplementarios(),
|
||||
interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })),
|
||||
atb: await getAllAtb(),
|
||||
@@ -580,6 +582,51 @@ app.delete('/api/cultivos/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== TIPOS DE CULTIVO ==========
|
||||
app.get('/api/tipos-cultivo', async (req, res) => {
|
||||
try {
|
||||
res.json(await getAllTiposCultivo());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al obtener tipos de cultivo' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/tipos-cultivo', async (req, res) => {
|
||||
try {
|
||||
const nuevo = await createTipoCultivo(req.body);
|
||||
res.json(nuevo);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message || 'Error al crear tipo de cultivo' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/tipos-cultivo/:id', async (req, res) => {
|
||||
try {
|
||||
const updated = await updateTipoCultivo(req.params.id, req.body);
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message || 'Error al actualizar tipo de cultivo' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/tipos-cultivo/:id', async (req, res) => {
|
||||
try {
|
||||
await deleteTipoCultivo(req.params.id);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al eliminar tipo de cultivo' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/tipos-cultivo/reset', async (req, res) => {
|
||||
try {
|
||||
const list = await restablecerTiposCultivo();
|
||||
res.json(list);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al restablecer tipos de cultivo' });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ESTUDIOS COMPLEMENTARIOS ==========
|
||||
app.get('/api/estudios-complementarios', async (req, res) => {
|
||||
try {
|
||||
|
||||
+158
-2
@@ -66,6 +66,8 @@ const memStore = {
|
||||
indicaciones: [],
|
||||
movimientos_indicaciones: [],
|
||||
pendientes: [],
|
||||
tipos_cultivo: [],
|
||||
otrosLaboratorios: [],
|
||||
kv: {}
|
||||
};
|
||||
|
||||
@@ -703,6 +705,160 @@ export async function deleteCultivo(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== TIPOS DE CULTIVO ==========
|
||||
export const DEFAULT_TIPOS_CULTIVO = [
|
||||
{ id: '1', nombre: 'HMCx2', categoria: 'Hemocultivos', descripcion: 'Hemocultivos seriados x2' },
|
||||
{ id: '2', nombre: 'RC', categoria: 'Catéter', descripcion: 'Retro cultivo / punta de catéter' },
|
||||
{ id: '3', nombre: 'PC', categoria: 'Punción', descripcion: 'Punción cultivo' },
|
||||
{ id: '4', nombre: 'UC', categoria: 'Urocultivo', descripcion: 'Urocultivo / muestra de orina' },
|
||||
{ id: '5', nombre: 'LCR', categoria: 'Líquidos', descripcion: 'Líquido cefalorraquídeo' },
|
||||
{ id: '6', nombre: 'LP', categoria: 'Líquidos', descripcion: 'Líquido pleural' },
|
||||
{ id: '7', nombre: 'LAsc', categoria: 'Líquidos', descripcion: 'Líquido ascítico' },
|
||||
{ id: '8', nombre: 'LAbd', categoria: 'Líquidos', descripcion: 'Líquido abdominal' },
|
||||
{ id: '9', nombre: 'Coleccion', categoria: 'Líquidos y Colecciones', descripcion: 'Muestra de colección / absceso' },
|
||||
{ id: '10', nombre: 'Partes Blandas', categoria: 'Tejidos', descripcion: 'Cultivo de partes blandas / tejido' },
|
||||
{ id: '11', nombre: 'Esputo GC', categoria: 'Respiratorio', descripcion: 'Esputo Germen Común' },
|
||||
{ id: '12', nombre: 'Esputo TBC', categoria: 'Respiratorio', descripcion: 'Esputo Tuberculosis' },
|
||||
{ id: '13', nombre: 'Baciloscopia', categoria: 'Respiratorio', descripcion: 'Baciloscopia directa' },
|
||||
{ id: '14', nombre: 'HNF Test Rápido', categoria: 'Hisopado Nasofaríngeo', descripcion: 'Hisopado nasofaríngeo test rápido' },
|
||||
{ id: '15', nombre: 'HNF Panel PCR', categoria: 'Hisopado Nasofaríngeo', descripcion: 'Hisopado nasofaríngeo panel PCR virológico' },
|
||||
{ id: '16', nombre: 'Hisopado Rectal KPC', categoria: 'Vigilancia Epidemiológica', descripcion: 'Hisopado rectal para screening de KPC/BLEE' }
|
||||
];
|
||||
|
||||
export async function getAllTiposCultivo() {
|
||||
let list = [];
|
||||
if (db) {
|
||||
const raw = await db.collection('tipos_cultivo').find().toArray();
|
||||
list = cleanDocs(raw);
|
||||
if (list.length === 0) {
|
||||
// Seed default
|
||||
for (const item of DEFAULT_TIPOS_CULTIVO) {
|
||||
await db.collection('tipos_cultivo').insertOne({ ...item });
|
||||
}
|
||||
list = [...DEFAULT_TIPOS_CULTIVO];
|
||||
}
|
||||
// Also read any existing custom tipoMuestra from registered cultivos that might not be in the list
|
||||
const allCultivos = await db.collection('cultivos').find().toArray();
|
||||
const existingNombres = new Set(list.map(t => (t.nombre || '').trim().toLowerCase()));
|
||||
for (const c of allCultivos) {
|
||||
if (c.tipoMuestra && c.tipoMuestra.trim() && !existingNombres.has(c.tipoMuestra.trim().toLowerCase())) {
|
||||
const nuevoTipo = {
|
||||
id: generateUUID(),
|
||||
nombre: c.tipoMuestra.trim(),
|
||||
categoria: 'Personalizado',
|
||||
descripcion: 'Importado automáticamente desde registro existente de cultivo'
|
||||
};
|
||||
await db.collection('tipos_cultivo').insertOne(nuevoTipo);
|
||||
list.push(nuevoTipo);
|
||||
existingNombres.add(c.tipoMuestra.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
return list;
|
||||
} else {
|
||||
if (!memStore.tipos_cultivo || memStore.tipos_cultivo.length === 0) {
|
||||
memStore.tipos_cultivo = DEFAULT_TIPOS_CULTIVO.map(item => ({ ...item }));
|
||||
}
|
||||
list = [...memStore.tipos_cultivo];
|
||||
const existingNombres = new Set(list.map(t => (t.nombre || '').trim().toLowerCase()));
|
||||
for (const c of (memStore.cultivos || [])) {
|
||||
if (c.tipoMuestra && c.tipoMuestra.trim() && !existingNombres.has(c.tipoMuestra.trim().toLowerCase())) {
|
||||
const nuevoTipo = {
|
||||
id: generateUUID(),
|
||||
nombre: c.tipoMuestra.trim(),
|
||||
categoria: 'Personalizado',
|
||||
descripcion: 'Importado automáticamente desde registro existente de cultivo'
|
||||
};
|
||||
memStore.tipos_cultivo.push(nuevoTipo);
|
||||
list.push(nuevoTipo);
|
||||
existingNombres.add(c.tipoMuestra.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTipoCultivo(tipo) {
|
||||
const doc = {
|
||||
id: tipo.id || generateUUID(),
|
||||
nombre: (tipo.nombre || '').trim(),
|
||||
categoria: (tipo.categoria || 'General').trim(),
|
||||
descripcion: (tipo.descripcion || '').trim()
|
||||
};
|
||||
if (!doc.nombre) {
|
||||
throw new Error('El nombre del tipo de cultivo es requerido');
|
||||
}
|
||||
|
||||
if (db) {
|
||||
const existing = await db.collection('tipos_cultivo').findOne({ nombre: doc.nombre });
|
||||
if (existing) {
|
||||
throw new Error(`Ya existe un tipo de cultivo con el nombre "${doc.nombre}"`);
|
||||
}
|
||||
await db.collection('tipos_cultivo').insertOne(doc);
|
||||
} else {
|
||||
if (!memStore.tipos_cultivo) memStore.tipos_cultivo = [];
|
||||
const exists = memStore.tipos_cultivo.some(t => t.nombre.toLowerCase() === doc.nombre.toLowerCase());
|
||||
if (exists) {
|
||||
throw new Error(`Ya existe un tipo de cultivo con el nombre "${doc.nombre}"`);
|
||||
}
|
||||
memStore.tipos_cultivo.push(doc);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function updateTipoCultivo(id, datos) {
|
||||
const updateDoc = {};
|
||||
if (datos.nombre !== undefined) updateDoc.nombre = datos.nombre.trim();
|
||||
if (datos.categoria !== undefined) updateDoc.categoria = datos.categoria.trim();
|
||||
if (datos.descripcion !== undefined) updateDoc.descripcion = datos.descripcion.trim();
|
||||
|
||||
if (db) {
|
||||
if (updateDoc.nombre) {
|
||||
const existing = await db.collection('tipos_cultivo').findOne({ nombre: updateDoc.nombre, id: { $ne: id } });
|
||||
if (existing) {
|
||||
throw new Error(`Ya existe otro tipo de cultivo con el nombre "${updateDoc.nombre}"`);
|
||||
}
|
||||
}
|
||||
await db.collection('tipos_cultivo').updateOne({ id }, { $set: updateDoc });
|
||||
} else {
|
||||
if (!memStore.tipos_cultivo) memStore.tipos_cultivo = [];
|
||||
if (updateDoc.nombre) {
|
||||
const exists = memStore.tipos_cultivo.some(t => t.id !== id && t.nombre.toLowerCase() === updateDoc.nombre.toLowerCase());
|
||||
if (exists) {
|
||||
throw new Error(`Ya existe otro tipo de cultivo con el nombre "${updateDoc.nombre}"`);
|
||||
}
|
||||
}
|
||||
const idx = memStore.tipos_cultivo.findIndex(t => t.id === id);
|
||||
if (idx !== -1) {
|
||||
memStore.tipos_cultivo[idx] = { ...memStore.tipos_cultivo[idx], ...updateDoc };
|
||||
}
|
||||
}
|
||||
return { id, ...datos };
|
||||
}
|
||||
|
||||
export async function deleteTipoCultivo(id) {
|
||||
if (db) {
|
||||
await db.collection('tipos_cultivo').deleteOne({ id });
|
||||
} else {
|
||||
if (memStore.tipos_cultivo) {
|
||||
memStore.tipos_cultivo = memStore.tipos_cultivo.filter(t => t.id !== id);
|
||||
}
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function restablecerTiposCultivo() {
|
||||
if (db) {
|
||||
await db.collection('tipos_cultivo').deleteMany({});
|
||||
for (const item of DEFAULT_TIPOS_CULTIVO) {
|
||||
await db.collection('tipos_cultivo').insertOne({ ...item });
|
||||
}
|
||||
return await getAllTiposCultivo();
|
||||
} else {
|
||||
memStore.tipos_cultivo = DEFAULT_TIPOS_CULTIVO.map(item => ({ ...item }));
|
||||
return [...memStore.tipos_cultivo];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== ESTUDIOS COMPLEMENTARIOS ==========
|
||||
export async function getAllEstudiosComplementarios() {
|
||||
if (db) {
|
||||
@@ -1003,7 +1159,7 @@ export async function exportAllData() {
|
||||
const collections = [
|
||||
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
||||
'evoluciones', 'laboratorios', 'glucemias', 'acidosbase',
|
||||
'cultivos', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'cultivos', 'tipos_cultivo', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
||||
];
|
||||
const dump = {};
|
||||
@@ -1024,7 +1180,7 @@ export async function importAllData(dump) {
|
||||
const collections = [
|
||||
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
||||
'evoluciones', 'laboratorios', 'glucemias', 'acidosbase',
|
||||
'cultivos', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'cultivos', 'tipos_cultivo', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
||||
];
|
||||
if (db) {
|
||||
|
||||
@@ -6,28 +6,33 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-150 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-blue-500/40 focus-visible:ring-offset-1 select-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
default:
|
||||
"bg-gradient-to-b from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 active:from-blue-700 active:to-blue-800 text-white shadow-xs shadow-blue-500/25 border border-blue-600/40 dark:from-blue-600 dark:to-blue-700 dark:hover:from-blue-500 dark:hover:to-blue-600 dark:border-blue-500/50",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
"bg-rose-600 text-white hover:bg-rose-700 active:bg-rose-800 shadow-xs shadow-rose-600/20 border border-rose-600/30 focus-visible:ring-rose-500/30 dark:bg-rose-600/90 dark:hover:bg-rose-600",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
"border border-blue-200/90 bg-white/90 text-blue-900 hover:bg-blue-50/90 hover:text-blue-700 hover:border-blue-300 dark:border-blue-800/80 dark:bg-gray-900/80 dark:text-blue-100 dark:hover:bg-blue-950/70 dark:hover:border-blue-700 shadow-2xs",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
"bg-blue-50 text-blue-700 hover:bg-blue-100 active:bg-blue-200/80 border border-blue-200/70 dark:bg-blue-950/60 dark:text-blue-200 dark:border-blue-800/80 dark:hover:bg-blue-900/60 shadow-2xs",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
"text-slate-700 dark:text-slate-200 hover:bg-blue-50 hover:text-blue-700 dark:hover:bg-blue-950/50 dark:hover:text-blue-300",
|
||||
link: "text-blue-600 underline-offset-4 hover:underline hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300",
|
||||
gradient:
|
||||
"bg-gradient-to-r from-blue-600 via-indigo-600 to-blue-700 hover:from-blue-700 hover:via-indigo-700 hover:to-blue-800 text-white shadow-sm shadow-blue-600/30 border border-blue-400/30",
|
||||
soft:
|
||||
"bg-blue-50/80 text-blue-700 hover:bg-blue-100 border border-transparent hover:border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-900/50",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 text-xs has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-lg px-6 text-base has-[>svg]:px-4",
|
||||
icon: "size-9 rounded-lg",
|
||||
"icon-sm": "size-8 rounded-md",
|
||||
"icon-lg": "size-10 rounded-lg",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
Glucemia,
|
||||
AcidoBase,
|
||||
Cultivo,
|
||||
TipoCultivo,
|
||||
EstudioComplementario,
|
||||
Interconsulta,
|
||||
ATB,
|
||||
@@ -45,6 +46,7 @@ interface HospitalState {
|
||||
glucemias: Glucemia[];
|
||||
acidosBase: AcidoBase[];
|
||||
cultivos: Cultivo[];
|
||||
tiposCultivo: TipoCultivo[];
|
||||
estudiosComplementarios: EstudioComplementario[];
|
||||
interconsultas: Interconsulta[];
|
||||
atb: ATB[];
|
||||
@@ -71,6 +73,7 @@ const defaultState = (): HospitalState => ({
|
||||
glucemias: [],
|
||||
acidosBase: [],
|
||||
cultivos: [],
|
||||
tiposCultivo: [],
|
||||
estudiosComplementarios: [],
|
||||
interconsultas: [],
|
||||
atb: [],
|
||||
@@ -104,6 +107,7 @@ export function useHospitalStore() {
|
||||
const normalized = {
|
||||
...defaults,
|
||||
...body,
|
||||
tiposCultivo: body.tiposCultivo || [],
|
||||
estudiosComplementarios: body.estudiosComplementarios || [],
|
||||
interconsultas: body.interconsultas || [],
|
||||
atb: body.atb || [],
|
||||
@@ -992,6 +996,75 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
}
|
||||
}, [state, checkGrupoPermission, apiCall]);
|
||||
|
||||
// Acciones de tipos de cultivo
|
||||
const agregarTipoCultivo = useCallback(async (tipo: Omit<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
|
||||
const agregarEstudioComplementario = useCallback(async (estudio: Omit<EstudioComplementario, 'id'>) => {
|
||||
const internacion = state.internaciones.find(i => i.id === estudio.internacionId);
|
||||
@@ -1581,6 +1654,10 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
agregarCultivo,
|
||||
actualizarCultivo,
|
||||
eliminarCultivo,
|
||||
agregarTipoCultivo,
|
||||
actualizarTipoCultivo,
|
||||
eliminarTipoCultivo,
|
||||
restablecerTiposCultivo,
|
||||
agregarEstudioComplementario,
|
||||
actualizarEstudioComplementario,
|
||||
eliminarEstudioComplementario,
|
||||
|
||||
+21
-21
@@ -10,28 +10,28 @@
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--secondary: 214 90% 96%;
|
||||
--secondary-foreground: 221.2 83.2% 40%;
|
||||
--muted: 214 32% 95%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 214 90% 96%;
|
||||
--accent-foreground: 221.2 83.2% 40%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--border: 214.3 31.8% 90%;
|
||||
--input: 214.3 31.8% 90%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.625rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-background: 0 0% 99%;
|
||||
--sidebar-foreground: 222.2 84% 4.9%;
|
||||
--sidebar-primary: 222.2 47.4% 11.2%;
|
||||
--sidebar-primary: 221.2 83.2% 53.3%;
|
||||
--sidebar-primary-foreground: 210 40% 98%;
|
||||
--sidebar-accent: 210 40% 96.1%;
|
||||
--sidebar-accent-foreground: 222.2 47.4% 11.2%;
|
||||
--sidebar-border: 214.3 31.8% 91.4%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
--sidebar-accent: 214 90% 96%;
|
||||
--sidebar-accent-foreground: 221.2 83.2% 40%;
|
||||
--sidebar-border: 214.3 31.8% 90%;
|
||||
--sidebar-ring: 221.2 83.2% 53.3%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -43,12 +43,12 @@
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--secondary: 217.2 45% 15%;
|
||||
--secondary-foreground: 213 94% 85%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--accent: 217.2 45% 15%;
|
||||
--accent-foreground: 213 94% 85%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 20%;
|
||||
@@ -58,8 +58,8 @@
|
||||
--sidebar-foreground: 210 40% 98%;
|
||||
--sidebar-primary: 217.2 91.2% 59.8%;
|
||||
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
|
||||
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||
--sidebar-accent-foreground: 210 40% 98%;
|
||||
--sidebar-accent: 217.2 45% 15%;
|
||||
--sidebar-accent-foreground: 213 94% 85%;
|
||||
--sidebar-border: 217.2 32.6% 20%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
+86
-72
@@ -94,23 +94,23 @@ export function Cultivos({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||
<div className="flex-1 flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
|
||||
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between">
|
||||
<div className="flex-1 flex flex-col sm:flex-row gap-2 w-full">
|
||||
<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 protocolo, germen, paciente..."
|
||||
placeholder="Buscar por protocolo, germen, paciente, DNI..."
|
||||
value={busqueda}
|
||||
onChange={(e) => setBusqueda(e.target.value)}
|
||||
className="pl-8"
|
||||
className="pl-9 w-full bg-white dark:bg-gray-800"
|
||||
/>
|
||||
</div>
|
||||
<Select value={filtroEstado} onValueChange={(v) => setFiltroEstado(v as Cultivo['estado'] | 'todos')}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
<SelectTrigger className="w-full sm:w-[190px] bg-white dark:bg-gray-800 shrink-0">
|
||||
<SelectValue placeholder="Filtrar estado" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="todos">Todos</SelectItem>
|
||||
<SelectItem value="todos">Todos los estados</SelectItem>
|
||||
<SelectItem value="NAF/Pendiente">NAF / Pendiente</SelectItem>
|
||||
<SelectItem value="Parcial">Parcial</SelectItem>
|
||||
<SelectItem value="Positivo">Positivo</SelectItem>
|
||||
@@ -126,40 +126,42 @@ export function Cultivos({
|
||||
const camaNombre = getCamaNombreForPaciente(cultivo.pacienteId);
|
||||
|
||||
return (
|
||||
<Card key={cultivo.id} className="hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`h-10 w-10 rounded-full flex items-center justify-center shrink-0 ${
|
||||
cultivo.estado === 'NAF/Pendiente' ? 'bg-amber-100 dark:bg-amber-950/80' :
|
||||
cultivo.estado === 'Parcial' ? 'bg-orange-100 dark:bg-orange-950/80' :
|
||||
cultivo.estado === 'Positivo' ? 'bg-red-100 dark:bg-red-950/80' : 'bg-green-100 dark:bg-green-950/80'
|
||||
<Card key={cultivo.id} className="hover:shadow-md transition-shadow overflow-hidden">
|
||||
<CardContent className="p-3.5 sm:p-5">
|
||||
<div className="flex flex-col gap-3 min-w-0">
|
||||
{/* Card Header Info */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between 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 shrink-0 mt-0.5 ${
|
||||
cultivo.estado === 'NAF/Pendiente' ? 'bg-amber-100 text-amber-700 dark:bg-amber-950/80 dark:text-amber-300' :
|
||||
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 ${
|
||||
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'
|
||||
}`} />
|
||||
<Microscope className="h-5 w-5 shrink-0" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{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">
|
||||
{camaNombre}
|
||||
</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'}
|
||||
</h3>
|
||||
</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">
|
||||
<Calendar className="h-3 w-3" />
|
||||
|
||||
<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">
|
||||
<span className="inline-flex items-center gap-1 shrink-0">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
{cultivo.fechaToma}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs">{cultivo.tipoMuestra}</Badge>
|
||||
<Badge className={`text-xs ${getEstadoColor(cultivo.estado)}`}>
|
||||
<span className="text-gray-300 dark:text-gray-600 hidden xs:inline">•</span>
|
||||
<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)}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -167,92 +169,102 @@ export function Cultivos({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Estado NAF / Pendiente */}
|
||||
{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">
|
||||
<span className="font-semibold">Observaciones:</span> {cultivo.observaciones}
|
||||
<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 text-gray-700 dark:text-gray-200">Observaciones:</span> {cultivo.observaciones}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 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 && (
|
||||
<p className="text-sm font-medium text-orange-800 dark:text-orange-300 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
PARCIAL - Germen: {cultivo.germen}
|
||||
<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 shrink-0 mt-0.5" />
|
||||
<span>PARCIAL - Germen: {cultivo.germen}</span>
|
||||
</p>
|
||||
)}
|
||||
{(cultivo.sensible || cultivo.resistente) && (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1.5 pt-1">
|
||||
{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 && (
|
||||
<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>
|
||||
)}
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Estado Positivo */}
|
||||
{cultivo.estado === 'Positivo' && (
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-2 mt-1">
|
||||
<div className="flex flex-col gap-2">
|
||||
{cultivo.germen && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
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"
|
||||
>
|
||||
<AlertCircle className="h-4 w-4 text-red-600 shrink-0" />
|
||||
<span>Germen: {cultivo.germen} {cultivo.fechaResultado ? `(Definitivo: ${cultivo.fechaResultado})` : ''}</span>
|
||||
</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">
|
||||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 shrink-0 mt-0.5" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="font-bold text-red-800 dark:text-red-300">Germen: </span>
|
||||
<span className="font-medium">{cultivo.germen}</span>
|
||||
{cultivo.fechaResultado && (
|
||||
<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 && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
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>Sensibilidad: {cultivo.sensible}</span>
|
||||
</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">
|
||||
<span className="font-bold text-green-800 dark:text-green-300">Sensibilidad: </span>
|
||||
<span>{cultivo.sensible}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cultivo.resistente && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
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>Resistencia: {cultivo.resistente}</span>
|
||||
</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">
|
||||
<span className="font-bold text-red-800 dark:text-red-300">Resistencia: </span>
|
||||
<span>{cultivo.resistente}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<p className="text-sm font-medium text-green-800 dark:text-green-200 flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Cultivo Negativo
|
||||
<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="font-semibold text-green-800 dark:text-green-300 flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 shrink-0" />
|
||||
Cultivo Negativo Final
|
||||
</p>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
@@ -265,9 +277,11 @@ export function Cultivos({
|
||||
})}
|
||||
|
||||
{cultivosFiltrados.length === 0 && (
|
||||
<p className="text-center text-gray-500 py-8">
|
||||
No se encontraron cultivos con los filtros seleccionados
|
||||
</p>
|
||||
<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">
|
||||
<Microscope className="h-10 w-10 text-gray-400 mx-auto mb-2 opacity-60" />
|
||||
<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>
|
||||
|
||||
+13
-14
@@ -3,7 +3,6 @@ import {
|
||||
Bed,
|
||||
Users,
|
||||
ClipboardList,
|
||||
FlaskConical,
|
||||
Activity,
|
||||
Microscope,
|
||||
TrendingUp,
|
||||
@@ -240,35 +239,35 @@ export function Dashboard({
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Button
|
||||
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')}
|
||||
>
|
||||
<Users className="h-6 w-6 text-blue-600" />
|
||||
<span className="text-sm">Nuevo Paciente</span>
|
||||
<Users className="h-6 w-6 text-blue-600 dark:text-blue-400 group-hover:scale-110 transition-transform" />
|
||||
<span className="text-sm font-medium">Nuevo Paciente</span>
|
||||
</Button>
|
||||
<Button
|
||||
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')}
|
||||
>
|
||||
<ClipboardList className="h-6 w-6 text-purple-600" />
|
||||
<span className="text-sm">Nueva Internación</span>
|
||||
<ClipboardList className="h-6 w-6 text-blue-600 dark:text-blue-400 group-hover:scale-110 transition-transform" />
|
||||
<span className="text-sm font-medium">Nueva Internación</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto py-4 flex flex-col items-center gap-2"
|
||||
onClick={() => onCambiarVista('laboratorios')}
|
||||
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('camas')}
|
||||
>
|
||||
<FlaskConical className="h-6 w-6 text-amber-600" />
|
||||
<span className="text-sm">Laboratorio</span>
|
||||
<Bed className="h-6 w-6 text-blue-600 dark:text-blue-400 group-hover:scale-110 transition-transform" />
|
||||
<span className="text-sm font-medium">Mapa de Camas</span>
|
||||
</Button>
|
||||
<Button
|
||||
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')}
|
||||
>
|
||||
<Microscope className="h-6 w-6 text-teal-600" />
|
||||
<span className="text-sm">Cultivo</span>
|
||||
<Microscope className="h-6 w-6 text-blue-600 dark:text-blue-400 group-hover:scale-110 transition-transform" />
|
||||
<span className="text-sm font-medium">Cultivos</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -2840,13 +2840,35 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE
|
||||
canEdit?: boolean;
|
||||
portalNode?: HTMLDivElement | null;
|
||||
}) {
|
||||
const { tiposCultivo } = useHospitalStore();
|
||||
const [dialog, setDialog] = useState(false);
|
||||
const [resDialog, setResDialog] = useState(false);
|
||||
const [editDialog, setEditDialog] = useState(false);
|
||||
const [selected, setSelected] = useState<Cultivo | null>(null);
|
||||
const [fechaToma, setFechaToma] = useState(getLocalToday());
|
||||
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 [fechaResultado, setFechaResultado] = useState(getLocalToday());
|
||||
const [estadoResultado, setEstadoResultado] = useState<Cultivo['estado']>('Parcial');
|
||||
@@ -2857,7 +2879,7 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE
|
||||
const reset = () => {
|
||||
setFechaToma(getLocalToday());
|
||||
setProtocolo('');
|
||||
setTipoMuestra('HMCx2');
|
||||
setTipoMuestra(listaTipos[0]?.nombre || 'HMCx2');
|
||||
setObservaciones('');
|
||||
setSelected(null);
|
||||
};
|
||||
@@ -2960,34 +2982,24 @@ function SeccionCultivos({ cults, patient, internacionId, add, update, del, canE
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader><DialogTitle>Nuevo Cultivo</DialogTitle></DialogHeader>
|
||||
<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>Protocolo</Label><Input value={protocolo} onChange={e => setProtocolo(e.target.value)} placeholder="N° Protocolo" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Tipo de Muestra *</Label>
|
||||
<Select value={tipoMuestra} onValueChange={(v: Cultivo['tipoMuestra']) => setTipoMuestra(v)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="HMCx2">HMCx2</SelectItem>
|
||||
<SelectItem value="RC">RC</SelectItem>
|
||||
<SelectItem value="PC">PC</SelectItem>
|
||||
<SelectItem value="UC">UC</SelectItem>
|
||||
<SelectItem value="LCR">LCR</SelectItem>
|
||||
<SelectItem value="LP">LP</SelectItem>
|
||||
<SelectItem value="LAsc">LAsc</SelectItem>
|
||||
<SelectItem value="LAbd">LAbd</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>
|
||||
<Select value={tipoMuestra} onValueChange={(v: string) => setTipoMuestra(v)}>
|
||||
<SelectTrigger><SelectValue placeholder="Seleccione tipo de muestra" /></SelectTrigger>
|
||||
<SelectContent className="max-h-72">
|
||||
{listaTipos.map((t) => (
|
||||
<SelectItem key={t.id} value={t.nombre}>
|
||||
<span className="font-medium">{t.nombre}</span>
|
||||
{t.categoria && <span className="text-xs text-gray-500 dark:text-gray-400 ml-2">({t.categoria})</span>}
|
||||
</SelectItem>
|
||||
))}
|
||||
{tipoMuestra && !listaTipos.some(t => t.nombre === tipoMuestra) && (
|
||||
<SelectItem value={tipoMuestra}>{tipoMuestra}</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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">
|
||||
<DialogHeader><DialogTitle>Cargar Resultado Definitivo</DialogTitle></DialogHeader>
|
||||
<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>Resultado *</Label>
|
||||
<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">
|
||||
{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 => (
|
||||
<Card key={c.id} className="hover:shadow-md transition-shadow"><CardContent className="p-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<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'}`}>
|
||||
<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'}`} />
|
||||
<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 min-w-0">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between 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 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 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>
|
||||
<p className="font-medium">{formatDateDDMMYYYY(c.fechaToma)}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Badge variant="outline">{c.tipoMuestra}</Badge>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-semibold text-gray-900 dark:text-white">{formatDateDDMMYYYY(c.fechaToma)}</p>
|
||||
<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" className="max-w-full truncate">{c.tipoMuestra}</Badge>
|
||||
<Badge className={getEstadoColor(c.estado)}>{getEstadoLabel(c.estado)}</Badge>
|
||||
</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' && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{(c.estado === 'Positivo' || c.estado === 'Negativo') && <Button size="sm" variant="outline" onClick={() => openDefinitivo(c)}><Pencil className="h-4 w-4" /></Button>}
|
||||
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(c.id)}><Trash2 /></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 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>
|
||||
{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') && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{c.estado === 'Parcial' ? 'Fecha Resultado Parcial' : 'Fecha Definitivo'}: {formatDateDDMMYYYY(c.fechaResultado)}
|
||||
</p>
|
||||
)}
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
{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 && (
|
||||
<p className="text-sm font-medium text-orange-800 dark:text-orange-300 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4" />PARCIAL - Germen: {c.germen}
|
||||
<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 shrink-0 mt-0.5" /><span>PARCIAL - Germen: {c.germen}</span>
|
||||
</p>
|
||||
)}
|
||||
{(c.sensible || c.resistente) && (
|
||||
<div className="space-y-1">
|
||||
{c.sensible && <p className="text-xs text-orange-700 dark:text-orange-300">Sensible: {c.sensible}</p>}
|
||||
{c.resistente && <p className="text-xs text-orange-700 dark:text-orange-300">Resistente: {c.resistente}</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 break-words">Resistente: {c.resistente}</p>}
|
||||
</div>
|
||||
)}
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{c.estado === 'Positivo' && (
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<div className="flex flex-col gap-2 mt-1">
|
||||
{c.germen && (
|
||||
<div>
|
||||
<Badge
|
||||
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" />
|
||||
<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">
|
||||
<AlertCircle className="h-4 w-4 text-red-600 shrink-0 mt-0.5" />
|
||||
<span>Germen: {c.germen}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(c.sensible || c.resistente) && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{c.sensible && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
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>Sensibilidad: {c.sensible}</span>
|
||||
</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">
|
||||
<span className="font-semibold">Sensibilidad:</span> {c.sensible}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{c.resistente && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
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>Resistencia: {c.resistente}</span>
|
||||
</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">
|
||||
<span className="font-semibold">Resistencia:</span> {c.resistente}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -235,7 +235,7 @@ export function Internaciones({
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<Dialog open={dialogoNuevaAbierto} onOpenChange={setDialogoNuevaAbierto}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" onClick={() => onNuevoIngreso ? onNuevoIngreso() : setDialogoNuevaAbierto(true)}>
|
||||
<Button onClick={() => onNuevoIngreso ? onNuevoIngreso() : setDialogoNuevaAbierto(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nuevo Ingreso
|
||||
</Button>
|
||||
@@ -361,7 +361,7 @@ export function Internaciones({
|
||||
placeholder="Nombre del médico ingresante"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline"
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleIniciarInternacion}
|
||||
disabled={!pacienteSeleccionado || !camaSeleccionada || !motivoConsulta || !enfermedadActual || !effectiveMedico}
|
||||
@@ -693,7 +693,7 @@ export function Internaciones({
|
||||
placeholder="Ingrese el diagnóstico de egreso..."
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline"
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleFinalizarInternacion}
|
||||
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) => (
|
||||
<nav className="flex flex-col gap-2">
|
||||
<nav className="flex flex-col gap-1.5">
|
||||
{filteredMenuItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = vistaActual === item.vista;
|
||||
@@ -72,13 +72,17 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
|
||||
<Button
|
||||
key={item.vista}
|
||||
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={() => {
|
||||
onCambiarVista(item.vista);
|
||||
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>
|
||||
</Button>
|
||||
);
|
||||
|
||||
+18
-12
@@ -158,24 +158,24 @@ export function MapaCamas({
|
||||
</Badge>
|
||||
<div className="ml-0 sm:ml-4 flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
||||
{currentUser?.rol === 'admin' && (
|
||||
<Button variant="outline" onClick={() => {
|
||||
<Button variant="secondary" onClick={() => {
|
||||
setEditingGrupoId(null);
|
||||
setGrupoNombre('');
|
||||
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
|
||||
</Button>
|
||||
)}
|
||||
{currentUser?.rol === 'admin' && (
|
||||
<Button variant="outline" onClick={() => {
|
||||
<Button onClick={() => {
|
||||
setEditingBed(null);
|
||||
setBedNumero('');
|
||||
setBedTipo('General');
|
||||
setBedGrupoId(grupos?.[0]?.id);
|
||||
setBedDialogOpen(true);
|
||||
}} className="text-xs px-2 py-1 sm:text-sm sm:px-4 sm:py-2">
|
||||
<Plus className="h-3 w-3 sm:h-4 sm:w-4 sm:mr-1" />
|
||||
<span className="hidden sm:inline">Agregar Cama</span>
|
||||
}} className="text-xs px-2.5 py-1.5 sm:text-sm sm:px-4 sm:py-2">
|
||||
<Plus className="h-3.5 w-3.5 sm:h-4 sm:w-4 mr-1" />
|
||||
<span>Agregar Cama</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -480,15 +480,15 @@ export function MapaCamas({
|
||||
</Button>
|
||||
)}
|
||||
<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 size="sm" variant="outline" onClick={() => {
|
||||
<Button size="sm" onClick={() => {
|
||||
if (!grupoNombre.trim()) return alert('Nombre requerido');
|
||||
if (editingGrupoId) onActualizarGrupo(editingGrupoId, { nombre: grupoNombre });
|
||||
else onAgregarGrupo({ nombre: grupoNombre });
|
||||
setGrupoDialogOpen(false);
|
||||
}}>
|
||||
<Save className="h-4 w-4" />
|
||||
<Save className="h-4 w-4 mr-1" /> Guardar
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
@@ -569,8 +569,11 @@ export function MapaCamas({
|
||||
Eliminar
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => setBedDialogOpen(false)}><X className="h-4 w-4 mr-2" />Cancelar</Button>
|
||||
<Button variant="outline" onClick={() => {
|
||||
<Button variant="outline" onClick={() => setBedDialogOpen(false)}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
if (!bedNumero.trim()) return alert('Número requerido');
|
||||
if (editingBed) {
|
||||
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 });
|
||||
}
|
||||
setBedDialogOpen(false);
|
||||
}}>Guardar</Button>
|
||||
}}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Guardar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -155,7 +155,7 @@ export function Pacientes({
|
||||
</div>
|
||||
<Dialog open={dialogoAbierto} onOpenChange={setDialogoAbierto}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" onClick={resetFormulario}>
|
||||
<Button onClick={resetFormulario}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nuevo Paciente
|
||||
</Button>
|
||||
@@ -307,7 +307,7 @@ export function Pacientes({
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button variant="outline"
|
||||
<Button
|
||||
onClick={handleGuardar}
|
||||
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'}
|
||||
</p>
|
||||
{!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" />
|
||||
Agregar primer paciente
|
||||
</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 { 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 { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||
import type { TipoCultivo } from '@/types';
|
||||
|
||||
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 [consoleHistory, setConsoleHistory] = useState<{ id: number; type: 'input' | 'output' | 'error'; content: string }[]>([]);
|
||||
const [logs, setLogs] = useState<string>('');
|
||||
@@ -27,6 +82,178 @@ export function SeccionSistema() {
|
||||
scrollToBottom();
|
||||
}, [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) => {
|
||||
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');
|
||||
}
|
||||
|
||||
toast.success('Base de datos importada exitosamente. Recarga la página para ver los cambios.');
|
||||
// Opcional: window.location.reload();
|
||||
toast.success('Base de datos importada exitosamente.');
|
||||
await refreshState();
|
||||
} catch (error) {
|
||||
toast.error('Error: ' + String(error));
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = ''; // Reset input
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const handleClearConsole = () => {
|
||||
@@ -148,13 +375,201 @@ export function SeccionSistema() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Sistema y Depuración</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400">Herramientas avanzadas para administradores</p>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Sistema y Configuración</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400">Administración de parámetros clínicos, base de datos y depuración</p>
|
||||
</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">
|
||||
<Card className="col-span-1 md:col-span-3">
|
||||
<CardHeader>
|
||||
@@ -267,7 +682,298 @@ export function SeccionSistema() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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 && (
|
||||
<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">
|
||||
|
||||
+8
-1
@@ -159,6 +159,13 @@ export interface AcidoBase {
|
||||
interpretacion?: string;
|
||||
}
|
||||
|
||||
export interface TipoCultivo {
|
||||
id: string;
|
||||
nombre: string;
|
||||
categoria?: string;
|
||||
descripcion?: string;
|
||||
}
|
||||
|
||||
export interface Cultivo {
|
||||
id: string;
|
||||
pacienteId: string;
|
||||
@@ -167,7 +174,7 @@ export interface Cultivo {
|
||||
fechaToma: string;
|
||||
protocolo?: string;
|
||||
fechaResultado?: string;
|
||||
tipoMuestra: 'HMCx2' | 'RC' | 'PC' | 'UC' | 'LCR' | 'LP' | 'LAsc' | 'LAbd' | 'Coleccion' | 'Partes Blandas' | 'Esputo GC' | 'Esputo TBC' | 'Baciloscopia' | 'HNF Test Rápido' | 'HNF Panel PCR' | 'Hisopado Rectal KPC';
|
||||
tipoMuestra: string;
|
||||
germen?: string;
|
||||
sensible?: string;
|
||||
resistente?: string;
|
||||
|
||||
Reference in New Issue
Block a user