Compare commits
67
Commits
d6ac73f79f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfe25e627e | ||
|
|
770a4ea426 | ||
|
|
5b51e4e8eb | ||
|
|
1665c76bba | ||
|
|
2f7f6eeb91 | ||
|
|
b07d6fd8d8 | ||
|
|
6258c4c631 | ||
|
|
5a00688efb | ||
|
|
4be522d35f | ||
|
|
796ff5737e | ||
|
|
652bd363b7 | ||
|
|
fb383b0aea | ||
|
|
174a5f6eef | ||
|
|
3afc8e0a91 | ||
|
|
ea00015030 | ||
|
|
0139cf1caf | ||
|
|
d9b0c7e109 | ||
|
|
fa99607dd2 | ||
|
|
6fb825778f | ||
|
|
ed31b23159 | ||
|
|
2734aad25f | ||
|
|
ead841df6c | ||
|
|
fce539abc2 | ||
|
|
369112d384 | ||
|
|
15049939c3 | ||
|
|
12c522cec3 | ||
|
|
f089b7368f | ||
|
|
ae24747afa | ||
|
|
e3f6b7a478 | ||
|
|
e75f807fc6 | ||
|
|
c06ca1db79 | ||
|
|
827db0a356 | ||
|
|
205c8f1c7c | ||
|
|
e038c25e9e | ||
|
|
d49250460b | ||
|
|
e405ff7a82 | ||
|
|
9180dc09a9 | ||
|
|
ab6f23c7f2 | ||
|
|
4ee30ad8fe | ||
|
|
69c6769342 | ||
|
|
8550b28fd1 | ||
|
|
0d4dbf7940 | ||
|
|
8497ecbb4f | ||
|
|
a5591c55d8 | ||
|
|
34b92ec733 | ||
|
|
ac9d1a5acf | ||
|
|
cce8a0b4c8 | ||
|
|
12f0a7f1e7 | ||
|
|
da7a82502f | ||
|
|
e869d6a19e | ||
|
|
0a0f7782e9 | ||
|
|
34f101aaa5 | ||
|
|
857ce09797 | ||
|
|
42cbe02861 | ||
|
|
6c4defe831 | ||
|
|
01bea69c9d | ||
|
|
cc7854bdd7 | ||
|
|
26869e4b39 | ||
|
|
51ae114a1e | ||
|
|
7c8fe6a0a3 | ||
|
|
23654b57ee | ||
|
|
afaca05916 | ||
|
|
a700c40d20 | ||
|
|
ca2f8a32e1 | ||
|
|
bf62f94380 | ||
|
|
32dd26620e | ||
|
|
d34c21fe60 |
@@ -1,10 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let code = fs.readFileSync('src/App.tsx', 'utf8');
|
||||
|
||||
code = code.replace(/store\.getLaboratoriosByPaciente\(paciente\.id\)/g, 'store.getLaboratoriosByInternacion(internacion.id)');
|
||||
code = code.replace(/store\.getGlucemiasByPaciente\(paciente\.id\)/g, 'store.getGlucemiasByInternacion(internacion.id)');
|
||||
code = code.replace(/store\.getAcidosBaseByPaciente\(paciente\.id\)/g, 'store.getAcidosBaseByInternacion(internacion.id)');
|
||||
code = code.replace(/store\.getCultivosByPaciente\(paciente\.id\)/g, 'store.getCultivosByInternacion(internacion.id)');
|
||||
code = code.replace(/store\.estudiosComplementarios\.filter\(e => e\.pacienteId === paciente\.id\)/g, 'store.estudiosComplementarios.filter(e => e.internacionId === internacion.id)');
|
||||
|
||||
fs.writeFileSync('src/App.tsx', code);
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let code = fs.readFileSync('server/db.js', 'utf8');
|
||||
|
||||
// Add computeSector function
|
||||
const computeSectorCode = `
|
||||
function computeSector(numero) {
|
||||
if (!numero) return 'En Área';
|
||||
const parts = String(numero).trim().split('-');
|
||||
if (parts.length === 0) return 'En Área';
|
||||
const salaStr = parts[0].trim();
|
||||
const salaNum = parseInt(salaStr, 10);
|
||||
if (isNaN(salaNum)) return 'En Área';
|
||||
if (salaNum >= 300 && salaNum < 400 && salaNum % 2 !== 0) {
|
||||
return 'Fuera de Área';
|
||||
}
|
||||
if (salaNum >= 400 && salaNum < 500) {
|
||||
return 'Fuera de Área';
|
||||
}
|
||||
return 'En Área';
|
||||
}
|
||||
`;
|
||||
|
||||
// Find where to insert computeSector
|
||||
code = code.replace("export function createCama(cama) {", computeSectorCode + "\nexport function createCama(cama) {");
|
||||
|
||||
// Update createCama
|
||||
code = code.replace(
|
||||
/export function createCama\(cama\) \{\s*const gId = cama.grupoId \|\| cama.areaId \|\| null;\s*db.prepare\('INSERT INTO camas \(id, numero, grupoId, areaId, tipo, estado, pacienteId, internacionId\) VALUES \(\?, \?, \?, \?, \?, \?, \?, \?\)'\)\.run\(cama.id, cama.numero, gId, gId, cama.tipo \|\| 'Estándar', cama.estado \|\| 'Disponible', cama.pacienteId \|\| null, cama.internacionId \|\| null\);\s*\}/,
|
||||
`export function createCama(cama) {
|
||||
const gId = cama.grupoId || cama.areaId || null;
|
||||
const sector = computeSector(cama.numero);
|
||||
db.prepare('INSERT INTO camas (id, numero, grupoId, areaId, tipo, estado, pacienteId, internacionId, sector) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)').run(cama.id, cama.numero, gId, gId, cama.tipo || 'Estándar', cama.estado || 'Disponible', cama.pacienteId || null, cama.internacionId || null, sector);
|
||||
}`
|
||||
);
|
||||
|
||||
// Update updateCama
|
||||
code = code.replace(
|
||||
/export function updateCama\(id, updates\) \{[\s\S]*?if \(fields\.length > 0\) \{/,
|
||||
`export function updateCama(id, updates) {
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (updates.numero !== undefined) {
|
||||
fields.push('numero = ?');
|
||||
values.push(updates.numero);
|
||||
fields.push('sector = ?');
|
||||
values.push(computeSector(updates.numero));
|
||||
}
|
||||
if (updates.tipo !== undefined) {
|
||||
fields.push('tipo = ?');
|
||||
values.push(updates.tipo);
|
||||
}
|
||||
if (updates.grupoId !== undefined) {
|
||||
fields.push('grupoId = ?');
|
||||
values.push(updates.grupoId);
|
||||
fields.push('areaId = ?');
|
||||
values.push(updates.grupoId);
|
||||
}
|
||||
if (updates.estado !== undefined) {
|
||||
fields.push('estado = ?');
|
||||
values.push(updates.estado);
|
||||
}
|
||||
if (updates.pacienteId !== undefined) {
|
||||
fields.push('pacienteId = ?');
|
||||
values.push(updates.pacienteId);
|
||||
}
|
||||
if (updates.internacionId !== undefined) {
|
||||
fields.push('internacionId = ?');
|
||||
values.push(updates.internacionId);
|
||||
}
|
||||
|
||||
if (fields.length > 0) {`
|
||||
);
|
||||
|
||||
// Update table schema
|
||||
code = code.replace(
|
||||
/CREATE TABLE IF NOT EXISTS camas \(\s*id TEXT PRIMARY KEY,\s*numero TEXT NOT NULL,\s*grupoId TEXT,\s*areaId TEXT,\s*tipo TEXT DEFAULT 'Estándar',\s*estado TEXT DEFAULT 'Disponible',\s*pacienteId TEXT,\s*internacionId TEXT\s*\);/,
|
||||
`CREATE TABLE IF NOT EXISTS camas (
|
||||
id TEXT PRIMARY KEY,
|
||||
numero TEXT NOT NULL,
|
||||
grupoId TEXT,
|
||||
areaId TEXT,
|
||||
tipo TEXT DEFAULT 'Estándar',
|
||||
estado TEXT DEFAULT 'Disponible',
|
||||
pacienteId TEXT,
|
||||
internacionId TEXT,
|
||||
sector TEXT DEFAULT 'En Área'
|
||||
);
|
||||
try {
|
||||
db.prepare("ALTER TABLE camas ADD COLUMN sector TEXT DEFAULT 'En Área'").run();
|
||||
} catch(e) {}
|
||||
try {
|
||||
// Initialize existing beds with their sector
|
||||
const rows = db.prepare('SELECT id, numero FROM camas WHERE sector IS NULL OR sector = "En Área"').all();
|
||||
for (const row of rows) {
|
||||
if (!row.numero) continue;
|
||||
const salaStr = String(row.numero).trim().split('-')[0].trim();
|
||||
const salaNum = parseInt(salaStr, 10);
|
||||
let s = 'En Área';
|
||||
if (!isNaN(salaNum)) {
|
||||
if (salaNum >= 300 && salaNum < 400 && salaNum % 2 !== 0) s = 'Fuera de Área';
|
||||
else if (salaNum >= 400 && salaNum < 500) s = 'Fuera de Área';
|
||||
}
|
||||
db.prepare('UPDATE camas SET sector = ? WHERE id = ?').run(s, row.id);
|
||||
}
|
||||
} catch(e) {}`
|
||||
);
|
||||
|
||||
fs.writeFileSync('server/db.js', code);
|
||||
@@ -1,10 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let code = fs.readFileSync('server/db.js', 'utf8');
|
||||
|
||||
code = code.replace(
|
||||
/const __dirname = dirname\(fileURLToPath\(import\.meta\.url\)\);/,
|
||||
`const currentDir = typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url));`
|
||||
);
|
||||
code = code.replace(/__dirname/g, "currentDir");
|
||||
|
||||
fs.writeFileSync('server/db.js', code);
|
||||
@@ -1,9 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let code = fs.readFileSync('server/db.js', 'utf8');
|
||||
|
||||
code = code.replace(
|
||||
/const currentDir = typeof currentDir !== 'undefined' \? currentDir : dirname\(fileURLToPath\(import\.meta\.url\)\);/,
|
||||
`const currentDir = typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url));`
|
||||
);
|
||||
|
||||
fs.writeFileSync('server/db.js', code);
|
||||
@@ -1,40 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let code = fs.readFileSync('server/index.js', 'utf8');
|
||||
|
||||
const computeSectorCode = `
|
||||
function computeSector(numero) {
|
||||
if (!numero) return 'En Área';
|
||||
const parts = String(numero).trim().split('-');
|
||||
if (parts.length === 0) return 'En Área';
|
||||
const salaStr = parts[0].trim();
|
||||
const salaNum = parseInt(salaStr, 10);
|
||||
if (isNaN(salaNum)) return 'En Área';
|
||||
if (salaNum >= 300 && salaNum < 400 && salaNum % 2 !== 0) return 'Fuera de Área';
|
||||
if (salaNum >= 400 && salaNum < 500) return 'Fuera de Área';
|
||||
return 'En Área';
|
||||
}
|
||||
`;
|
||||
|
||||
// Insert computeSector somewhere at the top
|
||||
code = code.replace("const PORT = process.env.PORT || 3000;", computeSectorCode + "\nconst PORT = process.env.PORT || 3000;");
|
||||
|
||||
code = code.replace(
|
||||
/const stmt = db\.prepare\('INSERT OR IGNORE INTO camas \(id, numero, grupoId, areaId, tipo, estado\) VALUES \(\?, \?, \?, \?, \?, \?\)'\);\s*for \(const c of state\.camas\) \{\s*const gId = c\.grupoId \|\| c\.areaId \|\| null;\s*stmt\.run\(c\.id, c\.numero, gId, gId, c\.tipo, c\.estado\);\s*\}/,
|
||||
`const stmt = db.prepare('INSERT OR IGNORE INTO camas (id, numero, grupoId, areaId, tipo, estado, sector) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
||||
for (const c of state.camas) {
|
||||
const gId = c.grupoId || c.areaId || null;
|
||||
const sector = computeSector(c.numero);
|
||||
stmt.run(c.id, c.numero, gId, gId, c.tipo, c.estado, sector);
|
||||
}`
|
||||
);
|
||||
|
||||
code = code.replace(
|
||||
/const stmt = db\.prepare\('INSERT INTO camas \(id, numero, areaId, tipo, estado\) VALUES \(\?, \?, \?, \?, \?\)'\);\s*for \(const c of updates\.camas\) \{\s*stmt\.run\(c\.id, c\.numero, c\.areaId, c\.tipo, c\.estado\);\s*\}/,
|
||||
`const stmt = db.prepare('INSERT INTO camas (id, numero, grupoId, areaId, tipo, estado, sector) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
||||
for (const c of updates.camas) {
|
||||
const sector = computeSector(c.numero);
|
||||
stmt.run(c.id, c.numero, c.grupoId || c.areaId || null, c.grupoId || c.areaId || null, c.tipo, c.estado, sector);
|
||||
}`
|
||||
);
|
||||
|
||||
fs.writeFileSync('server/index.js', code);
|
||||
@@ -1,31 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let code = fs.readFileSync('server/db.js', 'utf8');
|
||||
|
||||
code = code.replace(
|
||||
/ \);\n try \{\n db\.prepare\("ALTER TABLE camas ADD COLUMN sector TEXT DEFAULT 'En Área'"\)\.run\(\);\n \} catch\(e\) \{\}\n try \{\n \/\/ Initialize existing beds with their sector\n const rows = db\.prepare\('SELECT id, numero FROM camas WHERE sector IS NULL OR sector = "En Área"'\)\.all\(\);\n for \(const row of rows\) \{\n if \(\!row\.numero\) continue;\n const salaStr = String\(row\.numero\)\.trim\(\)\.split\('-'\)\[0\]\.trim\(\);\n const salaNum = parseInt\(salaStr, 10\);\n let s = 'En Área';\n if \(\!isNaN\(salaNum\)\) \{\n if \(salaNum >= 300 && salaNum < 400 && salaNum % 2 !== 0\) s = 'Fuera de Área';\n else if \(salaNum >= 400 && salaNum < 500\) s = 'Fuera de Área';\n \}\n db\.prepare\('UPDATE camas SET sector = \? WHERE id = \?'\)\.run\(s, row\.id\);\n \}\n \} catch\(e\) \{\}/,
|
||||
` );`
|
||||
);
|
||||
|
||||
code = code.replace(
|
||||
/\/\/ Seed default 4 grupos if empty/,
|
||||
`try { db.exec("ALTER TABLE camas ADD COLUMN sector TEXT DEFAULT 'En Área'"); } catch (_) {}
|
||||
try {
|
||||
// Initialize existing beds with their sector
|
||||
const rows = db.prepare('SELECT id, numero FROM camas WHERE sector IS NULL OR sector = "En Área"').all();
|
||||
for (const row of rows) {
|
||||
if (!row.numero) continue;
|
||||
const salaStr = String(row.numero).trim().split('-')[0].trim();
|
||||
const salaNum = parseInt(salaStr, 10);
|
||||
let s = 'En Área';
|
||||
if (!isNaN(salaNum)) {
|
||||
if (salaNum >= 300 && salaNum < 400 && salaNum % 2 !== 0) s = 'Fuera de Área';
|
||||
else if (salaNum >= 400 && salaNum < 500) s = 'Fuera de Área';
|
||||
}
|
||||
db.prepare('UPDATE camas SET sector = ? WHERE id = ?').run(s, row.id);
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Seed default 4 grupos if empty`
|
||||
);
|
||||
|
||||
fs.writeFileSync('server/db.js', code);
|
||||
@@ -1,21 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let code = fs.readFileSync('src/hooks/useHospitalStore.ts', 'utf8');
|
||||
|
||||
code = code.replace(/getLaboratoriosByPaciente = useCallback\(\(pacienteId: string\)/g, 'getLaboratoriosByInternacion = useCallback((internacionId: string)');
|
||||
code = code.replace(/\.filter\(l => l\.pacienteId === pacienteId\)/g, '.filter(l => l.internacionId === internacionId)');
|
||||
|
||||
code = code.replace(/getGlucemiasByPaciente = useCallback\(\(pacienteId: string\)/g, 'getGlucemiasByInternacion = useCallback((internacionId: string)');
|
||||
code = code.replace(/\.filter\(g => g\.pacienteId === pacienteId\)/g, '.filter(g => g.internacionId === internacionId)');
|
||||
|
||||
code = code.replace(/getAcidosBaseByPaciente = useCallback\(\(pacienteId: string\)/g, 'getAcidosBaseByInternacion = useCallback((internacionId: string)');
|
||||
code = code.replace(/\.filter\(a => a\.pacienteId === pacienteId\)/g, '.filter(a => a.internacionId === internacionId)');
|
||||
|
||||
code = code.replace(/getCultivosByPaciente = useCallback\(\(pacienteId: string\)/g, 'getCultivosByInternacion = useCallback((internacionId: string)');
|
||||
code = code.replace(/\.filter\(c => c\.pacienteId === pacienteId\)/g, '.filter(c => c.internacionId === internacionId)');
|
||||
|
||||
code = code.replace(/getLaboratoriosByPaciente,/g, 'getLaboratoriosByInternacion,');
|
||||
code = code.replace(/getGlucemiasByPaciente,/g, 'getGlucemiasByInternacion,');
|
||||
code = code.replace(/getAcidosBaseByPaciente,/g, 'getAcidosBaseByInternacion,');
|
||||
code = code.replace(/getCultivosByPaciente,/g, 'getCultivosByInternacion,');
|
||||
|
||||
fs.writeFileSync('src/hooks/useHospitalStore.ts', code);
|
||||
@@ -1,13 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let code = fs.readFileSync('src/hooks/useHospitalStore.ts', 'utf8');
|
||||
|
||||
code = code.replace(
|
||||
/try \{\s*await apiCall\('PUT', `\/camas\/\$\{id\}`/,
|
||||
`try {
|
||||
if (datos.numero !== undefined) {
|
||||
datos.sector = computeSector(datos.numero);
|
||||
}
|
||||
await apiCall('PUT', \`/camas/\${id}\``
|
||||
);
|
||||
|
||||
fs.writeFileSync('src/hooks/useHospitalStore.ts', code);
|
||||
@@ -1,16 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let code = fs.readFileSync('src/lib/utils.ts', 'utf8');
|
||||
|
||||
code = code.replace(
|
||||
/export function isCamaFueraDeGrupo\([\s\S]*?return false;\n\}/,
|
||||
`export function isCamaFueraDeGrupo(
|
||||
cama: { numero: string; grupoId?: string; areaId?: string; sector?: string },
|
||||
gruposOrAreas?: { id: string; nombre: string }[]
|
||||
): boolean {
|
||||
if (cama.sector === 'Fuera de Área') return true;
|
||||
if (cama.sector === 'En Área') return false;
|
||||
return computeSector(cama.numero) === 'Fuera de Área';
|
||||
}`
|
||||
);
|
||||
|
||||
fs.writeFileSync('src/lib/utils.ts', code);
|
||||
Binary file not shown.
+401
-33
@@ -8,17 +8,55 @@ import { initDb,
|
||||
getAllInternaciones, getInternacionById, createInternacion, updateInternacion, deleteInternacion,
|
||||
getAllEvoluciones, createEvolucion, updateEvolucion, deleteEvolucion,
|
||||
getAllLaboratorios, createLaboratorio, updateLaboratorio, deleteLaboratorio,
|
||||
getAllOtrosLaboratorios, createOtroLaboratorio, updateOtroLaboratorio, deleteOtroLaboratorio,
|
||||
getAllGlucemias, createGlucemia, updateGlucemia, deleteGlucemia,
|
||||
getAllAcidosBase, createAcidoBase, updateAcidoBase, deleteAcidoBase,
|
||||
getAllCultivos, createCultivo, updateCultivo, deleteCultivo,
|
||||
getAllTiposCultivo, createTipoCultivo, updateTipoCultivo, deleteTipoCultivo, restablecerTiposCultivo,
|
||||
getAllGruposLaboratorio, createGrupoLaboratorio, updateGrupoLaboratorio, deleteGrupoLaboratorio, restablecerGruposLaboratorio,
|
||||
getAllEstudiosComplementarios, createEstudioComplementario, updateEstudioComplementario, deleteEstudioComplementario,
|
||||
getAllInterconsultas, createInterconsulta, updateInterconsulta, deleteInterconsulta,
|
||||
getAllAtb, createAtb, updateAtb, deleteAtb,
|
||||
getAllIndicaciones, createIndicacion, updateIndicacion, deleteIndicacion,
|
||||
getAllMovimientosIndicaciones, createMovimientoIndicacion,
|
||||
getValue, setValue
|
||||
getAllPendientes, createPendiente, updatePendiente, deletePendiente,
|
||||
getValue, setValue,
|
||||
getDb, exportAllData, importAllData
|
||||
} from './db-mongodb.js';
|
||||
|
||||
// Capture live logs in-memory
|
||||
const logBuffer = [];
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
function addLog(type, args) {
|
||||
const message = args.map(arg => {
|
||||
if (typeof arg === 'object') {
|
||||
try { return JSON.stringify(arg); } catch { return String(arg); }
|
||||
}
|
||||
return String(arg);
|
||||
}).join(' ');
|
||||
const logEntry = `[${new Date().toISOString()}] [${type}] ${message}`;
|
||||
logBuffer.push(logEntry);
|
||||
if (logBuffer.length > 500) {
|
||||
logBuffer.shift();
|
||||
}
|
||||
}
|
||||
|
||||
console.log = (...args) => {
|
||||
addLog('INFO', args);
|
||||
originalLog.apply(console, args);
|
||||
};
|
||||
console.error = (...args) => {
|
||||
addLog('ERROR', args);
|
||||
originalError.apply(console, args);
|
||||
};
|
||||
console.warn = (...args) => {
|
||||
addLog('WARN', args);
|
||||
originalWarn.apply(console, args);
|
||||
};
|
||||
|
||||
const app = express();
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
@@ -41,29 +79,87 @@ function generateUUID() {
|
||||
// ========== STATE ENDPOINT (initial load) ==========
|
||||
app.get('/api/state', async (req, res) => {
|
||||
try {
|
||||
const areasList = await getAllAreas();
|
||||
const usuariosList = await getAllUsuarios();
|
||||
const state = {
|
||||
pacientes: await getAllPacientes(),
|
||||
areas: areasList,
|
||||
grupos: areasList,
|
||||
camas: await getAllCamas(),
|
||||
internaciones: await getAllInternaciones(),
|
||||
evoluciones: (await getAllEvoluciones()).map(e => ({
|
||||
const [
|
||||
pacientes,
|
||||
areasList,
|
||||
camas,
|
||||
internaciones,
|
||||
rawEvoluciones,
|
||||
rawLaboratorios,
|
||||
otrosLaboratorios,
|
||||
glucemias,
|
||||
acidosBase,
|
||||
cultivos,
|
||||
tiposCultivo,
|
||||
gruposDeterminacionesLab,
|
||||
estudiosComplementarios,
|
||||
rawInterconsultas,
|
||||
atb,
|
||||
indicaciones,
|
||||
movimientosIndicaciones,
|
||||
pendientes,
|
||||
usuariosList
|
||||
] = await Promise.all([
|
||||
getAllPacientes(),
|
||||
getAllAreas(),
|
||||
getAllCamas(),
|
||||
getAllInternaciones(),
|
||||
getAllEvoluciones(),
|
||||
getAllLaboratorios(),
|
||||
getAllOtrosLaboratorios(),
|
||||
getAllGlucemias(),
|
||||
getAllAcidosBase(),
|
||||
getAllCultivos(),
|
||||
getAllTiposCultivo(),
|
||||
getAllGruposLaboratorio(),
|
||||
getAllEstudiosComplementarios(),
|
||||
getAllInterconsultas(),
|
||||
getAllAtb(),
|
||||
getAllIndicaciones(),
|
||||
getAllMovimientosIndicaciones(),
|
||||
getAllPendientes(),
|
||||
getAllUsuarios()
|
||||
]);
|
||||
|
||||
const evoluciones = rawEvoluciones.map(e => ({
|
||||
...e,
|
||||
signosVitales: typeof e.signosVitales === 'string' ? JSON.parse(e.signosVitales) : e.signosVitales,
|
||||
examenFisico: typeof e.examenFisico === 'string' ? JSON.parse(e.examenFisico) : e.examenFisico
|
||||
})),
|
||||
laboratorios: (await getAllLaboratorios()).map(l => ({ ...l, resultados: typeof l.resultados === 'string' ? JSON.parse(l.resultados) : l.resultados })),
|
||||
glucemias: await getAllGlucemias(),
|
||||
acidosBase: await getAllAcidosBase(),
|
||||
cultivos: await getAllCultivos(),
|
||||
estudiosComplementarios: await getAllEstudiosComplementarios(),
|
||||
interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })),
|
||||
atb: await getAllAtb(),
|
||||
indicaciones: await getAllIndicaciones(),
|
||||
movimientosIndicaciones: await getAllMovimientosIndicaciones(),
|
||||
usuarios: usuariosList.map(({ passwordHash, ...u }) => u),
|
||||
}));
|
||||
|
||||
const laboratorios = rawLaboratorios.map(l => ({
|
||||
...l,
|
||||
resultados: typeof l.resultados === 'string' ? JSON.parse(l.resultados) : l.resultados
|
||||
}));
|
||||
|
||||
const interconsultas = rawInterconsultas.map(ic => ({
|
||||
...ic,
|
||||
realizada: !!ic.realizada
|
||||
}));
|
||||
|
||||
const usuarios = usuariosList.map(({ passwordHash, ...u }) => u);
|
||||
|
||||
const state = {
|
||||
pacientes,
|
||||
areas: areasList,
|
||||
grupos: areasList,
|
||||
camas,
|
||||
internaciones,
|
||||
evoluciones,
|
||||
laboratorios,
|
||||
otrosLaboratorios,
|
||||
glucemias,
|
||||
acidosBase,
|
||||
cultivos,
|
||||
tiposCultivo,
|
||||
gruposDeterminacionesLab,
|
||||
estudiosComplementarios,
|
||||
interconsultas,
|
||||
atb,
|
||||
indicaciones,
|
||||
movimientosIndicaciones,
|
||||
pendientes,
|
||||
usuarios,
|
||||
vistaActual: 'dashboard',
|
||||
currentInternacionId: null
|
||||
};
|
||||
@@ -162,7 +258,7 @@ app.get('/api/pacientes', async (req, res) => {
|
||||
|
||||
app.post('/api/pacientes', async (req, res) => {
|
||||
try {
|
||||
const paciente = { ...req.body, id: generateUUID() };
|
||||
const paciente = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createPaciente(paciente);
|
||||
res.json(paciente);
|
||||
} catch (err) {
|
||||
@@ -276,7 +372,7 @@ app.get('/api/internaciones', async (req, res) => {
|
||||
|
||||
app.post('/api/internaciones', async (req, res) => {
|
||||
try {
|
||||
const internacion = { ...req.body, id: generateUUID(), activa: true };
|
||||
const internacion = { ...req.body, id: req.body.id || generateUUID(), activa: req.body.activa !== undefined ? req.body.activa : true };
|
||||
await createInternacion(internacion);
|
||||
res.json(internacion);
|
||||
} catch (err) {
|
||||
@@ -313,7 +409,7 @@ app.get('/api/evoluciones', async (req, res) => {
|
||||
|
||||
app.post('/api/evoluciones', async (req, res) => {
|
||||
try {
|
||||
const evolucion = { ...req.body, id: generateUUID() };
|
||||
const evolucion = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createEvolucion(evolucion);
|
||||
res.json(evolucion);
|
||||
} catch (err) {
|
||||
@@ -350,7 +446,7 @@ app.get('/api/laboratorios', async (req, res) => {
|
||||
|
||||
app.post('/api/laboratorios', async (req, res) => {
|
||||
try {
|
||||
const laboratorio = { ...req.body, id: generateUUID() };
|
||||
const laboratorio = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createLaboratorio(laboratorio);
|
||||
res.json(laboratorio);
|
||||
} catch (err) {
|
||||
@@ -376,6 +472,61 @@ app.delete('/api/laboratorios/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ========== OTROS LABORATORIOS ==========
|
||||
app.get('/api/otros-laboratorios', async (req, res) => {
|
||||
try {
|
||||
const records = await getAllOtrosLaboratorios();
|
||||
res.json(records);
|
||||
} catch (error) {
|
||||
console.error('Error fetching otros-laboratorios:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch otros-laboratorios' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/otros-laboratorios', async (req, res) => {
|
||||
try {
|
||||
const { pacienteId, fecha } = req.body;
|
||||
if (!pacienteId || !fecha) {
|
||||
return res.status(400).json({ error: 'pacienteId and fecha are required' });
|
||||
}
|
||||
|
||||
const record = {
|
||||
id: generateUUID(),
|
||||
observaciones: '',
|
||||
...req.body
|
||||
};
|
||||
|
||||
const newRecord = await createOtroLaboratorio(record);
|
||||
res.status(201).json(newRecord);
|
||||
} catch (error) {
|
||||
console.error('Error creating otro laboratorio:', error);
|
||||
res.status(500).json({ error: 'Failed to create otro laboratorio' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/otros-laboratorios/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await updateOtroLaboratorio(req.params.id, req.body);
|
||||
if (!result.success) return res.status(404).json({ error: 'Record not found' });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Error updating otro laboratorio:', error);
|
||||
res.status(500).json({ error: 'Failed to update otro laboratorio' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/otros-laboratorios/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await deleteOtroLaboratorio(req.params.id);
|
||||
if (!result.success) return res.status(404).json({ error: 'Record not found' });
|
||||
res.json({ message: 'Deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting otro laboratorio:', error);
|
||||
res.status(500).json({ error: 'Failed to delete otro laboratorio' });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== GLUCEMIAS ==========
|
||||
app.get('/api/glucemias', async (req, res) => {
|
||||
try {
|
||||
@@ -387,7 +538,7 @@ app.get('/api/glucemias', async (req, res) => {
|
||||
|
||||
app.post('/api/glucemias', async (req, res) => {
|
||||
try {
|
||||
const glucemia = { ...req.body, id: generateUUID() };
|
||||
const glucemia = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createGlucemia(glucemia);
|
||||
res.json(glucemia);
|
||||
} catch (err) {
|
||||
@@ -424,7 +575,7 @@ app.get('/api/acid-os-base', async (req, res) => {
|
||||
|
||||
app.post('/api/acid-os-base', async (req, res) => {
|
||||
try {
|
||||
const acido = { ...req.body, id: generateUUID() };
|
||||
const acido = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createAcidoBase(acido);
|
||||
res.json(acido);
|
||||
} catch (err) {
|
||||
@@ -461,7 +612,7 @@ app.get('/api/cultivos', async (req, res) => {
|
||||
|
||||
app.post('/api/cultivos', async (req, res) => {
|
||||
try {
|
||||
const cultivo = { ...req.body, id: generateUUID() };
|
||||
const cultivo = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createCultivo(cultivo);
|
||||
res.json(cultivo);
|
||||
} catch (err) {
|
||||
@@ -487,6 +638,96 @@ 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' });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== GRUPOS DE DETERMINACIONES DE LABORATORIO ==========
|
||||
app.get('/api/grupos-laboratorio', async (req, res) => {
|
||||
try {
|
||||
res.json(await getAllGruposLaboratorio());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al obtener grupos de determinaciones de laboratorio' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/grupos-laboratorio', async (req, res) => {
|
||||
try {
|
||||
const nuevo = await createGrupoLaboratorio(req.body);
|
||||
res.json(nuevo);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message || 'Error al crear grupo de determinaciones' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/grupos-laboratorio/:id', async (req, res) => {
|
||||
try {
|
||||
const updated = await updateGrupoLaboratorio(req.params.id, req.body);
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message || 'Error al actualizar grupo de determinaciones' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/grupos-laboratorio/:id', async (req, res) => {
|
||||
try {
|
||||
await deleteGrupoLaboratorio(req.params.id);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al eliminar grupo de determinaciones' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/grupos-laboratorio/reset', async (req, res) => {
|
||||
try {
|
||||
const list = await restablecerGruposLaboratorio();
|
||||
res.json(list);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al restablecer grupos de determinaciones' });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ESTUDIOS COMPLEMENTARIOS ==========
|
||||
app.get('/api/estudios-complementarios', async (req, res) => {
|
||||
try {
|
||||
@@ -498,7 +739,7 @@ app.get('/api/estudios-complementarios', async (req, res) => {
|
||||
|
||||
app.post('/api/estudios-complementarios', async (req, res) => {
|
||||
try {
|
||||
const estudio = { ...req.body, id: generateUUID() };
|
||||
const estudio = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createEstudioComplementario(estudio);
|
||||
res.json(estudio);
|
||||
} catch (err) {
|
||||
@@ -535,7 +776,7 @@ app.get('/api/interconsultas', async (req, res) => {
|
||||
|
||||
app.post('/api/interconsultas', async (req, res) => {
|
||||
try {
|
||||
const interconsulta = { ...req.body, id: generateUUID() };
|
||||
const interconsulta = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createInterconsulta(interconsulta);
|
||||
res.json(interconsulta);
|
||||
} catch (err) {
|
||||
@@ -572,7 +813,7 @@ app.get('/api/atb', async (req, res) => {
|
||||
|
||||
app.post('/api/atb', async (req, res) => {
|
||||
try {
|
||||
const atb = { ...req.body, id: generateUUID() };
|
||||
const atb = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createAtb(atb);
|
||||
res.json(atb);
|
||||
} catch (err) {
|
||||
@@ -609,7 +850,7 @@ app.get('/api/indicaciones', async (req, res) => {
|
||||
|
||||
app.post('/api/indicaciones', async (req, res) => {
|
||||
try {
|
||||
const indicacion = { ...req.body, id: generateUUID() };
|
||||
const indicacion = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createIndicacion(indicacion);
|
||||
res.json(indicacion);
|
||||
} catch (err) {
|
||||
@@ -646,7 +887,7 @@ app.get('/api/movimientos-indicaciones', async (req, res) => {
|
||||
|
||||
app.post('/api/movimientos-indicaciones', async (req, res) => {
|
||||
try {
|
||||
const movimiento = { ...req.body, id: generateUUID() };
|
||||
const movimiento = { ...req.body, id: req.body.id || generateUUID() };
|
||||
console.log('Creating movimiento:', movimiento);
|
||||
await createMovimientoIndicacion(movimiento);
|
||||
res.json(movimiento);
|
||||
@@ -656,6 +897,43 @@ app.post('/api/movimientos-indicaciones', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== PENDIENTES ==========
|
||||
app.get('/api/pendientes', async (req, res) => {
|
||||
try {
|
||||
res.json(await getAllPendientes());
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al obtener pendientes' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/pendientes', async (req, res) => {
|
||||
try {
|
||||
const pendiente = { ...req.body, id: req.body.id || generateUUID() };
|
||||
await createPendiente(pendiente);
|
||||
res.json(pendiente);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al crear pendiente' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/pendientes/:id', async (req, res) => {
|
||||
try {
|
||||
await updatePendiente(req.params.id, req.body);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al actualizar pendiente' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/pendientes/:id', async (req, res) => {
|
||||
try {
|
||||
await deletePendiente(req.params.id);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al eliminar pendiente' });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== AUTH ==========
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
try {
|
||||
@@ -721,6 +999,96 @@ app.put('/api/auth/update-email', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ADMIN SYSTEM ENDPOINTS ==========
|
||||
|
||||
app.post('/api/admin/mongosh', async (req, res) => {
|
||||
try {
|
||||
const { command } = req.body;
|
||||
if (!command) {
|
||||
return res.status(400).json({ error: 'Comando requerido' });
|
||||
}
|
||||
|
||||
const dbInstance = getDb();
|
||||
if (!dbInstance) {
|
||||
return res.json({ result: 'Advertencia: MongoDB no está conectado. Utilizando store en memoria. Las consultas de mongosh no están disponibles.', isMemStore: true });
|
||||
}
|
||||
|
||||
const dbProxy = new Proxy(dbInstance, {
|
||||
get(target, prop) {
|
||||
if (typeof target[prop] !== 'undefined') {
|
||||
if (typeof target[prop] === 'function') {
|
||||
return target[prop].bind(target);
|
||||
}
|
||||
return target[prop];
|
||||
}
|
||||
return target.collection(prop);
|
||||
}
|
||||
});
|
||||
|
||||
const { ObjectId } = await import('mongodb');
|
||||
|
||||
const evalFn = new Function('db', 'ObjectId', `
|
||||
return (async () => {
|
||||
${command.trim().includes('return') || command.trim().startsWith('{') || command.trim().includes(';') ? command : 'return (' + command + ')'}
|
||||
})();
|
||||
`);
|
||||
|
||||
let finalResult = await evalFn(dbProxy, ObjectId);
|
||||
|
||||
// Si el resultado es un cursor de MongoDB (ej. db.collection.find()), lo convertimos a array
|
||||
if (finalResult && typeof finalResult === 'object' && typeof finalResult.toArray === 'function') {
|
||||
finalResult = await finalResult.toArray();
|
||||
}
|
||||
|
||||
res.json({ result: finalResult });
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message || String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/admin/db/export', async (req, res) => {
|
||||
try {
|
||||
const dump = await exportAllData();
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=hospital_backup.json');
|
||||
res.json(dump);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al exportar base de datos: ' + err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/admin/db/import', async (req, res) => {
|
||||
try {
|
||||
const dump = req.body;
|
||||
if (!dump || typeof dump !== 'object') {
|
||||
return res.status(400).json({ error: 'Formato de importación inválido' });
|
||||
}
|
||||
await importAllData(dump);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Error al importar base de datos: ' + err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/admin/logs', async (req, res) => {
|
||||
try {
|
||||
let fileLogs = '';
|
||||
try {
|
||||
const fs = await import('fs/promises');
|
||||
fileLogs = await fs.readFile('server_log.txt', 'utf8');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const memoryLogs = logBuffer.join('\n');
|
||||
const combined = [fileLogs, '\n--- LOGS DE LA SESION EN VIVO ---', memoryLogs].filter(Boolean).join('\n');
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(combined);
|
||||
} catch (err) {
|
||||
res.status(500).send('Error al leer logs: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize DB and start server
|
||||
const PORT = process.env.PORT || 4000;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
+587
-3
@@ -1,4 +1,4 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
import { MongoClient, ObjectId } from 'mongodb';
|
||||
import bcrypt from 'bcryptjs';
|
||||
const { compareSync, hashSync } = bcrypt;
|
||||
|
||||
@@ -56,6 +56,7 @@ const memStore = {
|
||||
internaciones: [],
|
||||
evoluciones: [],
|
||||
laboratorios: [],
|
||||
otrosLaboratorios: [],
|
||||
glucemias: [],
|
||||
acidosbase: [],
|
||||
cultivos: [],
|
||||
@@ -64,6 +65,9 @@ const memStore = {
|
||||
atb: [],
|
||||
indicaciones: [],
|
||||
movimientos_indicaciones: [],
|
||||
pendientes: [],
|
||||
tipos_cultivo: [],
|
||||
grupos_laboratorio: [],
|
||||
kv: {}
|
||||
};
|
||||
|
||||
@@ -102,6 +106,9 @@ export async function initDb() {
|
||||
function cleanDoc(doc) {
|
||||
if (!doc) return null;
|
||||
const { _id, ...rest } = doc;
|
||||
if (!rest.id && _id) {
|
||||
rest.id = _id.toString();
|
||||
}
|
||||
return rest;
|
||||
}
|
||||
|
||||
@@ -352,12 +359,21 @@ export async function getAllInternaciones() {
|
||||
}
|
||||
|
||||
export async function getInternacionById(id) {
|
||||
if (!id) return null;
|
||||
const idStr = String(id).trim();
|
||||
if (db) {
|
||||
const internacion = await db.collection('internaciones').findOne({ id });
|
||||
let internacion = await db.collection('internaciones').findOne({ id: idStr });
|
||||
if (!internacion && ObjectId.isValid(idStr)) {
|
||||
try {
|
||||
internacion = await db.collection('internaciones').findOne({ _id: new ObjectId(idStr) });
|
||||
} catch {
|
||||
// ignore invalid objectid
|
||||
}
|
||||
}
|
||||
if (internacion) internacion.activa = !!internacion.activa;
|
||||
return cleanDoc(internacion);
|
||||
}
|
||||
const internacion = memStore.internaciones.find(i => i.id === id);
|
||||
const internacion = memStore.internaciones.find(i => i.id === idStr || String(i.id).trim() === idStr);
|
||||
if (!internacion) return null;
|
||||
return { ...internacion, activa: !!internacion.activa };
|
||||
}
|
||||
@@ -379,6 +395,7 @@ export async function createInternacion(internacion) {
|
||||
diagnosticoEgreso: internacion.diagnosticoEgreso || null,
|
||||
medicoIngresante: internacion.medicoIngresante || null,
|
||||
motivoEgreso: internacion.motivoEgreso || null,
|
||||
servicioAlQuePasa: internacion.servicioAlQuePasa || null,
|
||||
activa: internacion.activa ? 1 : 0,
|
||||
apache: internacion.apache || null,
|
||||
derivacion: internacion.derivacion || null
|
||||
@@ -688,6 +705,412 @@ 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];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== GRUPOS DE DETERMINACIONES DE LABORATORIO ==========
|
||||
export const DEFAULT_GRUPOS_LABORATORIO = [
|
||||
{
|
||||
id: 'grp-lipidos',
|
||||
nombreGrupo: 'Perfil Lipídico',
|
||||
descripcion: 'Determinaciones del metabolismo lipídico y riesgo aterogénico',
|
||||
activo: true,
|
||||
orden: 1,
|
||||
determinaciones: [
|
||||
{ id: 'lip-1', nombre: 'Colesterol Total', claves: ['colesterol total', 'colesterol', 'colest. total', 'col. total', 'colest total'], unidad: 'mg/dL', rangoReferencia: '< 200 mg/dL', esAdicional: true },
|
||||
{ id: 'lip-2', nombre: 'Colesterol LDL', claves: ['colesterol ldl', 'ldl-c', 'ldl', 'colest. ldl', 'colest ldl'], unidad: 'mg/dL', rangoReferencia: '< 100 mg/dL', esAdicional: true },
|
||||
{ id: 'lip-3', nombre: 'Colesterol No HDL', claves: ['colesterol no-hdl', 'colesterol no hdl', 'no-hdl', 'no hdl', 'no_hdl'], unidad: 'mg/dL', rangoReferencia: '< 130 mg/dL', esAdicional: true },
|
||||
{ id: 'lip-4', nombre: 'Colesterol HDL', claves: ['colesterol hdl', 'hdl-c', 'hdl', 'colest. hdl', 'colest hdl'], unidad: 'mg/dL', rangoReferencia: '> 40 mg/dL', esAdicional: true },
|
||||
{ id: 'lip-5', nombre: 'Triglicéridos', claves: ['triglicéridos', 'trigliceridos', 'tg', 'triglicérido', 'triglicerido'], unidad: 'mg/dL', rangoReferencia: '< 150 mg/dL', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-ferrico',
|
||||
nombreGrupo: 'Perfil Férrico',
|
||||
descripcion: 'Metabolismo del hierro, transferrina, ferritina y vitaminas hematopoyéticas',
|
||||
activo: true,
|
||||
orden: 2,
|
||||
determinaciones: [
|
||||
{ id: 'fer-1', nombre: 'Hierro', claves: ['hierro', 'sideremia', 'fe'], unidad: 'µg/dL', rangoReferencia: '60 - 170 µg/dL', esAdicional: true },
|
||||
{ id: 'fer-2', nombre: 'Transferrina', claves: ['transferrina', 'transferrin'], unidad: 'mg/dL', rangoReferencia: '200 - 360 mg/dL', esAdicional: true },
|
||||
{ id: 'fer-3', nombre: 'Porcentaje de Saturación de Transferrina', claves: ['porcentaje de saturacion de transferrina', 'porcentaje de saturación de transferrina', 'saturacion de transferrina', 'saturación de transferrina', 'sat. transferrina', 'sat transferrina', '% sat', '% de sat', '% saturacion', '% saturación'], unidad: '%', rangoReferencia: '20 - 50 %', esAdicional: true },
|
||||
{ id: 'fer-4', nombre: 'Ferritina', claves: ['ferritina', 'ferritin'], unidad: 'ng/mL', rangoReferencia: '30 - 400 ng/mL', esAdicional: true },
|
||||
{ id: 'fer-5', nombre: 'Ácido Fólico', claves: ['acido folico', 'ácido fólico', 'folato', 'folatos', 'folico', 'fólico'], unidad: 'ng/mL', rangoReferencia: '3.0 - 17.0 ng/mL', esAdicional: true },
|
||||
{ id: 'fer-6', nombre: 'Vitamina B12', claves: ['vitamina b12', 'b12', 'vit. b12'], unidad: 'pg/mL', rangoReferencia: '200 - 900 pg/mL', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-fosfocalcico',
|
||||
nombreGrupo: 'Metabolismo Fosfocálcico y Medio Interno Extra',
|
||||
descripcion: 'Calcio total, calcio iónico, fósforo y magnesio sérico',
|
||||
activo: true,
|
||||
orden: 3,
|
||||
determinaciones: [
|
||||
{ id: 'fcal-1', nombre: 'Calcio Total', claves: ['calcio total', 'calcio', 'ca total', 'ca+', 'ca++'], unidad: 'mg/dL', rangoReferencia: '8.5 - 10.5 mg/dL', esAdicional: true },
|
||||
{ id: 'fcal-2', nombre: 'Calcio Iónico', claves: ['calcio ionico', 'calcio iónico', 'ca ionico', 'ca iónico', 'ca++ ionico', 'ca++ iónico', 'calcio_ionico'], unidad: 'mmol/L', rangoReferencia: '1.15 - 1.33 mmol/L', esAdicional: true },
|
||||
{ id: 'fcal-3', nombre: 'Fósforo', claves: ['fósforo', 'fosforo', 'fosfemia'], unidad: 'mg/dL', rangoReferencia: '2.5 - 4.5 mg/dL', esAdicional: true },
|
||||
{ id: 'fcal-4', nombre: 'Magnesio', claves: ['magnesio', 'magnesemia', 'mg++', 'mg+', 'mg2+', 'mg 2+', 'mg.', 'magnesio plasmatico', 'magnesio plasmático', 'magnesio serico', 'magnesio sérico', 'magnesio en sangre', 'mg serico', 'mg sérico', 'mg plasmatico', 'mg plasmático', 'mg'], unidad: 'mg/dL', rangoReferencia: '1.7 - 2.4 mg/dL', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-enzimas-inflamacion',
|
||||
nombreGrupo: 'Enzimas, Proteínas e Inflamación',
|
||||
descripcion: 'Albúmina, FAL, LDH, Procalcitonina, PCR, eritrosedimentación y enzimas',
|
||||
activo: true,
|
||||
orden: 4,
|
||||
determinaciones: [
|
||||
{ id: 'enz-1', nombre: 'Albúmina', claves: ['albúmina', 'albumina'], unidad: 'g/dL', rangoReferencia: '3.5 - 5.0 g/dL', esAdicional: true },
|
||||
{ id: 'enz-2', nombre: 'Fosfatasa Alcalina', claves: ['fosfatasa alcalina', 'fal'], unidad: 'U/L', rangoReferencia: '40 - 130 U/L', esAdicional: true },
|
||||
{ id: 'enz-3', nombre: 'LDH', claves: ['ldh', 'lactato deshidrogenasa', 'lactatodeshidrogenasa'], unidad: 'U/L', rangoReferencia: '135 - 225 U/L', esAdicional: true },
|
||||
{ id: 'enz-4', nombre: 'Procalcitonina', claves: ['procalcitonina', 'pct'], unidad: 'ng/mL', rangoReferencia: '< 0.5 ng/mL', esAdicional: true },
|
||||
{ id: 'enz-5', nombre: 'Proteína C Reactiva', claves: ['proteina c reactiva', 'proteína c reactiva', 'pcr cuantitativa', 'pcr ultrasensible', 'pcr'], unidad: 'mg/L', rangoReferencia: '< 5 mg/L', esAdicional: true },
|
||||
{ id: 'enz-6', nombre: 'Eritrosedimentación', claves: ['eritrosedimentacion', 'eritrosedimentación', 'vsg', 'esr', 'eritro'], unidad: 'mm/h', rangoReferencia: '< 20 mm/h', esAdicional: true },
|
||||
{ id: 'enz-7', nombre: 'CPK', claves: ['cpk', 'creatinfosfoquinasa', 'creatin fosfoquinasa', 'ck total', 'ck'], unidad: 'U/L', rangoReferencia: '20 - 200 U/L', esAdicional: true },
|
||||
{ id: 'enz-8', nombre: 'Amilasa', claves: ['amilasa', 'amilasemia'], unidad: 'U/L', rangoReferencia: '28 - 100 U/L', esAdicional: true },
|
||||
{ id: 'enz-9', nombre: 'Lipasa', claves: ['lipasa', 'lipasemia'], unidad: 'U/L', rangoReferencia: '13 - 60 U/L', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-tiroideo',
|
||||
nombreGrupo: 'Perfil Tiroideo',
|
||||
descripcion: 'Hormonas tiroideas e hipofisarias (TSH, T4L, T4, T3)',
|
||||
activo: true,
|
||||
orden: 5,
|
||||
determinaciones: [
|
||||
{ id: 'tir-1', nombre: 'TSH', claves: ['tsh', 'tirotrofina', 'tirotropina', 'tsh ultrasensible'], unidad: 'uUI/mL', rangoReferencia: '0.4 - 4.0 uUI/mL', esAdicional: true },
|
||||
{ id: 'tir-2', nombre: 'T4 Libre', claves: ['t4 libre', 't4l', 't4-l', 'tiroxina libre'], unidad: 'ng/dL', rangoReferencia: '0.8 - 1.8 ng/dL', esAdicional: true },
|
||||
{ id: 'tir-3', nombre: 'T4 Total', claves: ['t4 total', 't4', 'tiroxina'], unidad: 'µg/dL', rangoReferencia: '4.5 - 12.0 µg/dL', esAdicional: true },
|
||||
{ id: 'tir-4', nombre: 'T3 Total', claves: ['t3 total', 't3', 'triyodotironina'], unidad: 'ng/dL', rangoReferencia: '80 - 200 ng/dL', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-cardiacos',
|
||||
nombreGrupo: 'Biomarcadores Cardíacos',
|
||||
descripcion: 'Péptidos natriuréticos, troponinas y marcadores de isquemia/falla',
|
||||
activo: true,
|
||||
orden: 6,
|
||||
determinaciones: [
|
||||
{ id: 'car-1', nombre: 'NT-proBNP', claves: ['nt-probnp', 'nt probnp', 'nt_probnp', 'probnp', 'pro-bnp', 'pro bnp'], unidad: 'pg/mL', rangoReferencia: '< 125 pg/mL', esAdicional: true },
|
||||
{ id: 'car-2', nombre: 'Troponina T / I', claves: ['troponina t', 'troponina i', 'troponina ultrasensible', 'troponina', 'tn-t', 'tn-i', 'tnt', 'tni'], unidad: 'ng/mL', rangoReferencia: '< 0.014 ng/mL', esAdicional: true },
|
||||
{ id: 'car-3', nombre: 'CK-MB', claves: ['ck-mb', 'ckmb', 'ck mb'], unidad: 'U/L', rangoReferencia: '< 25 U/L', esAdicional: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grp-hemograma-indices',
|
||||
nombreGrupo: 'Hemograma - Índices y Fórmula',
|
||||
descripcion: 'Constantes corpusculares e índices hematimétricos adicionales',
|
||||
activo: true,
|
||||
orden: 7,
|
||||
determinaciones: [
|
||||
{ id: 'hem-1', nombre: 'VCM', claves: ['volumen corpuscular medio', 'vcm'], unidad: 'fL', rangoReferencia: '80 - 100 fL', esAdicional: true },
|
||||
{ id: 'hem-2', nombre: 'HCM', claves: ['hemoglobina corpuscular media', 'hcm'], unidad: 'pg', rangoReferencia: '27 - 33 pg', esAdicional: true },
|
||||
{ id: 'hem-3', nombre: 'CHCM', claves: ['concentracion de hemoglobina corpuscular media', 'chcm'], unidad: 'g/dL', rangoReferencia: '32 - 36 g/dL', esAdicional: true },
|
||||
{ id: 'hem-4', nombre: 'RDW', claves: ['rdw', 'ide', 'ancho de distribucion eritrocitaria'], unidad: '%', rangoReferencia: '11.5 - 14.5 %', esAdicional: true },
|
||||
{ id: 'hem-5', nombre: 'Neutrófilos', claves: ['neutrófilos', 'neutrofilos', 'neutrofilos segmentados', 'segmentados'], unidad: '%', rangoReferencia: '45 - 70 %', esAdicional: true },
|
||||
{ id: 'hem-6', nombre: 'Linfocitos', claves: ['linfocitos', 'linfo'], unidad: '%', rangoReferencia: '20 - 45 %', esAdicional: true },
|
||||
{ id: 'hem-7', nombre: 'Monocitos', claves: ['monocitos', 'mono'], unidad: '%', rangoReferencia: '2 - 10 %', esAdicional: true },
|
||||
{ id: 'hem-8', nombre: 'Eosinófilos', claves: ['eosinófilos', 'eosinofilos', 'eosino'], unidad: '%', rangoReferencia: '1 - 4 %', esAdicional: true },
|
||||
{ id: 'hem-9', nombre: 'Basófilos', claves: ['basófilos', 'basofilos'], unidad: '%', rangoReferencia: '0 - 1 %', esAdicional: true },
|
||||
{ id: 'hem-10', nombre: 'Eritroblastos', claves: ['eritroblastos'], unidad: '%', rangoReferencia: '0 %', esAdicional: true },
|
||||
{ id: 'hem-11', nombre: 'VPM', claves: ['volumen plaquetario medio', 'vpm'], unidad: 'fL', rangoReferencia: '7.5 - 11.5 fL', esAdicional: true }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export async function getAllGruposLaboratorio() {
|
||||
let list = [];
|
||||
if (db) {
|
||||
const raw = await db.collection('grupos_laboratorio').find().sort({ orden: 1, nombreGrupo: 1 }).toArray();
|
||||
list = cleanDocs(raw);
|
||||
if (list.length === 0) {
|
||||
for (const item of DEFAULT_GRUPOS_LABORATORIO) {
|
||||
await db.collection('grupos_laboratorio').insertOne({ ...item });
|
||||
}
|
||||
list = [...DEFAULT_GRUPOS_LABORATORIO];
|
||||
}
|
||||
return list;
|
||||
} else {
|
||||
if (!memStore.grupos_laboratorio || memStore.grupos_laboratorio.length === 0) {
|
||||
memStore.grupos_laboratorio = DEFAULT_GRUPOS_LABORATORIO.map(item => JSON.parse(JSON.stringify(item)));
|
||||
}
|
||||
return [...memStore.grupos_laboratorio];
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGrupoLaboratorio(grupo) {
|
||||
const doc = {
|
||||
id: grupo.id || generateUUID(),
|
||||
nombreGrupo: (grupo.nombreGrupo || '').trim(),
|
||||
descripcion: (grupo.descripcion || '').trim(),
|
||||
activo: grupo.activo !== false,
|
||||
orden: typeof grupo.orden === 'number' ? grupo.orden : 99,
|
||||
determinaciones: Array.isArray(grupo.determinaciones) ? grupo.determinaciones.map(d => ({
|
||||
id: d.id || generateUUID(),
|
||||
nombre: (d.nombre || '').trim(),
|
||||
claves: Array.isArray(d.claves) ? d.claves.map(c => (c || '').trim().toLowerCase()).filter(Boolean) : [],
|
||||
unidad: (d.unidad || '').trim(),
|
||||
esPrincipal: Boolean(d.esPrincipal),
|
||||
esAdicional: d.esAdicional !== false,
|
||||
rangoReferencia: (d.rangoReferencia || '').trim(),
|
||||
descripcion: (d.descripcion || '').trim()
|
||||
})) : [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (!doc.nombreGrupo) {
|
||||
throw new Error('El nombre del grupo de laboratorio es requerido');
|
||||
}
|
||||
|
||||
if (db) {
|
||||
const existing = await db.collection('grupos_laboratorio').findOne({ nombreGrupo: doc.nombreGrupo });
|
||||
if (existing) {
|
||||
throw new Error(`Ya existe un grupo de determinaciones con el nombre "${doc.nombreGrupo}"`);
|
||||
}
|
||||
await db.collection('grupos_laboratorio').insertOne(doc);
|
||||
} else {
|
||||
if (!memStore.grupos_laboratorio) memStore.grupos_laboratorio = [];
|
||||
const exists = memStore.grupos_laboratorio.some(g => g.nombreGrupo.toLowerCase() === doc.nombreGrupo.toLowerCase());
|
||||
if (exists) {
|
||||
throw new Error(`Ya existe un grupo de determinaciones con el nombre "${doc.nombreGrupo}"`);
|
||||
}
|
||||
memStore.grupos_laboratorio.push(doc);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function updateGrupoLaboratorio(id, datos) {
|
||||
const updateDoc = { ...datos, updatedAt: new Date().toISOString() };
|
||||
delete updateDoc._id;
|
||||
delete updateDoc.id;
|
||||
|
||||
if (datos.nombreGrupo) {
|
||||
updateDoc.nombreGrupo = datos.nombreGrupo.trim();
|
||||
}
|
||||
if (datos.descripcion !== undefined) {
|
||||
updateDoc.descripcion = datos.descripcion.trim();
|
||||
}
|
||||
if (datos.activo !== undefined) {
|
||||
updateDoc.activo = Boolean(datos.activo);
|
||||
}
|
||||
if (datos.orden !== undefined) {
|
||||
updateDoc.orden = Number(datos.orden);
|
||||
}
|
||||
if (Array.isArray(datos.determinaciones)) {
|
||||
updateDoc.determinaciones = datos.determinaciones.map(d => ({
|
||||
id: d.id || generateUUID(),
|
||||
nombre: (d.nombre || '').trim(),
|
||||
claves: Array.isArray(d.claves) ? d.claves.map(c => (c || '').trim().toLowerCase()).filter(Boolean) : [],
|
||||
unidad: (d.unidad || '').trim(),
|
||||
esPrincipal: Boolean(d.esPrincipal),
|
||||
esAdicional: d.esAdicional !== false,
|
||||
rangoReferencia: (d.rangoReferencia || '').trim(),
|
||||
descripcion: (d.descripcion || '').trim()
|
||||
}));
|
||||
}
|
||||
|
||||
if (db) {
|
||||
if (updateDoc.nombreGrupo) {
|
||||
const existing = await db.collection('grupos_laboratorio').findOne({
|
||||
id: { $ne: id },
|
||||
nombreGrupo: updateDoc.nombreGrupo
|
||||
});
|
||||
if (existing) {
|
||||
throw new Error(`Ya existe otro grupo de determinaciones con el nombre "${updateDoc.nombreGrupo}"`);
|
||||
}
|
||||
}
|
||||
await db.collection('grupos_laboratorio').updateOne({ id }, { $set: updateDoc });
|
||||
} else {
|
||||
if (!memStore.grupos_laboratorio) memStore.grupos_laboratorio = [];
|
||||
if (updateDoc.nombreGrupo) {
|
||||
const exists = memStore.grupos_laboratorio.some(g => g.id !== id && g.nombreGrupo.toLowerCase() === updateDoc.nombreGrupo.toLowerCase());
|
||||
if (exists) {
|
||||
throw new Error(`Ya existe otro grupo de determinaciones con el nombre "${updateDoc.nombreGrupo}"`);
|
||||
}
|
||||
}
|
||||
const idx = memStore.grupos_laboratorio.findIndex(g => g.id === id);
|
||||
if (idx !== -1) {
|
||||
memStore.grupos_laboratorio[idx] = { ...memStore.grupos_laboratorio[idx], ...updateDoc };
|
||||
}
|
||||
}
|
||||
return { id, ...updateDoc };
|
||||
}
|
||||
|
||||
export async function deleteGrupoLaboratorio(id) {
|
||||
if (db) {
|
||||
await db.collection('grupos_laboratorio').deleteOne({ id });
|
||||
} else {
|
||||
if (memStore.grupos_laboratorio) {
|
||||
memStore.grupos_laboratorio = memStore.grupos_laboratorio.filter(g => g.id !== id);
|
||||
}
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function restablecerGruposLaboratorio() {
|
||||
if (db) {
|
||||
await db.collection('grupos_laboratorio').deleteMany({});
|
||||
for (const item of DEFAULT_GRUPOS_LABORATORIO) {
|
||||
await db.collection('grupos_laboratorio').insertOne({ ...item });
|
||||
}
|
||||
return await getAllGruposLaboratorio();
|
||||
} else {
|
||||
memStore.grupos_laboratorio = DEFAULT_GRUPOS_LABORATORIO.map(item => JSON.parse(JSON.stringify(item)));
|
||||
return [...memStore.grupos_laboratorio];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== ESTUDIOS COMPLEMENTARIOS ==========
|
||||
export async function getAllEstudiosComplementarios() {
|
||||
if (db) {
|
||||
@@ -923,6 +1346,42 @@ export async function createMovimientoIndicacion(mov) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== PENDIENTES ==========
|
||||
export async function getAllPendientes() {
|
||||
if (db) {
|
||||
const pendientes = await db.collection('pendientes').find().toArray();
|
||||
return cleanDocs(pendientes);
|
||||
}
|
||||
return [...memStore.pendientes];
|
||||
}
|
||||
|
||||
export async function createPendiente(pendiente) {
|
||||
if (db) {
|
||||
await db.collection('pendientes').insertOne(pendiente);
|
||||
} else {
|
||||
memStore.pendientes.push(pendiente);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePendiente(id, datos) {
|
||||
if (db) {
|
||||
await db.collection('pendientes').updateOne({ id }, { $set: datos });
|
||||
} else {
|
||||
const idx = memStore.pendientes.findIndex(p => p.id === id);
|
||||
if (idx !== -1) {
|
||||
memStore.pendientes[idx] = { ...memStore.pendientes[idx], ...datos };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePendiente(id) {
|
||||
if (db) {
|
||||
await db.collection('pendientes').deleteOne({ id });
|
||||
} else {
|
||||
memStore.pendientes = memStore.pendientes.filter(p => p.id !== id);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== KV STORE ==========
|
||||
export async function getValue(key) {
|
||||
if (db) {
|
||||
@@ -943,3 +1402,128 @@ export async function setValue(key, value) {
|
||||
memStore.kv[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDb() {
|
||||
return db;
|
||||
}
|
||||
|
||||
export async function exportAllData() {
|
||||
const collections = [
|
||||
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
||||
'evoluciones', 'laboratorios', 'otrosLaboratorios', 'glucemias', 'acidosbase',
|
||||
'cultivos', 'tipos_cultivo', 'grupos_laboratorio', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
||||
];
|
||||
const dump = {};
|
||||
if (db) {
|
||||
for (const name of collections) {
|
||||
const docs = await db.collection(name).find().toArray();
|
||||
dump[name] = docs;
|
||||
}
|
||||
} else {
|
||||
for (const name of collections) {
|
||||
dump[name] = [...(memStore[name] || [])];
|
||||
}
|
||||
}
|
||||
return dump;
|
||||
}
|
||||
|
||||
export async function importAllData(dump) {
|
||||
const collections = [
|
||||
'usuarios', 'pacientes', 'areas', 'camas', 'internaciones',
|
||||
'evoluciones', 'laboratorios', 'otrosLaboratorios', 'glucemias', 'acidosbase',
|
||||
'cultivos', 'tipos_cultivo', 'grupos_laboratorio', 'estudiosComplementarios', 'interconsultas', 'atb',
|
||||
'indicaciones', 'movimientos_indicaciones', 'pendientes', 'kv'
|
||||
];
|
||||
if (db) {
|
||||
for (const name of collections) {
|
||||
if (Array.isArray(dump[name])) {
|
||||
try {
|
||||
await db.collection(name).deleteMany({});
|
||||
} catch (e) {
|
||||
console.warn(`Could not clear collection ${name}:`, e);
|
||||
}
|
||||
if (dump[name].length > 0) {
|
||||
const cleaned = dump[name].map(doc => {
|
||||
const copy = { ...doc };
|
||||
if (copy._id) {
|
||||
if (ObjectId.isValid(copy._id)) {
|
||||
copy._id = new ObjectId(copy._id);
|
||||
} else {
|
||||
delete copy._id;
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
await db.collection(name).insertMany(cleaned);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const name of collections) {
|
||||
if (Array.isArray(dump[name])) {
|
||||
memStore[name] = [...dump[name]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function getAllOtrosLaboratorios() {
|
||||
if (db) {
|
||||
const records = await db.collection('otros-laboratorios').find().toArray();
|
||||
return cleanDocs(records);
|
||||
}
|
||||
return [...memStore.otrosLaboratorios];
|
||||
}
|
||||
|
||||
export async function createOtroLaboratorio(record) {
|
||||
const doc = {
|
||||
id: record.id,
|
||||
pacienteId: record.pacienteId,
|
||||
internacionId: record.internacionId,
|
||||
fecha: record.fecha,
|
||||
hora: record.hora,
|
||||
observaciones: record.observaciones,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (db) {
|
||||
await db.collection('otros-laboratorios').insertOne(doc);
|
||||
return cleanDoc(doc);
|
||||
}
|
||||
|
||||
memStore.otrosLaboratorios.push(doc);
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function updateOtroLaboratorio(id, updates) {
|
||||
if (db) {
|
||||
await db.collection('otros-laboratorios').updateOne(
|
||||
{ id },
|
||||
{ $set: { ...updates, updatedAt: new Date().toISOString() } }
|
||||
);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const idx = memStore.otrosLaboratorios.findIndex(x => x.id === id);
|
||||
if (idx !== -1) {
|
||||
memStore.otrosLaboratorios[idx] = { ...memStore.otrosLaboratorios[idx], ...updates, updatedAt: new Date().toISOString() };
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
export async function deleteOtroLaboratorio(id) {
|
||||
if (db) {
|
||||
await db.collection('otros-laboratorios').deleteOne({ id });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const idx = memStore.otrosLaboratorios.findIndex(x => x.id === id);
|
||||
if (idx !== -1) {
|
||||
memStore.otrosLaboratorios.splice(idx, 1);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
+72
-8
@@ -13,6 +13,8 @@ import { NuevoIngreso } from '@/sections/NuevoIngreso';
|
||||
import { EditIngreso } from '@/sections/EditIngreso';
|
||||
import { Login } from '@/sections/Login';
|
||||
import { GestionUsuarios } from '@/sections/GestionUsuarios';
|
||||
import { PendientesSala } from '@/sections/PendientesSala';
|
||||
import { SeccionSistema } from '@/sections/SeccionSistema';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
@@ -91,6 +93,22 @@ function AppContent() {
|
||||
getCamaById={store.getCamaById}
|
||||
onVerHC={(id) => { store.setCurrentInternacion(id); store.setVista('historiaclinica'); }}
|
||||
onNuevoIngreso={() => store.setVista('nuevoingreso')}
|
||||
onVerPendientesDeSala={() => store.setVista('pendientessala')}
|
||||
/>
|
||||
);
|
||||
case 'pendientessala':
|
||||
return (
|
||||
<PendientesSala
|
||||
pendientes={store.pendientes}
|
||||
internaciones={store.internaciones}
|
||||
pacientes={store.pacientes}
|
||||
camas={store.camas}
|
||||
onActualizarPendiente={store.actualizarPendiente}
|
||||
onEliminarPendiente={store.eliminarPendiente}
|
||||
onVerHC={(id) => { store.setCurrentInternacion(id); store.setVista('historiaclinica'); }}
|
||||
onVolver={() => store.setVista('internaciones')}
|
||||
getPacienteById={store.getPacienteById}
|
||||
getCamaById={store.getCamaById}
|
||||
/>
|
||||
);
|
||||
case 'evoluciones':
|
||||
@@ -131,12 +149,30 @@ function AppContent() {
|
||||
/>
|
||||
);
|
||||
case 'historiaclinica': {
|
||||
const internacionId = store.currentInternacionId || '';
|
||||
const internacionId = store.currentInternacionId || sessionStorage.getItem('hospital_current_internacion_id') || '';
|
||||
const internacion = store.getInternacionById(internacionId);
|
||||
const paciente = internacion ? store.getPacienteById(internacion.pacienteId) : undefined;
|
||||
if (!internacion || !paciente) {
|
||||
const paciente = internacion
|
||||
? (store.getPacienteById(internacion.pacienteId) || store.pacientes.find(p => p.id === internacion.pacienteId || p.dni === internacion.pacienteId) || {
|
||||
id: internacion.pacienteId || 'paciente-unknown',
|
||||
apellido: 'Paciente',
|
||||
nombre: 'Sin registrar',
|
||||
dni: internacion.pacienteId || 'N/A',
|
||||
fechaNacimiento: '1990-01-01',
|
||||
sexo: 'Otro' as const,
|
||||
fechaRegistro: new Date().toISOString()
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (!internacion) {
|
||||
if (!store.isLoaded) {
|
||||
return (
|
||||
<div className="p-4 text-center">
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<p>Cargando información de la internación...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="p-4 text-center space-y-4">
|
||||
<p className="text-gray-500 dark:text-gray-400">Internación no encontrada (id: {internacionId})</p>
|
||||
<Button onClick={() => store.setVista('internaciones')}>Volver a Internaciones</Button>
|
||||
</div>
|
||||
@@ -215,12 +251,30 @@ function AppContent() {
|
||||
);
|
||||
|
||||
case 'editaringreso': {
|
||||
const internacionId = store.currentInternacionId || '';
|
||||
const internacionId = store.currentInternacionId || sessionStorage.getItem('hospital_current_internacion_id') || '';
|
||||
const internacion = store.getInternacionById(internacionId);
|
||||
const paciente = internacion ? store.getPacienteById(internacion.pacienteId) : undefined;
|
||||
if (!internacion || !paciente) {
|
||||
const paciente = internacion
|
||||
? (store.getPacienteById(internacion.pacienteId) || store.pacientes.find(p => p.id === internacion.pacienteId || p.dni === internacion.pacienteId) || {
|
||||
id: internacion.pacienteId || 'paciente-unknown',
|
||||
apellido: 'Paciente',
|
||||
nombre: 'Sin registrar',
|
||||
dni: internacion.pacienteId || 'N/A',
|
||||
fechaNacimiento: '1990-01-01',
|
||||
sexo: 'Otro' as const,
|
||||
fechaRegistro: new Date().toISOString()
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (!internacion) {
|
||||
if (!store.isLoaded) {
|
||||
return (
|
||||
<div className="p-4 text-center">
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<p>Cargando información de la internación...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="p-4 text-center space-y-4">
|
||||
<p className="text-gray-500 dark:text-gray-400">Internación no encontrada (id: {internacionId})</p>
|
||||
<Button onClick={() => store.setVista('internaciones')}>Volver a Internaciones</Button>
|
||||
</div>
|
||||
@@ -253,6 +307,16 @@ function AppContent() {
|
||||
return (
|
||||
<GestionUsuarios />
|
||||
);
|
||||
case 'sistema':
|
||||
if (store.currentUser?.rol !== 'admin') {
|
||||
return (
|
||||
<div className="p-8 text-center text-red-500 font-semibold">
|
||||
<p>No tiene permisos para acceder a esta sección.</p>
|
||||
<Button className="mt-4" onClick={() => store.setVista('dashboard')}>Volver al Dashboard</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <SeccionSistema />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
export function EsculapioIcon({ className = "w-16 h-16 text-neutral-900 dark:text-neutral-100", size }: { className?: string; size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
width={size || undefined}
|
||||
height={size || undefined}
|
||||
className={className}
|
||||
fill="currentColor"
|
||||
aria-label="Vara de Esculapio"
|
||||
>
|
||||
<g transform="translate(0, -2)">
|
||||
{/* Pomo esférico superior */}
|
||||
<circle cx="50.5" cy="14" r="6.8" />
|
||||
|
||||
{/* Cuello del pomo */}
|
||||
<path d="M47.8 19.8 h5.4 v2.8 h-5.4 z" />
|
||||
|
||||
{/* Bastón recto vertical terminado en punta cónica */}
|
||||
<path d="M49 22.6 h3 l-.3 65.4 -1.2 3.2 -1.2-3.2 z" />
|
||||
|
||||
{/* Serpiente: Cabeza y primer bucle superior */}
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M37.5 24.8 c1.2-1.6 3.6-2.6 6.8-2.3 2.6.2 5.4 1.3 8.2 2.5 3.1 1.3 6.2 2.5 9.2 1.8 3.8-.9 6.2-4.2 5.2-7.8-.3-1.1-.9-2-1.8-2.8 l3.8-2.4 c1.5 1.4 2.5 3.1 3 5.2 1.4 5.8-1.8 11.2-7.8 12.8-4.5 1.2-8.8-.6-12.2-2-2.7-1.1-4.8-1.8-6.6-1.8-2.2 0-3.9 1-5 2.5 z
|
||||
M43.8 24.2 a1 1 0 1 0 0-2 1 1 0 0 0 0 2 z"
|
||||
/>
|
||||
|
||||
{/* Curva 1: Cruce superior de derecha a izquierda */}
|
||||
<path d="M64 30.5 c-3.2 5-8 8.8-13.8 10.3-5.8 1.5-11.8.2-16.5-3.4 l-3.5 4 c6.1 4.7 13.8 6.3 21.4 4.3 7.5-2 13.6-7 17.6-13.6 z" />
|
||||
|
||||
{/* Curva 2: Cruce medio de izquierda a derecha */}
|
||||
<path d="M32.5 41.2 c-3.2 5.5-3 12.2-.2 17.8 2.8 5.8 8.2 9.8 14.5 10.6 5.2.7 10.5-1.1 14.5-4.6 l-3.2-4.1 c-3.2 2.8-7.5 4.2-11.8 3.6-5-.7-9.2-3.8-11.5-8.4-2.2-4.4-2.3-9.7.2-14.1 z" />
|
||||
|
||||
{/* Curva 3: Cruce inferior de derecha a izquierda */}
|
||||
<path d="M63 64.8 c-2.8 4.8-7.2 8.4-12.8 9.8-5.4 1.4-11.2.2-15.8-3 l-3.2 4.2 c6 4.2 13.5 5.7 20.6 3.8 7.2-1.8 13-6.6 16.8-13 z" />
|
||||
|
||||
{/* Curva 4: Cola que se enrosca en la punta del bastón */}
|
||||
<path d="M33 74.8 c-2.2 3.8-2 8.5-.2 12.2 1.5 3.2 4.2 5.5 7.6 6.2 3.5.7 7.2-.2 10-2.2 l-2.8-4.2 c-2 1.4-4.6 2-7.1 1.5-2.4-.5-4.4-2.1-5.4-4.4-1.2-2.6-1.4-5.8 0-8.4 z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: {
|
||||
|
||||
@@ -22,7 +22,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return () => window.removeEventListener("resize", checkMobile)
|
||||
}, [])
|
||||
|
||||
const effectivePosition = isMobile ? "bottom-center" : (props.position || "top-right")
|
||||
const effectivePosition = isMobile ? "top-center" : (props.position || "top-right")
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
|
||||
+371
-22
@@ -1,21 +1,24 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { getNombreProfesional, isCamaFueraDeGrupo, computeSector } from '@/lib/utils';
|
||||
import { getNombreProfesional, isCamaFueraDeGrupo, computeSector, sortCamas, getLocalToday } from '@/lib/utils';
|
||||
import type {
|
||||
Paciente,
|
||||
Cama,
|
||||
Grupo,
|
||||
Internacion,
|
||||
Evolucion,
|
||||
Laboratorio,
|
||||
Laboratorio, OtroLaboratorio,
|
||||
Glucemia,
|
||||
AcidoBase,
|
||||
Cultivo,
|
||||
TipoCultivo,
|
||||
GrupoDeterminacionLaboratorio,
|
||||
EstudioComplementario,
|
||||
Interconsulta,
|
||||
ATB,
|
||||
Indicacion,
|
||||
MovimientoIndicacion,
|
||||
Pendiente,
|
||||
Vista,
|
||||
Usuario
|
||||
} from '@/types';
|
||||
@@ -40,14 +43,18 @@ interface HospitalState {
|
||||
internaciones: Internacion[];
|
||||
evoluciones: Evolucion[];
|
||||
laboratorios: Laboratorio[];
|
||||
otrosLaboratorios: OtroLaboratorio[];
|
||||
glucemias: Glucemia[];
|
||||
acidosBase: AcidoBase[];
|
||||
cultivos: Cultivo[];
|
||||
tiposCultivo: TipoCultivo[];
|
||||
gruposDeterminacionesLab: GrupoDeterminacionLaboratorio[];
|
||||
estudiosComplementarios: EstudioComplementario[];
|
||||
interconsultas: Interconsulta[];
|
||||
atb: ATB[];
|
||||
indicaciones: Indicacion[];
|
||||
movimientosIndicaciones: MovimientoIndicacion[];
|
||||
pendientes: Pendiente[];
|
||||
vistaActual: Vista;
|
||||
currentInternacionId?: string | null;
|
||||
usuarios: Usuario[];
|
||||
@@ -64,15 +71,19 @@ const defaultState = (): HospitalState => ({
|
||||
internaciones: [],
|
||||
evoluciones: [],
|
||||
laboratorios: [],
|
||||
otrosLaboratorios: [],
|
||||
glucemias: [],
|
||||
acidosBase: [],
|
||||
cultivos: [],
|
||||
tiposCultivo: [],
|
||||
gruposDeterminacionesLab: [],
|
||||
estudiosComplementarios: [],
|
||||
interconsultas: [],
|
||||
atb: [],
|
||||
vistaActual: 'dashboard',
|
||||
indicaciones: [],
|
||||
movimientosIndicaciones: [],
|
||||
pendientes: [],
|
||||
camas: [],
|
||||
usuarios: [],
|
||||
currentUser: null,
|
||||
@@ -94,15 +105,21 @@ export function useHospitalStore() {
|
||||
if (mounted && body) {
|
||||
const defaults = defaultState();
|
||||
const storedUser = sessionStorage.getItem('hospital_user');
|
||||
const storedInternacionId = sessionStorage.getItem('hospital_current_internacion_id');
|
||||
const currentUser = storedUser ? JSON.parse(storedUser) : null;
|
||||
const normalized = {
|
||||
...defaults,
|
||||
...body,
|
||||
tiposCultivo: body.tiposCultivo || [],
|
||||
gruposDeterminacionesLab: body.gruposDeterminacionesLab || [],
|
||||
estudiosComplementarios: body.estudiosComplementarios || [],
|
||||
interconsultas: body.interconsultas || [],
|
||||
atb: body.atb || [],
|
||||
glucemias: body.glucemias || [],
|
||||
movimientosIndicaciones: body.movimientosIndicaciones || [],
|
||||
pendientes: body.pendientes || [],
|
||||
currentInternacionId: storedInternacionId || body.currentInternacionId || null,
|
||||
camas: sortCamas(body.camas || []),
|
||||
currentUser,
|
||||
isAuthenticated: !!currentUser,
|
||||
};
|
||||
@@ -248,7 +265,7 @@ export function useHospitalStore() {
|
||||
const nuevoPaciente: Paciente = {
|
||||
...paciente,
|
||||
id: generateUUID(),
|
||||
fechaRegistro: new Date().toISOString().split('T')[0],
|
||||
fechaRegistro: getLocalToday(),
|
||||
};
|
||||
try {
|
||||
await apiCall('POST', '/pacientes', nuevoPaciente);
|
||||
@@ -298,11 +315,15 @@ export function useHospitalStore() {
|
||||
const cama = state.camas.find(c => c.id === id);
|
||||
if (!cama) return;
|
||||
const user = state.currentUser;
|
||||
const grupoId = cama.grupoId || null;
|
||||
if (user && user.rol !== 'admin' && grupoId && user.grupoId !== grupoId) {
|
||||
console.warn('No tiene permisos para actualizar cama en esta área');
|
||||
if (user && user.rol !== 'admin') {
|
||||
// Must have a working group and the bed must belong to it
|
||||
if (!user.grupoId || cama.grupoId !== user.grupoId) {
|
||||
toast.error('No tiene permisos para modificar camas fuera de su grupo de trabajo');
|
||||
return;
|
||||
}
|
||||
// A regular user can ONLY modify the "Tipo" of the bed.
|
||||
datos = { tipo: datos.tipo };
|
||||
}
|
||||
try {
|
||||
if (datos.numero !== undefined) {
|
||||
datos.sector = computeSector(datos.numero);
|
||||
@@ -310,7 +331,7 @@ export function useHospitalStore() {
|
||||
await apiCall('PUT', `/camas/${id}`, datos);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
camas: prev.camas.map(c => c.id === id ? { ...c, ...datos } : c),
|
||||
camas: sortCamas(prev.camas.map(c => c.id === id ? { ...c, ...datos } : c)),
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Error al actualizar cama:', err);
|
||||
@@ -319,6 +340,10 @@ export function useHospitalStore() {
|
||||
}, [state, apiCall]);
|
||||
|
||||
const agregarCama = useCallback(async (cama: Omit<Cama, 'id'>) => {
|
||||
if (state.currentUser?.rol !== 'admin') {
|
||||
toast.error('Solo el usuario administrador puede agregar camas');
|
||||
return;
|
||||
}
|
||||
const nuevaCama: Cama = {
|
||||
...cama,
|
||||
id: generateUUID(),
|
||||
@@ -328,28 +353,38 @@ const agregarCama = useCallback(async (cama: Omit<Cama, 'id'>) => {
|
||||
await apiCall('POST', '/camas', nuevaCama);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
camas: [...prev.camas, nuevaCama],
|
||||
camas: sortCamas([...prev.camas, nuevaCama]),
|
||||
}));
|
||||
return nuevaCama.id;
|
||||
} catch (err) {
|
||||
console.error('Error al agregar cama:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
}, [state.currentUser, apiCall]);
|
||||
|
||||
const eliminarCama = useCallback(async (id: string) => {
|
||||
if (state.currentUser?.rol !== 'admin') {
|
||||
toast.error('Solo el usuario administrador puede eliminar camas');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Si hay una internación activa asociada a esta cama, remover la asociación en el servidor
|
||||
const activeInternacion = state.internaciones.find(i => i.camaId === id && i.activa);
|
||||
if (activeInternacion) {
|
||||
await apiCall('PUT', `/internaciones/${activeInternacion.id}`, { camaId: '' });
|
||||
}
|
||||
|
||||
await apiCall('DELETE', `/camas/${id}`);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
camas: prev.camas.filter(c => c.id !== id),
|
||||
internaciones: prev.internaciones.map(i => i.camaId === id ? { ...i, activa: false } : i),
|
||||
internaciones: prev.internaciones.map(i => i.camaId === id ? { ...i, camaId: undefined } : i),
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Error al eliminar cama:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
}, [state.currentUser, state.internaciones, apiCall]);
|
||||
|
||||
// Acciones de internaciones
|
||||
const iniciarInternacion = useCallback(async (internacion: Omit<Internacion, 'id' | 'activa'>) => {
|
||||
@@ -395,7 +430,8 @@ const agregarCama = useCallback(async (cama: Omit<Cama, 'id'>) => {
|
||||
const finalizarInternacion = useCallback(async (internacionId: string, datos: {
|
||||
fechaEgreso: string;
|
||||
diagnosticoEgreso: string;
|
||||
motivoEgreso: Internacion['motivoEgreso']
|
||||
motivoEgreso: Internacion['motivoEgreso'];
|
||||
servicioAlQuePasa?: string;
|
||||
}) => {
|
||||
const internacion = state.internaciones.find(i => i.id === internacionId);
|
||||
if (!internacion) return;
|
||||
@@ -405,13 +441,18 @@ const agregarCama = useCallback(async (cama: Omit<Cama, 'id'>) => {
|
||||
const esFueraDeGrupo = !internacion.grupoId || !grupoFueraDeGrupoId || internacion.grupoId === grupoFueraDeGrupoId;
|
||||
const camaId = internacion.camaId;
|
||||
|
||||
const camaObj = camaId ? state.camas.find(c => c.id === camaId) : undefined;
|
||||
const debeEliminarCama = camaObj
|
||||
? (computeSector(camaObj.numero) === 'Fuera de Área')
|
||||
: esFueraDeGrupo;
|
||||
|
||||
try {
|
||||
// API call inmediata para finalizar internación
|
||||
await apiCall('PUT', `/internaciones/${internacionId}`, { ...datos, activa: false });
|
||||
|
||||
// Eliminar o actualizar la cama según el área
|
||||
if (camaId) {
|
||||
if (esFueraDeGrupo) {
|
||||
if (debeEliminarCama) {
|
||||
// Eliminar la cama si es "Fuera de área"
|
||||
await apiCall('DELETE', `/camas/${camaId}`);
|
||||
} else {
|
||||
@@ -433,7 +474,7 @@ const agregarCama = useCallback(async (cama: Omit<Cama, 'id'>) => {
|
||||
? { ...i, ...datos, activa: false }
|
||||
: i
|
||||
),
|
||||
camas: esFueraDeGrupo
|
||||
camas: debeEliminarCama
|
||||
? prev.camas.filter(c => c.id !== camaId)
|
||||
: prev.camas.map(c =>
|
||||
c.id === camaId
|
||||
@@ -463,10 +504,15 @@ const agregarCama = useCallback(async (cama: Omit<Cama, 'id'>) => {
|
||||
const esFueraDeGrupo = !internacion.grupoId || !grupoFueraDeGrupoId || internacion.grupoId === grupoFueraDeGrupoId;
|
||||
const camaId = internacion.camaId;
|
||||
|
||||
const camaObj = camaId ? state.camas.find(c => c.id === camaId) : undefined;
|
||||
const debeEliminarCama = camaObj
|
||||
? (computeSector(camaObj.numero) === 'Fuera de Área')
|
||||
: esFueraDeGrupo;
|
||||
|
||||
try {
|
||||
await apiCall('DELETE', `/internaciones/${internacionId}`);
|
||||
if (camaId) {
|
||||
if (esFueraDeGrupo) {
|
||||
if (debeEliminarCama) {
|
||||
await apiCall('DELETE', `/camas/${camaId}`);
|
||||
} else {
|
||||
await apiCall('PUT', `/camas/${camaId}`, {
|
||||
@@ -480,7 +526,7 @@ const agregarCama = useCallback(async (cama: Omit<Cama, 'id'>) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
internaciones: prev.internaciones.filter(i => i.id !== internacionId),
|
||||
camas: esFueraDeGrupo
|
||||
camas: debeEliminarCama
|
||||
? prev.camas.filter(c => c.id !== camaId)
|
||||
: prev.camas.map(c =>
|
||||
c.id === camaId
|
||||
@@ -708,6 +754,51 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
}
|
||||
}, [state, checkGrupoPermission, apiCall]);
|
||||
|
||||
// Acciones de otros laboratorios
|
||||
const agregarOtroLaboratorio = useCallback(async (otroLaboratorio: Omit<OtroLaboratorio, 'id'>) => {
|
||||
const nuevo: OtroLaboratorio = {
|
||||
...otroLaboratorio,
|
||||
id: generateUUID(),
|
||||
};
|
||||
try {
|
||||
await apiCall('POST', '/otros-laboratorios', nuevo);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
otrosLaboratorios: [...prev.otrosLaboratorios, nuevo],
|
||||
}));
|
||||
return nuevo.id;
|
||||
} catch (err) {
|
||||
console.error('Error al agregar otro laboratorio:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const eliminarOtroLaboratorio = useCallback(async (id: string) => {
|
||||
try {
|
||||
await apiCall('DELETE', `/otros-laboratorios/${id}`);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
otrosLaboratorios: prev.otrosLaboratorios.filter(o => o.id !== id),
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Error al eliminar otro laboratorio:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const actualizarOtroLaboratorio = useCallback(async (id: string, datos: Partial<OtroLaboratorio>) => {
|
||||
try {
|
||||
await apiCall('PUT', `/otros-laboratorios/${id}`, datos);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
otrosLaboratorios: prev.otrosLaboratorios.map(o => o.id === id ? { ...o, ...datos } : o),
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Error al actualizar otro laboratorio:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
// Acciones de glucemias
|
||||
const agregarGlucemia = useCallback(async (glucemia: Omit<Glucemia, 'id'>) => {
|
||||
const internacion = state.internaciones.find(i => i.id === glucemia.internacionId);
|
||||
@@ -909,6 +1000,144 @@ 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 grupos de determinaciones de laboratorio (Entrenamiento del Parser)
|
||||
const agregarGrupoDeterminacionLab = useCallback(async (grupo: Omit<GrupoDeterminacionLaboratorio, 'id'>) => {
|
||||
const nuevoGrupo: GrupoDeterminacionLaboratorio = {
|
||||
...grupo,
|
||||
id: generateUUID(),
|
||||
};
|
||||
try {
|
||||
const res = await apiCall('POST', '/grupos-laboratorio', nuevoGrupo);
|
||||
const saved = res || nuevoGrupo;
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
gruposDeterminacionesLab: [...(prev.gruposDeterminacionesLab || []).filter(g => g.id !== saved.id), saved],
|
||||
}));
|
||||
toast.success(`Grupo de laboratorio "${saved.nombreGrupo}" guardado exitosamente`);
|
||||
return saved;
|
||||
} catch (err) {
|
||||
console.error('Error al agregar grupo de determinaciones:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Error al agregar grupo de determinaciones');
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const actualizarGrupoDeterminacionLab = useCallback(async (id: string, datos: Partial<GrupoDeterminacionLaboratorio>) => {
|
||||
try {
|
||||
const res = await apiCall('PUT', `/grupos-laboratorio/${id}`, datos);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
gruposDeterminacionesLab: (prev.gruposDeterminacionesLab || []).map(g => g.id === id ? { ...g, ...datos, ...(res || {}) } : g),
|
||||
}));
|
||||
toast.success('Grupo de determinaciones actualizado exitosamente');
|
||||
} catch (err) {
|
||||
console.error('Error al actualizar grupo de determinaciones:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Error al actualizar grupo de determinaciones');
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const eliminarGrupoDeterminacionLab = useCallback(async (id: string) => {
|
||||
try {
|
||||
await apiCall('DELETE', `/grupos-laboratorio/${id}`);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
gruposDeterminacionesLab: (prev.gruposDeterminacionesLab || []).filter(g => g.id !== id),
|
||||
}));
|
||||
toast.success('Grupo de determinaciones eliminado');
|
||||
} catch (err) {
|
||||
console.error('Error al eliminar grupo de determinaciones:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Error al eliminar grupo de determinaciones');
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const restablecerGruposDeterminacionesLab = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiCall('POST', '/grupos-laboratorio/reset');
|
||||
if (Array.isArray(res)) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
gruposDeterminacionesLab: res,
|
||||
}));
|
||||
}
|
||||
toast.success('Grupos de determinaciones de laboratorio restablecidos a valores estándar');
|
||||
} catch (err) {
|
||||
console.error('Error al restablecer grupos de determinaciones:', err);
|
||||
toast.error('Error al restablecer grupos de determinaciones');
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
// Acciones de estudios complementarios
|
||||
const agregarEstudioComplementario = useCallback(async (estudio: Omit<EstudioComplementario, 'id'>) => {
|
||||
const internacion = state.internaciones.find(i => i.id === estudio.internacionId);
|
||||
@@ -1194,6 +1423,10 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
|
||||
// Acciones de grupos
|
||||
const agregarGrupo = useCallback(async (grupo: Omit<Grupo, 'id'>) => {
|
||||
if (state.currentUser?.rol !== 'admin') {
|
||||
toast.error('Solo el usuario administrador puede agregar grupos de trabajo');
|
||||
return;
|
||||
}
|
||||
const nuevaGrupo: Grupo = { ...grupo, id: generateUUID() };
|
||||
try {
|
||||
await apiCall('POST', '/grupos', nuevaGrupo);
|
||||
@@ -1206,9 +1439,13 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
console.error('Error al agregar área:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
}, [state.currentUser, apiCall]);
|
||||
|
||||
const actualizarGrupo = useCallback(async (id: string, datos: Partial<Grupo>) => {
|
||||
if (state.currentUser?.rol !== 'admin') {
|
||||
toast.error('Solo el usuario administrador puede modificar grupos de trabajo');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiCall('PUT', `/grupos/${id}`, datos);
|
||||
setState(prev => ({
|
||||
@@ -1219,9 +1456,13 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
console.error('Error al actualizar área:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
}, [state.currentUser, apiCall]);
|
||||
|
||||
const eliminarGrupo = useCallback(async (id: string) => {
|
||||
if (state.currentUser?.rol !== 'admin') {
|
||||
toast.error('Solo el usuario administrador puede eliminar grupos de trabajo');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiCall('DELETE', `/grupos/${id}`);
|
||||
setState(prev => ({
|
||||
@@ -1233,19 +1474,28 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
console.error('Error al eliminar área:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
}, [state.currentUser, apiCall]);
|
||||
|
||||
// Funciones de utilidad para consultas
|
||||
const getPacienteById = useCallback((id: string) => {
|
||||
return state.pacientes.find(p => p.id === id);
|
||||
if (!id) return undefined;
|
||||
const target = String(id).trim().toLowerCase();
|
||||
return state.pacientes.find(p =>
|
||||
(p.id && String(p.id).trim().toLowerCase() === target) ||
|
||||
(p.dni && String(p.dni).trim().toLowerCase() === target)
|
||||
);
|
||||
}, [state.pacientes]);
|
||||
|
||||
const getCamaById = useCallback((id: string) => {
|
||||
return state.camas.find(c => c.id === id);
|
||||
if (!id) return undefined;
|
||||
const target = String(id).trim().toLowerCase();
|
||||
return state.camas.find(c => c.id && String(c.id).trim().toLowerCase() === target);
|
||||
}, [state.camas]);
|
||||
|
||||
const getInternacionById = useCallback((id: string) => {
|
||||
return state.internaciones.find(i => i.id === id);
|
||||
if (!id) return undefined;
|
||||
const target = String(id).trim().toLowerCase();
|
||||
return state.internaciones.find(i => i.id && String(i.id).trim().toLowerCase() === target);
|
||||
}, [state.internaciones]);
|
||||
|
||||
const getInternacionActivaByPaciente = useCallback((pacienteId: string) => {
|
||||
@@ -1282,6 +1532,60 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
.sort((a, b) => new Date(b.fechaToma).getTime() - new Date(a.fechaToma).getTime());
|
||||
}, [state.cultivos]);
|
||||
|
||||
const agregarPendiente = useCallback(async (datos: Omit<Pendiente, 'id'>) => {
|
||||
const nuevoPendiente: Pendiente = {
|
||||
...datos,
|
||||
id: generateUUID(),
|
||||
fechaCreacion: datos.fechaCreacion || getLocalToday(),
|
||||
horaCreacion: datos.horaCreacion || new Date().toTimeString().slice(0, 5),
|
||||
estado: datos.estado || 'pendiente',
|
||||
prioridad: datos.prioridad || 'media',
|
||||
};
|
||||
try {
|
||||
await apiCall('POST', '/pendientes', nuevoPendiente);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
pendientes: [...(prev.pendientes || []), nuevoPendiente],
|
||||
}));
|
||||
return nuevoPendiente.id;
|
||||
} catch (err) {
|
||||
console.error('Error al agregar pendiente:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const actualizarPendiente = useCallback(async (id: string, datos: Partial<Pendiente>) => {
|
||||
try {
|
||||
await apiCall('PUT', `/pendientes/${id}`, datos);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
pendientes: (prev.pendientes || []).map(p => p.id === id ? { ...p, ...datos } : p),
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Error al actualizar pendiente:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const eliminarPendiente = useCallback(async (id: string) => {
|
||||
try {
|
||||
await apiCall('DELETE', `/pendientes/${id}`);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
pendientes: (prev.pendientes || []).filter(p => p.id !== id),
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Error al eliminar pendiente:', err);
|
||||
throw err;
|
||||
}
|
||||
}, [apiCall]);
|
||||
|
||||
const getPendientesByPaciente = useCallback((pacienteId: string) => {
|
||||
return (state.pendientes || [])
|
||||
.filter(p => p.pacienteId === pacienteId)
|
||||
.sort((a, b) => new Date(`${b.fechaCreacion}T${b.horaCreacion || '00:00'}`).getTime() - new Date(`${a.fechaCreacion}T${a.horaCreacion || '00:00'}`).getTime());
|
||||
}, [state.pendientes]);
|
||||
|
||||
const getEstadisticas = useCallback(() => {
|
||||
const camasEnArea = state.camas.filter(c => !isCamaFueraDeGrupo(c));
|
||||
const camasFueraDeArea = state.camas.filter(c => isCamaFueraDeGrupo(c));
|
||||
@@ -1309,6 +1613,11 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
|
||||
// Current internacion selection (for HC page)
|
||||
const setCurrentInternacion = useCallback((id?: string | null) => {
|
||||
if (id) {
|
||||
sessionStorage.setItem('hospital_current_internacion_id', id);
|
||||
} else {
|
||||
sessionStorage.removeItem('hospital_current_internacion_id');
|
||||
}
|
||||
setState(prev => ({ ...prev, currentInternacionId: id ?? null }));
|
||||
}, []);
|
||||
|
||||
@@ -1363,9 +1672,32 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const refreshState = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/state`);
|
||||
if (!res.ok) throw new Error('no state');
|
||||
const body = await res.json();
|
||||
if (body) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
...body,
|
||||
estudiosComplementarios: body.estudiosComplementarios || prev.estudiosComplementarios,
|
||||
interconsultas: body.interconsultas || prev.interconsultas,
|
||||
atb: body.atb || prev.atb,
|
||||
glucemias: body.glucemias || prev.glucemias,
|
||||
movimientosIndicaciones: body.movimientosIndicaciones || prev.movimientosIndicaciones,
|
||||
pendientes: body.pendientes || prev.pendientes,
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error refreshing state:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
isLoaded,
|
||||
refreshState,
|
||||
setVista,
|
||||
agregarPaciente,
|
||||
actualizarPaciente,
|
||||
@@ -1383,6 +1715,9 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
agregarLaboratorio,
|
||||
actualizarLaboratorio,
|
||||
eliminarLaboratorio,
|
||||
agregarOtroLaboratorio,
|
||||
actualizarOtroLaboratorio,
|
||||
eliminarOtroLaboratorio,
|
||||
agregarGlucemia,
|
||||
actualizarGlucemia,
|
||||
eliminarGlucemia,
|
||||
@@ -1392,6 +1727,15 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
agregarCultivo,
|
||||
actualizarCultivo,
|
||||
eliminarCultivo,
|
||||
agregarTipoCultivo,
|
||||
actualizarTipoCultivo,
|
||||
eliminarTipoCultivo,
|
||||
restablecerTiposCultivo,
|
||||
gruposDeterminacionesLab: state.gruposDeterminacionesLab,
|
||||
agregarGrupoDeterminacionLab,
|
||||
actualizarGrupoDeterminacionLab,
|
||||
eliminarGrupoDeterminacionLab,
|
||||
restablecerGruposDeterminacionesLab,
|
||||
agregarEstudioComplementario,
|
||||
actualizarEstudioComplementario,
|
||||
eliminarEstudioComplementario,
|
||||
@@ -1418,6 +1762,10 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
getGlucemiasByInternacion,
|
||||
getAcidosBaseByInternacion,
|
||||
getCultivosByInternacion,
|
||||
agregarPendiente,
|
||||
actualizarPendiente,
|
||||
eliminarPendiente,
|
||||
getPendientesByPaciente,
|
||||
setCurrentInternacion,
|
||||
getEstadisticas,
|
||||
login,
|
||||
@@ -1434,5 +1782,6 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P
|
||||
getCamaGrupoId,
|
||||
getInternacionGrupoId,
|
||||
getPacienteGrupoId,
|
||||
isLoaded,
|
||||
};
|
||||
}
|
||||
|
||||
+23
-23
@@ -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%;
|
||||
}
|
||||
@@ -98,8 +98,8 @@
|
||||
|
||||
@media (max-width: 639px) {
|
||||
[data-sonner-toaster] {
|
||||
bottom: 20px !important;
|
||||
top: auto !important;
|
||||
top: 16px !important;
|
||||
bottom: auto !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
width: calc(100% - 32px) !important;
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
import type { ResultadoLaboratorio, AcidoBase, GrupoDeterminacionLaboratorio } from '@/types';
|
||||
|
||||
export const RANGOS_LABORATORIO_DEFAULT: Record<string, { min: number; max: number }> = {
|
||||
'Hematocrito': { min: 36, max: 50 },
|
||||
'Hemoglobina': { min: 12, max: 16.5 },
|
||||
'Leucocitos': { min: 4000, max: 10000 },
|
||||
'Plaquetas': { min: 150000, max: 400000 },
|
||||
'Glucemia': { min: 70, max: 110 },
|
||||
'Urea': { min: 15, max: 45 },
|
||||
'Creatinina': { min: 0.6, max: 1.2 },
|
||||
'Sodio': { min: 135, max: 145 },
|
||||
'Potasio': { min: 3.5, max: 5.0 },
|
||||
'Cloro': { min: 96, max: 106 },
|
||||
'Bilirrubina Total': { min: 0.2, max: 1.2 },
|
||||
'Bilirrubina Directa': { min: 0.0, max: 0.3 },
|
||||
'GOT': { min: 0, max: 40 },
|
||||
'GPT': { min: 0, max: 40 },
|
||||
'Tiempo de Protrombina': { min: 70, max: 100 },
|
||||
'KPTT': { min: 25, max: 38 },
|
||||
'INR': { min: 0.8, max: 1.2 },
|
||||
'Colesterol Total': { min: 0, max: 200 },
|
||||
'Colesterol LDL': { min: 0, max: 100 },
|
||||
'Colesterol No HDL': { min: 0, max: 130 },
|
||||
'Colesterol HDL': { min: 40, max: 100 },
|
||||
'Triglicéridos': { min: 0, max: 150 },
|
||||
'Hierro': { min: 60, max: 170 },
|
||||
'Transferrina': { min: 200, max: 360 },
|
||||
'Porcentaje de Saturación de Transferrina': { min: 20, max: 50 },
|
||||
'Ferritina': { min: 30, max: 400 },
|
||||
'Ácido Fólico': { min: 3.0, max: 17.0 },
|
||||
'Vitamina B12': { min: 200, max: 900 },
|
||||
'Albúmina': { min: 3.5, max: 5.0 },
|
||||
'Calcio Total': { min: 8.5, max: 10.5 },
|
||||
'Fosfatasa Alcalina': { min: 40, max: 130 },
|
||||
'LDH': { min: 135, max: 225 },
|
||||
'NT-proBNP': { min: 0, max: 125 },
|
||||
'Procalcitonina': { min: 0, max: 0.5 },
|
||||
'Proteína C Reactiva': { min: 0, max: 5 },
|
||||
'Eritrosedimentación': { min: 0, max: 20 },
|
||||
'Fósforo': { min: 2.5, max: 4.5 },
|
||||
'Magnesio': { min: 1.6, max: 2.6 },
|
||||
'Calcio Iónico': { min: 1.12, max: 1.32 },
|
||||
'TSH': { min: 0.4, max: 4.0 },
|
||||
'T4 Libre': { min: 0.8, max: 1.8 },
|
||||
'T4 Total': { min: 4.5, max: 12.0 },
|
||||
'T3 Total': { min: 80, max: 200 },
|
||||
'Troponina T / I': { min: 0, max: 0.014 },
|
||||
'CK-MB': { min: 0, max: 25 },
|
||||
'CPK': { min: 20, max: 200 },
|
||||
'Amilasa': { min: 28, max: 100 },
|
||||
'Lipasa': { min: 13, max: 60 },
|
||||
'VCM': { min: 80, max: 100 },
|
||||
'HCM': { min: 27, max: 33 },
|
||||
'CHCM': { min: 32, max: 36 },
|
||||
'RDW': { min: 11.5, max: 14.5 },
|
||||
};
|
||||
|
||||
export function calcularEstadoLaboratorioExtendido(parametro: string, valor: string | number, rangoCustom?: string): 'Normal' | 'Alto' | 'Bajo' | 'Crítico' {
|
||||
const num = typeof valor === 'number' ? valor : parseFloat(valor);
|
||||
if (isNaN(num)) return 'Normal';
|
||||
|
||||
// Check custom range format like "0.4 - 4.0" or "< 200" or "> 40"
|
||||
if (rangoCustom) {
|
||||
const rangeMatch = rangoCustom.match(/(\d+(?:\.\d+)?)\s*[-–—]\s*(\d+(?:\.\d+)?)/);
|
||||
if (rangeMatch) {
|
||||
const min = parseFloat(rangeMatch[1]);
|
||||
const max = parseFloat(rangeMatch[2]);
|
||||
if (!isNaN(min) && !isNaN(max)) {
|
||||
if (num < min) return 'Bajo';
|
||||
if (num > max) return 'Alto';
|
||||
return 'Normal';
|
||||
}
|
||||
}
|
||||
const lessMatch = rangoCustom.match(/<\s*(\d+(?:\.\d+)?)/);
|
||||
if (lessMatch) {
|
||||
const max = parseFloat(lessMatch[1]);
|
||||
if (!isNaN(max) && num > max) return 'Alto';
|
||||
return 'Normal';
|
||||
}
|
||||
const greaterMatch = rangoCustom.match(/>\s*(\d+(?:\.\d+)?)/);
|
||||
if (greaterMatch) {
|
||||
const min = parseFloat(greaterMatch[1]);
|
||||
if (!isNaN(min) && num < min) return 'Bajo';
|
||||
return 'Normal';
|
||||
}
|
||||
}
|
||||
|
||||
const rango = RANGOS_LABORATORIO_DEFAULT[parametro];
|
||||
if (!rango) return 'Normal';
|
||||
|
||||
if (parametro === 'Tiempo de Protrombina' && num > 50) {
|
||||
if (num < 70) return 'Bajo';
|
||||
if (num > 100) return 'Alto';
|
||||
return 'Normal';
|
||||
}
|
||||
|
||||
if (parametro === 'Calcio Iónico') {
|
||||
if (num < 2) {
|
||||
if (num < 1.12) return 'Bajo';
|
||||
if (num > 1.32) return 'Alto';
|
||||
return 'Normal';
|
||||
} else {
|
||||
if (num < 4.5) return 'Bajo';
|
||||
if (num > 5.6) return 'Alto';
|
||||
return 'Normal';
|
||||
}
|
||||
}
|
||||
|
||||
if (num < rango.min) return 'Bajo';
|
||||
if (num > rango.max) return 'Alto';
|
||||
return 'Normal';
|
||||
}
|
||||
|
||||
export interface RecognizedDetermination {
|
||||
grupoId: string;
|
||||
grupoNombre: string;
|
||||
parametro: string;
|
||||
valor: number;
|
||||
unidad: string;
|
||||
estado: 'Normal' | 'Alto' | 'Bajo' | 'Crítico';
|
||||
claveCoincidente: string;
|
||||
rangoReferencia?: string;
|
||||
lineaOriginal: string;
|
||||
}
|
||||
|
||||
export interface PotentialUnknownDetermination {
|
||||
linea: string;
|
||||
posibleNombre: string;
|
||||
posibleValor: number;
|
||||
posibleUnidad: string;
|
||||
}
|
||||
|
||||
export interface ParseResult {
|
||||
resultados: ResultadoLaboratorio[];
|
||||
observaciones: string;
|
||||
observacionesPorGrupo: { grupo: string; lineas: string[] }[];
|
||||
reconocidos: RecognizedDetermination[];
|
||||
desconocidosPotenciales: PotentialUnknownDetermination[];
|
||||
acidoBase: Omit<AcidoBase, 'id'> | null;
|
||||
}
|
||||
|
||||
export const CORE_PARAMETERS_MAPPING = [
|
||||
{ claves: ['hematocrito', 'hto'], nombre: 'Hematocrito', unidad: '%', esPrincipal: true },
|
||||
{ claves: ['hemoglobina', 'hb'], nombre: 'Hemoglobina', unidad: 'g/dL', esPrincipal: true },
|
||||
{ claves: ['recuento de leucocitos', 'glóbulos blancos', 'globulos blancos', 'leucocitos', 'gb'], nombre: 'Leucocitos', unidad: '10³/µl', esPrincipal: true },
|
||||
{ claves: ['recuento de plaquetas', 'plaquetas', 'plaq'], nombre: 'Plaquetas', unidad: '10³/µl', esPrincipal: true },
|
||||
{ claves: ['glucosa', 'glucemia'], nombre: 'Glucemia', unidad: 'mg/dL', esPrincipal: true },
|
||||
{ claves: ['urea'], nombre: 'Urea', unidad: 'mg/dL', esPrincipal: true },
|
||||
{ claves: ['creatinina', 'creat'], nombre: 'Creatinina', unidad: 'mg/dL', esPrincipal: true },
|
||||
{ claves: ['sodio', 'na'], nombre: 'Sodio', unidad: 'mEq/L', esPrincipal: true },
|
||||
{ claves: ['potasio', 'k+', 'k'], nombre: 'Potasio', unidad: 'mEq/L', esPrincipal: true },
|
||||
{ claves: ['cloro', 'cl-', 'cl'], nombre: 'Cloro', unidad: 'mEq/L', esPrincipal: true },
|
||||
{ claves: ['bilirrubina total', 'bt'], nombre: 'Bilirrubina Total', unidad: 'mg/dL', esPrincipal: true },
|
||||
{ claves: ['bilirrubina directa', 'bd'], nombre: 'Bilirrubina Directa', unidad: 'mg/dL', esPrincipal: true },
|
||||
{ claves: ['got', 'ast'], nombre: 'GOT', unidad: 'U/L', esPrincipal: true },
|
||||
{ claves: ['gpt', 'alt'], nombre: 'GPT', unidad: 'U/L', esPrincipal: true },
|
||||
{ claves: ['proteínas totales', 'proteinas totales'], nombre: 'Proteínas Totales', unidad: 'g/dL', esPrincipal: false },
|
||||
{ claves: ['tiempo de protrombina', 't.p.', 't.p', 'tp', 'protrombina'], nombre: 'Tiempo de Protrombina', unidad: '%', esPrincipal: true },
|
||||
{ claves: ['rin', 'inr'], nombre: 'INR', unidad: '', esPrincipal: true },
|
||||
{ claves: ['aptt', 'kptt'], nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
|
||||
];
|
||||
|
||||
const CORE_LIST_SET = new Set([
|
||||
'hematocrito', 'hto', 'hemoglobina', 'hb', 'leucocitos', 'gb', 'plaquetas', 'plaq',
|
||||
'glucemia', 'glucosa', 'urea', 'creatinina', 'creat', 'sodio', 'na', 'potasio', 'k', 'k+',
|
||||
'cloro', 'cl', 'cl-', 'bilirrubina total', 'bt', 'bilirrubina directa', 'bd', 'got', 'ast', 'gpt', 'alt',
|
||||
'tiempo de protrombina', 'tp', 't.p.', 't.p', 'rin', 'inr', 'aptt', 'kptt', 'proteínas totales', 'proteinas totales'
|
||||
]);
|
||||
|
||||
export function esParametroCore(param: string): boolean {
|
||||
return CORE_LIST_SET.has(param.toLowerCase().trim());
|
||||
}
|
||||
|
||||
const regexCache = new Map<string, RegExp>();
|
||||
|
||||
export function matchClaveTexto(lineaLower: string, clave: string): boolean {
|
||||
const c = clave.toLowerCase().trim();
|
||||
if (!c) return false;
|
||||
if (c === 'mg') {
|
||||
const execMatch = /(?:^|[^a-z0-9_])mg(?=[:\s=+\d]|$)(?!\/(?:dl|l|24h|ml)|%)/i.exec(lineaLower);
|
||||
if (!execMatch) return false;
|
||||
const prefix = lineaLower.substring(0, execMatch.index);
|
||||
return !/\d+\s*$/.test(prefix);
|
||||
}
|
||||
if (c.length > 4) {
|
||||
return lineaLower.includes(c);
|
||||
}
|
||||
let regex = regexCache.get(c);
|
||||
if (!regex) {
|
||||
const escaped = c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
regex = new RegExp(`(?:^|[^a-z0-9_])${escaped}(?:$|[^a-z0-9_])`, 'i');
|
||||
regexCache.set(c, regex);
|
||||
}
|
||||
return regex.test(lineaLower);
|
||||
}
|
||||
|
||||
export function parseAcidoBaseTexto(texto: string, fecha?: string, pacienteId?: string): Omit<AcidoBase, 'id'> | null {
|
||||
const lineas = texto.split('\n');
|
||||
const cleanLineas = lineas.map(linea =>
|
||||
linea.replace(/show_chart|list_alt|\t+/g, ' ').replace(/,/g, '.').replace(/\s+/g, ' ').trim().toLowerCase()
|
||||
);
|
||||
|
||||
let ph: number | undefined;
|
||||
let pco2: number | undefined;
|
||||
let po2: number | undefined;
|
||||
let hco3: number | undefined;
|
||||
let be: number | undefined;
|
||||
let sato2: number | undefined;
|
||||
let lactato: number | undefined;
|
||||
let fio2: number | undefined;
|
||||
|
||||
const getValue = (cleanLinea: string, param: string): number | undefined => {
|
||||
const idx = cleanLinea.indexOf(param);
|
||||
if (idx === -1) return undefined;
|
||||
const after = cleanLinea.slice(idx + param.length).trim();
|
||||
const parts = after.split(' ');
|
||||
for (const p of parts) {
|
||||
const v = parseFloat(p);
|
||||
if (!isNaN(v) && v > 0 && v < 1000) return v;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
for (const cleanLinea of cleanLineas) {
|
||||
if ((cleanLinea.includes('estado') && cleanLinea.includes('ácido')) || cleanLinea.includes('base') || cleanLinea.includes('gases en sangre')) {
|
||||
const phMatch = cleanLinea.match(/ph\s+(\d+\.?\d*)/);
|
||||
ph = getValue(cleanLinea, 'ph') || (phMatch ? parseFloat(phMatch[1]) : undefined);
|
||||
pco2 = getValue(cleanLinea, 'pco2') || getValue(cleanLinea, 'pco₂');
|
||||
po2 = getValue(cleanLinea, 'po2') || getValue(cleanLinea, 'po₂');
|
||||
hco3 = getValue(cleanLinea, 'hco3');
|
||||
be = getValue(cleanLinea, 'exceso de base') || getValue(cleanLinea, 'base excess') || getValue(cleanLinea, 'exceso');
|
||||
sato2 = getValue(cleanLinea, 'saturación') || getValue(cleanLinea, 'sato2') || getValue(cleanLinea, 'sat');
|
||||
lactato = getValue(cleanLinea, 'lactato');
|
||||
fio2 = getValue(cleanLinea, 'fio2');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ph) {
|
||||
for (const cleanLinea of cleanLineas) {
|
||||
if (cleanLinea.includes('ph') && !cleanLinea.includes('pco2') && !cleanLinea.includes('pco')) { ph = getValue(cleanLinea, 'ph'); }
|
||||
else if (cleanLinea.includes('pco2') || cleanLinea.includes('pco₂')) { pco2 = getValue(cleanLinea, 'pco2'); }
|
||||
else if (cleanLinea.includes('po2') || cleanLinea.includes('po₂')) { po2 = getValue(cleanLinea, 'po2'); }
|
||||
else if (cleanLinea.includes('hco3')) { hco3 = getValue(cleanLinea, 'hco3'); }
|
||||
else if (cleanLinea.includes('exceso') || cleanLinea.includes('base excess')) { be = getValue(cleanLinea, 'exceso') || getValue(cleanLinea, 'base'); }
|
||||
else if (cleanLinea.includes('saturación') || cleanLinea.includes('sato2')) { sato2 = getValue(cleanLinea, 'saturación') || getValue(cleanLinea, 'sat'); }
|
||||
else if (cleanLinea.includes('lactato')) { lactato = getValue(cleanLinea, 'lactato'); }
|
||||
else if (cleanLinea.includes('fio2')) { fio2 = getValue(cleanLinea, 'fio2'); }
|
||||
}
|
||||
}
|
||||
|
||||
if (ph) {
|
||||
return {
|
||||
pacienteId: pacienteId || '',
|
||||
fecha: fecha || new Date().toISOString().split('T')[0],
|
||||
hora: '',
|
||||
ph,
|
||||
pco2: pco2 || 40,
|
||||
po2: po2 || 85,
|
||||
hco3: hco3 || 24,
|
||||
be: be || 0,
|
||||
sato2: sato2 || 97,
|
||||
lactato,
|
||||
fio2,
|
||||
interpretacion: ''
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseLaboratorioTextoCompleto(
|
||||
texto: string,
|
||||
gruposConfigurados: GrupoDeterminacionLaboratorio[] = [],
|
||||
fecha?: string,
|
||||
pacienteId?: string
|
||||
): ParseResult {
|
||||
const resultados: ResultadoLaboratorio[] = [];
|
||||
const reconocidos: RecognizedDetermination[] = [];
|
||||
const processedLines = new Set<number>();
|
||||
const matchedParamNames = new Set<string>();
|
||||
const lineas = texto.split('\n');
|
||||
|
||||
// 1. Process Core Parameters
|
||||
for (let lIdx = 0; lIdx < lineas.length; lIdx++) {
|
||||
const linea = lineas[lIdx];
|
||||
const lineaLower = linea.toLowerCase().trim();
|
||||
if (!lineaLower) continue;
|
||||
|
||||
for (const core of CORE_PARAMETERS_MAPPING) {
|
||||
if (matchedParamNames.has(core.nombre)) continue;
|
||||
|
||||
let matchedClave = false;
|
||||
let matchedClaveString = '';
|
||||
|
||||
for (const clave of core.claves) {
|
||||
if (matchClaveTexto(lineaLower, clave)) {
|
||||
matchedClave = true;
|
||||
matchedClaveString = clave;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedClave) {
|
||||
let subLinea = linea;
|
||||
const idxClave = lineaLower.indexOf(matchedClaveString);
|
||||
if (idxClave !== -1) {
|
||||
subLinea = linea.substring(idxClave + matchedClaveString.length);
|
||||
}
|
||||
|
||||
const cleanSubLinea = subLinea.replace(/\([^)]*?(?:-|–|—|:|[0-9]\s*[-–—]\s*[0-9]|ref|vr|val|norm|min|max)[^)]*?\)/gi, ' ');
|
||||
let match = cleanSubLinea.match(/(\d+(?:[.,]\d+)?)/);
|
||||
if (!match) match = subLinea.match(/(\d+(?:[.,]\d+)?)/);
|
||||
if (!match) match = linea.match(/[a-zA-ZáéíóúñÁÉÍÓÚÑ]+.*?(\d+(?:[.,]\d+)?)/);
|
||||
|
||||
if (match && match[1]) {
|
||||
const valor = parseFloat(match[1].replace(',', '.'));
|
||||
if (!isNaN(valor) && valor > 0 && valor < 1000000) {
|
||||
let valorFinal = valor;
|
||||
if (core.nombre === 'Leucocitos' || core.nombre === 'Plaquetas') {
|
||||
if (valor < 200) valorFinal = valor * 1000;
|
||||
}
|
||||
|
||||
let unidadFinal = core.unidad;
|
||||
if (core.nombre === 'Tiempo de Protrombina') {
|
||||
if (lineaLower.includes('seg') || lineaLower.includes('segundo')) {
|
||||
unidadFinal = 'seg';
|
||||
} else if (lineaLower.includes('%')) {
|
||||
unidadFinal = '%';
|
||||
}
|
||||
}
|
||||
|
||||
resultados.push({
|
||||
parametro: core.nombre,
|
||||
valor: valorFinal,
|
||||
unidad: unidadFinal,
|
||||
estado: calcularEstadoLaboratorioExtendido(core.nombre, String(valorFinal))
|
||||
});
|
||||
|
||||
matchedParamNames.add(core.nombre);
|
||||
processedLines.add(lIdx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process Configured Dynamic Groups of Other Determinations
|
||||
const activeGroups = (gruposConfigurados || []).filter(g => g.activo !== false);
|
||||
|
||||
for (const grupo of activeGroups) {
|
||||
for (const det of (grupo.determinaciones || [])) {
|
||||
if (matchedParamNames.has(det.nombre)) continue;
|
||||
|
||||
for (let lIdx = 0; lIdx < lineas.length; lIdx++) {
|
||||
const linea = lineas[lIdx];
|
||||
const lineaLower = linea.toLowerCase().trim();
|
||||
if (!lineaLower) continue;
|
||||
|
||||
let matchedClave = false;
|
||||
let matchedClaveString = '';
|
||||
|
||||
for (const clave of (det.claves || [])) {
|
||||
if (det.nombre === 'Colesterol Total' && clave === 'colesterol') {
|
||||
if (lineaLower.includes('ldl') || lineaLower.includes('hdl') || lineaLower.includes('no')) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (det.nombre === 'Transferrina' && clave === 'transferrina') {
|
||||
if (lineaLower.includes('saturac') || lineaLower.includes('sat') || lineaLower.includes('%')) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (matchClaveTexto(lineaLower, clave)) {
|
||||
matchedClave = true;
|
||||
matchedClaveString = clave;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedClave) {
|
||||
let subLinea = linea;
|
||||
const idxClave = lineaLower.indexOf(matchedClaveString);
|
||||
if (idxClave !== -1) {
|
||||
subLinea = linea.substring(idxClave + matchedClaveString.length);
|
||||
}
|
||||
|
||||
const cleanSubLinea = subLinea.replace(/\([^)]*?(?:-|–|—|:|[0-9]\s*[-–—]\s*[0-9]|ref|vr|val|norm|min|max)[^)]*?\)/gi, ' ');
|
||||
let match = cleanSubLinea.match(/(\d+(?:[.,]\d+)?)/);
|
||||
if (!match) match = subLinea.match(/(\d+(?:[.,]\d+)?)/);
|
||||
if (!match) match = linea.match(/[a-zA-ZáéíóúñÁÉÍÓÚÑ]+.*?(\d+(?:[.,]\d+)?)/);
|
||||
|
||||
if (match && match[1]) {
|
||||
const valor = parseFloat(match[1].replace(',', '.'));
|
||||
if (!isNaN(valor) && valor >= 0 && valor < 10000000) {
|
||||
const estado = calcularEstadoLaboratorioExtendido(det.nombre, valor, det.rangoReferencia);
|
||||
|
||||
const recItem: RecognizedDetermination = {
|
||||
grupoId: grupo.id,
|
||||
grupoNombre: grupo.nombreGrupo,
|
||||
parametro: det.nombre,
|
||||
valor,
|
||||
unidad: det.unidad,
|
||||
estado,
|
||||
claveCoincidente: matchedClaveString,
|
||||
rangoReferencia: det.rangoReferencia,
|
||||
lineaOriginal: linea.trim()
|
||||
};
|
||||
|
||||
reconocidos.push(recItem);
|
||||
processedLines.add(lIdx);
|
||||
matchedParamNames.add(det.nombre);
|
||||
|
||||
resultados.push({
|
||||
parametro: det.nombre,
|
||||
valor,
|
||||
unidad: det.unidad,
|
||||
estado,
|
||||
valorReferencia: det.rangoReferencia
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Detect Potential Unrecognized Determinations (Training Suggestions)
|
||||
const desconocidosPotenciales: PotentialUnknownDetermination[] = [];
|
||||
for (let lIdx = 0; lIdx < lineas.length; lIdx++) {
|
||||
if (processedLines.has(lIdx)) continue;
|
||||
const rawLine = lineas[lIdx].trim();
|
||||
if (!rawLine || rawLine.length < 3 || rawLine.length > 120) continue;
|
||||
|
||||
// Check if line looks like: Name : 12.3 mg/dL OR Name 12.3
|
||||
const pat1 = /^([a-zA-ZáéíóúñÁÉÍÓÚÑ\s/().%+-]{3,40})[:=]\s*(\d+(?:[.,]\d+)?)\s*([a-zA-Z/µ%³0-9-]*)/;
|
||||
const m1 = rawLine.match(pat1);
|
||||
if (m1) {
|
||||
const posNombre = m1[1].trim();
|
||||
const posVal = parseFloat(m1[2].replace(',', '.'));
|
||||
const posUnidad = m1[3].trim();
|
||||
if (posNombre && !isNaN(posVal) && posNombre.toLowerCase() !== 'fecha' && posNombre.toLowerCase() !== 'hora' && posNombre.toLowerCase() !== 'cama' && posNombre.toLowerCase() !== 'dni') {
|
||||
desconocidosPotenciales.push({
|
||||
linea: rawLine,
|
||||
posibleNombre: posNombre,
|
||||
posibleValor: posVal,
|
||||
posibleUnidad: posUnidad
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Build Organized Observaciones per Group
|
||||
const observacionesPorGrupo: { grupo: string; lineas: string[] }[] = [];
|
||||
for (const grupo of activeGroups) {
|
||||
const itemsDelGrupo = reconocidos.filter(r => r.grupoId === grupo.id);
|
||||
if (itemsDelGrupo.length > 0) {
|
||||
const lineasGrupo: string[] = [];
|
||||
for (const item of itemsDelGrupo) {
|
||||
lineasGrupo.push(`- ${item.parametro}: ${item.valor} ${item.unidad}`);
|
||||
}
|
||||
observacionesPorGrupo.push({
|
||||
grupo: grupo.nombreGrupo.toUpperCase(),
|
||||
lineas: lineasGrupo
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build the complete formatted observations string
|
||||
const blocks: string[] = [];
|
||||
for (const g of observacionesPorGrupo) {
|
||||
blocks.push(`${g.grupo}:\n${g.lineas.join('\n')}`);
|
||||
}
|
||||
const observacionesStr = blocks.join('\n\n');
|
||||
|
||||
// 5. Parse Gasometría / Ácido Base
|
||||
const acidoBase = parseAcidoBaseTexto(texto, fecha, pacienteId);
|
||||
|
||||
return {
|
||||
resultados,
|
||||
observaciones: observacionesStr,
|
||||
observacionesPorGrupo,
|
||||
reconocidos,
|
||||
desconocidosPotenciales,
|
||||
acidoBase
|
||||
};
|
||||
}
|
||||
|
||||
export function buildObservacionesFromResults(
|
||||
resultadosArray: ResultadoLaboratorio[],
|
||||
gruposConfigurados: GrupoDeterminacionLaboratorio[] = []
|
||||
): string {
|
||||
if (!resultadosArray || resultadosArray.length === 0) return '';
|
||||
const blocks: string[] = [];
|
||||
const processedParams = new Set<string>();
|
||||
|
||||
const activeGroups = (gruposConfigurados || []).filter(g => g.activo !== false);
|
||||
|
||||
for (const grupo of activeGroups) {
|
||||
const groupItems: string[] = [];
|
||||
for (const det of (grupo.determinaciones || [])) {
|
||||
const match = resultadosArray.find(r => r.parametro === det.nombre);
|
||||
if (match) {
|
||||
groupItems.push(`- ${match.parametro}: ${match.valor} ${match.unidad}`);
|
||||
processedParams.add(match.parametro);
|
||||
}
|
||||
}
|
||||
if (groupItems.length > 0) {
|
||||
blocks.push(`${grupo.nombreGrupo.toUpperCase()}:\n${groupItems.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Any other non-core results not belonging to known groups
|
||||
const uncategorized = resultadosArray.filter(r => !esParametroCore(r.parametro) && !processedParams.has(r.parametro));
|
||||
if (uncategorized.length > 0) {
|
||||
const uncatLines = uncategorized.map(u => `- ${u.parametro}: ${u.valor} ${u.unidad}`);
|
||||
blocks.push(`OTRAS DETERMINACIONES:\n${uncatLines.join('\n')}`);
|
||||
}
|
||||
|
||||
return blocks.join('\n\n');
|
||||
}
|
||||
+49
-1
@@ -14,9 +14,16 @@ export function formatDateDDMMYYYY(dateString: string): string {
|
||||
return `${day}-${month}-${year}`;
|
||||
}
|
||||
|
||||
export function getLocalToday(): string {
|
||||
const d = new Date();
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function getNombreProfesional(user?: Usuario | null): string {
|
||||
if (!user) return 'Admin';
|
||||
if (user.rol === 'admin') return 'Admin';
|
||||
const apellido = user.apellido?.trim() || '';
|
||||
const nombre = user.nombre?.trim() || '';
|
||||
if (apellido && nombre) return `${apellido}, ${nombre}`;
|
||||
@@ -45,6 +52,47 @@ export function isCamaFueraDeGrupo(
|
||||
return computeSector(cama?.numero || '') === 'Fuera de Área';
|
||||
}
|
||||
|
||||
export function sortCamas<T extends { numero: string }>(camas: T[]): T[] {
|
||||
if (!camas || camas.length <= 1) return camas || [];
|
||||
|
||||
const parseBedKey = (numero: string) => {
|
||||
const clean = (numero || '').trim();
|
||||
const match = clean.match(/^(\d{3})-(\d+)$/);
|
||||
if (!match) return { weight: 10, salaDir: 0, cama: 0 };
|
||||
|
||||
const sala = parseInt(match[1], 10);
|
||||
const cama = parseInt(match[2], 10);
|
||||
const grupo = Math.floor(sala / 100);
|
||||
const esPar = sala % 2 === 0;
|
||||
|
||||
let weight = 10;
|
||||
let isDesc = false;
|
||||
|
||||
if (grupo === 2) {
|
||||
if (!esPar) { weight = 1; isDesc = true; }
|
||||
else { weight = 2; isDesc = false; }
|
||||
} else if (grupo === 3) {
|
||||
if (esPar) { weight = 3; isDesc = false; }
|
||||
else { weight = 4; isDesc = true; }
|
||||
} else if (grupo === 4) {
|
||||
if (!esPar) { weight = 5; isDesc = true; }
|
||||
else { weight = 6; isDesc = false; }
|
||||
}
|
||||
|
||||
const salaDir = isDesc ? -sala : sala;
|
||||
return { weight, salaDir, cama };
|
||||
};
|
||||
|
||||
const mapped = camas.map(item => ({ item, key: parseBedKey(item.numero) }));
|
||||
mapped.sort((a, b) => {
|
||||
if (a.key.weight !== b.key.weight) return a.key.weight - b.key.weight;
|
||||
if (a.key.salaDir !== b.key.salaDir) return a.key.salaDir - b.key.salaDir;
|
||||
return a.key.cama - b.key.cama;
|
||||
});
|
||||
|
||||
return mapped.map(m => m.item);
|
||||
}
|
||||
|
||||
export function computeSector(numero: string): "En Área" | "Fuera de Área" {
|
||||
if (!numero) return 'En Área';
|
||||
const cleanNumero = String(numero).trim();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import type { AcidoBase, Paciente } from '@/types';
|
||||
import { getLocalToday } from '@/lib/utils';
|
||||
|
||||
interface AcidoBaseProps {
|
||||
acidosBase: AcidoBase[];
|
||||
@@ -39,7 +40,7 @@ export function AcidoBaseSection({
|
||||
// Formulario
|
||||
const [pacienteSeleccionado, setPacienteSeleccionado] = useState('');
|
||||
const [busquedaPaciente, setBusquedaPaciente] = useState('');
|
||||
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [fecha, setFecha] = useState(getLocalToday());
|
||||
const [hora, setHora] = useState(new Date().toTimeString().slice(0, 5));
|
||||
const [ph, setPh] = useState('');
|
||||
const [pco2, setPco2] = useState('');
|
||||
@@ -53,7 +54,7 @@ export function AcidoBaseSection({
|
||||
const resetFormulario = () => {
|
||||
setPacienteSeleccionado('');
|
||||
setBusquedaPaciente('');
|
||||
setFecha(new Date().toISOString().split('T')[0]);
|
||||
setFecha(getLocalToday());
|
||||
setHora(new Date().toTimeString().slice(0, 5));
|
||||
setPh('');
|
||||
setPco2('');
|
||||
|
||||
+112
-75
@@ -94,24 +94,24 @@ 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="NAF/Pendiente">NAF/Pendiente</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>
|
||||
<SelectItem value="Negativo">Negativo</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,72 +169,105 @@ 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}
|
||||
</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/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/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="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.5 pt-1">
|
||||
{cultivo.sensible && (
|
||||
<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 && (
|
||||
<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-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 p-2 rounded">
|
||||
{cultivo.observaciones}
|
||||
<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>
|
||||
)}
|
||||
|
||||
{cultivo.estado === 'Parcial' && cultivo.germen && (
|
||||
<div className="bg-orange-50 p-3 rounded-lg border border-orange-200">
|
||||
<p className="text-sm font-medium text-orange-800 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
PARCIAL - Germen: {cultivo.germen}
|
||||
</p>
|
||||
{(cultivo.sensible || cultivo.resistente) && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{cultivo.sensible && (
|
||||
<p className="text-xs text-orange-700">Sensible: {cultivo.sensible}</p>
|
||||
)}
|
||||
{cultivo.resistente && (
|
||||
<p className="text-xs text-orange-700">Resistente: {cultivo.resistente}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cultivo.estado === 'Positivo' && cultivo.germen && (
|
||||
<div className="bg-red-50 p-3 rounded-lg border border-red-200">
|
||||
<p className="text-sm font-medium text-red-800 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
Germen: {cultivo.germen}
|
||||
</p>
|
||||
{/* Estado Positivo */}
|
||||
{cultivo.estado === 'Positivo' && (
|
||||
<div className="flex flex-col gap-2 mt-1">
|
||||
<div className="flex flex-col gap-2">
|
||||
{cultivo.germen && (
|
||||
<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 && (
|
||||
<p className="text-xs text-red-600 mt-1">
|
||||
Resultado: {cultivo.fechaResultado}
|
||||
</p>
|
||||
<span className="text-xs text-red-700/80 dark:text-red-300/80 block sm:inline sm:ml-2">
|
||||
(Definitivo: {cultivo.fechaResultado})
|
||||
</span>
|
||||
)}
|
||||
{(cultivo.sensible || cultivo.resistente) && (
|
||||
<div className="mt-2 space-y-2">
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cultivo.sensible && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-green-700 mb-1">Sensible a:</p>
|
||||
<p className="text-sm text-green-800">{cultivo.sensible}</p>
|
||||
<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 && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-red-700 mb-1">Resistente a:</p>
|
||||
<p className="text-sm text-red-800">{cultivo.resistente}</p>
|
||||
<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/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 p-3 rounded-lg border border-green-200">
|
||||
<p className="text-sm font-medium text-green-800 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/80 dark:border-green-800/60 pt-2 break-words">
|
||||
<span className="font-semibold">Observaciones:</span> {cultivo.observaciones}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -242,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>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
import type { Paciente, Cama, Grupo, Internacion } from '@/types';
|
||||
import { getNombreProfesional, calcularEdad, computeSector } from '@/lib/utils';
|
||||
import { getNombreProfesional, calcularEdad, computeSector, sortCamas } from '@/lib/utils';
|
||||
import { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||
|
||||
interface EditIngresoProps {
|
||||
@@ -57,48 +57,7 @@ export function EditIngreso({
|
||||
const grupoFueraDeGrupo = grupos.find(a => normalizeStr(a.nombre) === 'fuera de grupo');
|
||||
const grupoFueraDeGrupoId = grupoFueraDeGrupo?.id || grupos[0]?.id || 'fuera-de-grupo';
|
||||
|
||||
const parseBedNumber = (numero: string) => {
|
||||
const match = numero.match(/^(\d{3})-(\d+)$/);
|
||||
if (!match) return { sala: 0, cama: 0 };
|
||||
return {
|
||||
sala: parseInt(match[1]),
|
||||
cama: parseInt(match[2])
|
||||
};
|
||||
};
|
||||
|
||||
const sortedCamas = [...camas].sort((a, b) => {
|
||||
const pa = parseBedNumber(a.numero);
|
||||
const pb = parseBedNumber(b.numero);
|
||||
|
||||
const getOrden = (sala: number) => {
|
||||
const grupo = Math.floor(sala / 100);
|
||||
const esPar = sala % 2 === 0;
|
||||
|
||||
if (grupo === 2) {
|
||||
if (esPar) return { grupo: 2, suborden: 0, sala };
|
||||
return { grupo: 1, suborden: 1, sala };
|
||||
}
|
||||
if (grupo === 3) {
|
||||
if (esPar) return { grupo: 3, suborden: 0, sala };
|
||||
return { grupo: 4, suborden: 1, sala };
|
||||
}
|
||||
if (grupo === 4) {
|
||||
if (esPar) return { grupo: 6, suborden: 0, sala };
|
||||
return { grupo: 5, suborden: 1, sala };
|
||||
}
|
||||
return { grupo: 10, suborden: 1, sala };
|
||||
};
|
||||
|
||||
const oa = getOrden(pa.sala);
|
||||
const ob = getOrden(pb.sala);
|
||||
|
||||
if (oa.grupo !== ob.grupo) return oa.grupo - ob.grupo;
|
||||
if (oa.suborden === ob.suborden) {
|
||||
if (oa.suborden === 0) return (oa.sala || pa.sala) - (ob.sala || pb.sala);
|
||||
return (ob.sala || 0) - (oa.sala || 0);
|
||||
}
|
||||
return oa.suborden - ob.suborden;
|
||||
});
|
||||
const sortedCamas = sortCamas(camas);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!pacienteSeleccionado) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
||||
import { toast } from 'sonner';
|
||||
import type { Evolucion, Internacion, Paciente, Cama, SignosVitales, ExamenFisico } from '@/types';
|
||||
import { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||
import { getNombreProfesional } from '@/lib/utils';
|
||||
import { getNombreProfesional, getLocalToday } from '@/lib/utils';
|
||||
|
||||
|
||||
interface EvolucionesProps {
|
||||
@@ -46,7 +46,7 @@ export function Evoluciones({
|
||||
const [evolucionDetalle, setEvolucionDetalle] = useState<Evolucion | null>(null);
|
||||
const [evolucionEditando, setEvolucionEditando] = useState<Evolucion | null>(null);
|
||||
|
||||
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [fecha, setFecha] = useState(getLocalToday());
|
||||
const [hora, setHora] = useState(new Date().toTimeString().slice(0, 5));
|
||||
const effectiveMedico = getNombreProfesional(currentUser);
|
||||
|
||||
@@ -68,7 +68,7 @@ export function Evoluciones({
|
||||
const [pendientes, setPendientes] = useState('');
|
||||
|
||||
const resetFormulario = () => {
|
||||
setFecha(new Date().toISOString().split('T')[0]);
|
||||
setFecha(getLocalToday());
|
||||
setHora(new Date().toTimeString().slice(0, 5));
|
||||
setTemperatura('');
|
||||
setPresionSistolica('');
|
||||
|
||||
+1302
-474
File diff suppressed because it is too large
Load Diff
+121
-46
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { ClipboardList, Plus, Search, LogOut, CheckCircle2, MoreHorizontal, FileText, Trash2, Bed, User, Calendar, IdCard, Activity, CalendarDays, Clock, Settings } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ClipboardList, Plus, Search, LogOut, CheckCircle2, MoreHorizontal, FileText, Trash2, Bed, User, Calendar, IdCard, Activity, CalendarDays, Clock, Settings, ListTodo } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -11,7 +11,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { toast } from 'sonner';
|
||||
import type { Internacion, Paciente, Cama, Grupo, Evolucion, Laboratorio, Cultivo } from '@/types';
|
||||
import { formatDateDDMMYYYY, getNombreProfesional } from '@/lib/utils';
|
||||
import { formatDateDDMMYYYY, getNombreProfesional, sortCamas, getLocalToday } from '@/lib/utils';
|
||||
import { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||
|
||||
interface InternacionesProps {
|
||||
@@ -26,13 +26,15 @@ interface InternacionesProps {
|
||||
onFinalizarInternacion: (internacionId: string, datos: {
|
||||
fechaEgreso: string;
|
||||
diagnosticoEgreso: string;
|
||||
motivoEgreso: Internacion['motivoEgreso']
|
||||
motivoEgreso: Internacion['motivoEgreso'];
|
||||
servicioAlQuePasa?: string;
|
||||
}) => void;
|
||||
onEliminarInternacion?: (internacionId: string) => Promise<void> | void;
|
||||
getPacienteById: (id: string) => Paciente | undefined;
|
||||
getCamaById: (id: string) => Cama | undefined;
|
||||
onVerHC?: (internacionId: string) => void;
|
||||
onNuevoIngreso?: () => void;
|
||||
onVerPendientesDeSala?: () => void;
|
||||
}
|
||||
|
||||
export function Internaciones({
|
||||
@@ -47,9 +49,14 @@ export function Internaciones({
|
||||
grupos,
|
||||
onVerHC,
|
||||
onNuevoIngreso,
|
||||
onVerPendientesDeSala,
|
||||
}: InternacionesProps) {
|
||||
const { currentUser } = useHospitalStore();
|
||||
const [filtroEstado, setFiltroEstado] = useState<'todas' | 'activas' | 'finalizadas'>('todas');
|
||||
const { currentUser, refreshState } = useHospitalStore();
|
||||
|
||||
useEffect(() => {
|
||||
refreshState();
|
||||
}, [refreshState]);
|
||||
const [filtroEstado, setFiltroEstado] = useState<'todas' | 'activas' | 'finalizadas'>('activas');
|
||||
const [dialogoNuevaAbierto, setDialogoNuevaAbierto] = useState(false);
|
||||
const [dialogoEgresoAbierto, setDialogoEgresoAbierto] = useState(false);
|
||||
const [dialogoEliminarAbierto, setDialogoEliminarAbierto] = useState(false);
|
||||
@@ -69,6 +76,7 @@ export function Internaciones({
|
||||
const [fechaEgreso, setFechaEgreso] = useState('');
|
||||
const [diagnosticoEgreso, setDiagnosticoEgreso] = useState('');
|
||||
const [motivoEgreso, setMotivoEgreso] = useState<Internacion['motivoEgreso']>('Alta médica');
|
||||
const [servicioAlQuePasa, setServicioAlQuePasa] = useState('');
|
||||
|
||||
const resetFormularioNueva = () => {
|
||||
setPacienteSeleccionado('');
|
||||
@@ -83,6 +91,7 @@ export function Internaciones({
|
||||
setFechaEgreso('');
|
||||
setDiagnosticoEgreso('');
|
||||
setMotivoEgreso('Alta médica');
|
||||
setServicioAlQuePasa('');
|
||||
setInternacionSeleccionada(null);
|
||||
};
|
||||
|
||||
@@ -113,16 +122,29 @@ export function Internaciones({
|
||||
|
||||
const handleFinalizarInternacion = () => {
|
||||
if (internacionSeleccionada && fechaEgreso && diagnosticoEgreso && motivoEgreso) {
|
||||
if (motivoEgreso === 'Pase servicio' && !servicioAlQuePasa.trim()) {
|
||||
toast.error('Por favor indique el servicio al que pasa');
|
||||
return;
|
||||
}
|
||||
onFinalizarInternacion(internacionSeleccionada.id, {
|
||||
fechaEgreso,
|
||||
diagnosticoEgreso,
|
||||
motivoEgreso,
|
||||
servicioAlQuePasa: motivoEgreso === 'Pase servicio' ? servicioAlQuePasa : undefined,
|
||||
});
|
||||
resetFormularioEgreso();
|
||||
setDialogoEgresoAbierto(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sortedCamas = sortCamas(camas);
|
||||
|
||||
const getCamaIndex = (camaId?: string) => {
|
||||
if (!camaId) return 999999;
|
||||
const index = sortedCamas.findIndex(c => c.id === camaId);
|
||||
return index !== -1 ? index : 999999;
|
||||
};
|
||||
|
||||
const internacionesFiltradas = internaciones.filter(i => {
|
||||
const paciente = getPacienteById(i.pacienteId);
|
||||
const cumpleBusqueda = !busqueda ||
|
||||
@@ -137,39 +159,20 @@ export function Internaciones({
|
||||
(filtroEstado === 'finalizadas' && !i.activa);
|
||||
|
||||
return cumpleBusqueda && cumpleEstado;
|
||||
}).sort((a, b) => new Date(b.fechaIngresoClinica || '').getTime() - new Date(a.fechaIngresoClinica || '').getTime());
|
||||
}).sort((a, b) => {
|
||||
const indexA = getCamaIndex(a.camaId);
|
||||
const indexB = getCamaIndex(b.camaId);
|
||||
if (indexA !== indexB) {
|
||||
return indexA - indexB;
|
||||
}
|
||||
return new Date(b.fechaIngresoClinica || '').getTime() - new Date(a.fechaIngresoClinica || '').getTime();
|
||||
});
|
||||
|
||||
const pacientesSinInternar = pacientes.filter(p => {
|
||||
const internacionActiva = internaciones.find(i => i.pacienteId === p.id && i.activa);
|
||||
return !internacionActiva;
|
||||
});
|
||||
|
||||
const parseBedNumber = (numero: string) => {
|
||||
const match = numero.match(/^(\d{3})-(\d+)$/);
|
||||
if (!match) return { sala: 0, cama: 0 };
|
||||
return { sala: parseInt(match[1]), cama: parseInt(match[2]) };
|
||||
};
|
||||
|
||||
const sortedCamas = [...camas].sort((a, b) => {
|
||||
const pa = parseBedNumber(a.numero);
|
||||
const pb = parseBedNumber(b.numero);
|
||||
const getOrden = (sala: number) => {
|
||||
const grupo = Math.floor(sala / 100);
|
||||
const esPar = sala % 2 === 0;
|
||||
if (grupo === 2) return esPar ? { grupo: 2, suborden: 0, sala } : { grupo: 1, suborden: 1, sala };
|
||||
if (grupo === 3) return esPar ? { grupo: 3, suborden: 0, sala } : { grupo: 4, suborden: 1, sala };
|
||||
if (grupo === 4) return esPar ? { grupo: 6, suborden: 0, sala } : { grupo: 5, suborden: 1, sala };
|
||||
return { grupo: 10, suborden: 1, sala };
|
||||
};
|
||||
const oa = getOrden(pa.sala);
|
||||
const ob = getOrden(pb.sala);
|
||||
if (oa.grupo !== ob.grupo) return oa.grupo - ob.grupo;
|
||||
if (oa.suborden === ob.suborden) {
|
||||
return oa.suborden === 0 ? (oa.sala - ob.sala) : (ob.sala - oa.sala);
|
||||
}
|
||||
return oa.suborden - ob.suborden;
|
||||
});
|
||||
|
||||
const getGrupoName = (grupoId?: string) => grupos.find(a => a.id === grupoId)?.nombre;
|
||||
|
||||
const calcularEdad = (fechaNacimiento?: string) => {
|
||||
@@ -212,7 +215,7 @@ export function Internaciones({
|
||||
|
||||
const abrirDialogoEgreso = (internacion: Internacion) => {
|
||||
setInternacionSeleccionada(internacion);
|
||||
setFechaEgreso(new Date().toISOString().split('T')[0]);
|
||||
setFechaEgreso(getLocalToday());
|
||||
setDialogoEgresoAbierto(true);
|
||||
};
|
||||
|
||||
@@ -229,9 +232,10 @@ export function Internaciones({
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Gestión de internaciones en sala</p>
|
||||
</div>
|
||||
<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>
|
||||
@@ -357,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}
|
||||
@@ -368,6 +372,16 @@ export function Internaciones({
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onVerPendientesDeSala ? onVerPendientesDeSala() : null}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ListTodo className="h-4 w-4" />
|
||||
Pendientes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtros */}
|
||||
@@ -457,6 +471,12 @@ export function Internaciones({
|
||||
<span>Fecha Ingreso</span>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
<div className="flex items-center justify-center gap-1.5">
|
||||
<CalendarDays className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400 shrink-0" />
|
||||
<span>Fecha Egreso</span>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
<div className="flex items-center justify-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400 shrink-0" />
|
||||
@@ -497,19 +517,63 @@ export function Internaciones({
|
||||
{paciente?.dni || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{internacion.activa ? (
|
||||
{(() => {
|
||||
if (internacion.activa) {
|
||||
return (
|
||||
<Badge className="bg-purple-100 text-purple-800 dark:bg-purple-950/80 dark:text-purple-300 border-purple-200 hover:bg-purple-100">
|
||||
Activa
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 border-gray-300">
|
||||
);
|
||||
}
|
||||
const motivo = internacion.motivoEgreso;
|
||||
if (motivo === 'Alta médica') {
|
||||
return (
|
||||
<Badge variant="outline" className="bg-emerald-100 text-emerald-800 dark:bg-emerald-950/80 dark:text-emerald-300 border-emerald-200 hover:bg-emerald-100">
|
||||
Alta
|
||||
</Badge>
|
||||
)}
|
||||
);
|
||||
}
|
||||
if (motivo === 'Egreso Voluntario') {
|
||||
return (
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800 dark:bg-blue-950/80 dark:text-blue-300 border-blue-200 hover:bg-blue-100">
|
||||
Egreso Voluntario
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (motivo === 'Obito') {
|
||||
return (
|
||||
<Badge variant="outline" className="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-300 border-slate-300 hover:bg-slate-200">
|
||||
Óbito
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (motivo === 'Pase servicio') {
|
||||
return (
|
||||
<Badge variant="outline" className="bg-indigo-100 text-indigo-800 dark:bg-indigo-950/80 dark:text-indigo-300 border-indigo-200 hover:bg-indigo-100">
|
||||
Pase Servicio
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (motivo === 'Derivación') {
|
||||
return (
|
||||
<Badge variant="outline" className="bg-amber-100 text-amber-800 dark:bg-amber-950/80 dark:text-amber-300 border-amber-200 hover:bg-amber-100">
|
||||
Derivación
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge variant="outline" className="bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 border-gray-300 hover:bg-gray-100">
|
||||
Alta
|
||||
</Badge>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-xs whitespace-nowrap">
|
||||
{fechaIng}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-xs whitespace-nowrap">
|
||||
{internacion.fechaEgreso ? formatDateDDMMYYYY(internacion.fechaEgreso) : '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-xs whitespace-nowrap">
|
||||
{(() => {
|
||||
const dias = calcularDiasDuracion(internacion);
|
||||
@@ -572,7 +636,7 @@ export function Internaciones({
|
||||
<Dialog open={dialogoEgresoAbierto} onOpenChange={(open) => { if (!open) resetFormularioEgreso(); setDialogoEgresoAbierto(open); }}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dar Alta</DialogTitle>
|
||||
<DialogTitle>Registrar Egreso</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@@ -596,19 +660,30 @@ export function Internaciones({
|
||||
</div>
|
||||
<div>
|
||||
<Label>Motivo de Egreso *</Label>
|
||||
<Select value={motivoEgreso} onValueChange={(v) => setMotivoEgreso(v as Internacion['motivoEgreso'])}>
|
||||
<Select value={motivoEgreso} onValueChange={(v) => { setMotivoEgreso(v as Internacion['motivoEgreso']); if (v !== 'Pase servicio') setServicioAlQuePasa(''); }}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Alta médica">Alta médica</SelectItem>
|
||||
<SelectItem value="Alta voluntaria">Alta voluntaria</SelectItem>
|
||||
<SelectItem value="Egreso Voluntario">Egreso Voluntario</SelectItem>
|
||||
<SelectItem value="Derivación">Derivación</SelectItem>
|
||||
<SelectItem value="Fallecimiento">Fallecimiento</SelectItem>
|
||||
<SelectItem value="Obito">Obito</SelectItem>
|
||||
<SelectItem value="Pase servicio">Pase servicio</SelectItem>
|
||||
<SelectItem value="Otro">Otro</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{motivoEgreso === 'Pase servicio' && (
|
||||
<div>
|
||||
<Label>Servicio al que pasa *</Label>
|
||||
<Input
|
||||
value={servicioAlQuePasa}
|
||||
onChange={(e) => setServicioAlQuePasa(e.target.value)}
|
||||
placeholder="Ej. Terapia Intensiva, Quirófano, etc."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Label>Diagnóstico de Egreso *</Label>
|
||||
<textarea
|
||||
@@ -618,10 +693,10 @@ export function Internaciones({
|
||||
placeholder="Ingrese el diagnóstico de egreso..."
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline"
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleFinalizarInternacion}
|
||||
disabled={!fechaEgreso || !diagnosticoEgreso || !motivoEgreso}
|
||||
disabled={!fechaEgreso || !diagnosticoEgreso || !motivoEgreso || (motivoEgreso === 'Pase servicio' && !servicioAlQuePasa.trim())}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||||
Confirmar Egreso
|
||||
|
||||
+12
-6
@@ -11,7 +11,8 @@ import {
|
||||
Sun,
|
||||
Moon,
|
||||
LogOut,
|
||||
UserCog
|
||||
UserCog,
|
||||
Terminal
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetDescription, SheetHeader } from '@/components/ui/sheet';
|
||||
@@ -59,10 +60,11 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
|
||||
|
||||
if ((currentUser?.rol as string) === 'admin') {
|
||||
filteredMenuItems.push({ vista: 'usuarios', label: 'Usuarios', icon: UserCog });
|
||||
filteredMenuItems.push({ vista: 'sistema', label: 'Sistema', icon: Terminal });
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -70,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>
|
||||
);
|
||||
@@ -200,8 +206,8 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 lg:ml-64 pt-16 lg:pt-0 min-h-screen">
|
||||
<div className="p-4 lg:p-8 max-w-7xl mx-auto">
|
||||
<main className="flex-1 min-w-0 lg:ml-64 pt-16 lg:pt-0 min-h-screen">
|
||||
<div className="p-4 lg:p-8 max-w-7xl mx-auto w-full">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Shield, Lock, User } from 'lucide-react';
|
||||
import { Lock, User, Hospital } from 'lucide-react';
|
||||
|
||||
export function Login() {
|
||||
const { login } = useHospitalStore();
|
||||
@@ -30,12 +30,14 @@ export function Login() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900 p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center">
|
||||
<Shield className="w-8 h-8 text-white" />
|
||||
<Card className="w-full max-w-md shadow-lg border-neutral-200 dark:border-neutral-800">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<div className="mx-auto mb-3 w-16 h-16 rounded-full bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 flex items-center justify-center shadow-sm">
|
||||
<Hospital className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Sistema de Gestión Hospitalaria</CardTitle>
|
||||
<CardTitle className="text-2xl font-bold tracking-tight text-neutral-900 dark:text-neutral-100">
|
||||
Sistema de Gestión Hospitalaria
|
||||
</CardTitle>
|
||||
<CardDescription>Ingrese sus credenciales para acceder</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
+49
-67
@@ -3,7 +3,7 @@ import { Bed, CheckCircle2, Clock, Wrench, User, Plus, Trash, Edit2, Save, X } f
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { formatDateDDMMYYYY, getNombreProfesional, isCamaFueraDeGrupo, computeSector } from '@/lib/utils';
|
||||
import { formatDateDDMMYYYY, getNombreProfesional, isCamaFueraDeGrupo, computeSector, sortCamas } from '@/lib/utils';
|
||||
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
@@ -86,7 +86,7 @@ export function MapaCamas({
|
||||
}
|
||||
if (cama.estado === 'Ocupada') {
|
||||
// Tipos de aislamiento
|
||||
const aislamientos = ['Aislamiento KPC', 'Aislamiento COVID', 'Aislamiento Clostridium', 'Aislamiento Neutropenico'];
|
||||
const aislamientos = ['Aislamiento KPC', 'Aislamiento COVID', 'Aislamiento Clostridium', 'Aislamiento Neutropenico', 'Aislamiento TBC', 'Aislamiento Escabiosis'];
|
||||
if (aislamientos.includes(cama.tipo)) {
|
||||
return 'bg-red-100 border-red-300 text-red-800 dark:bg-red-950/80 dark:border-red-700/60 dark:text-red-200';
|
||||
}
|
||||
@@ -104,48 +104,7 @@ export function MapaCamas({
|
||||
}
|
||||
};
|
||||
|
||||
const parseBedNumber = (numero: string) => {
|
||||
const match = numero.match(/^(\d{3})-(\d+)$/);
|
||||
if (!match) return { sala: 0, cama: 0 };
|
||||
return {
|
||||
sala: parseInt(match[1]),
|
||||
cama: parseInt(match[2])
|
||||
};
|
||||
};
|
||||
|
||||
const sortedCamas = [...camasFiltradas].sort((a, b) => {
|
||||
const pa = parseBedNumber(a.numero);
|
||||
const pb = parseBedNumber(b.numero);
|
||||
|
||||
const getOrden = (sala: number) => {
|
||||
const grupo = Math.floor(sala / 100);
|
||||
const esPar = sala % 2 === 0;
|
||||
|
||||
if (grupo === 2) {
|
||||
if (esPar) return { grupo: 2, suborden: 0, sala }; // 2XX pares, menor a mayor
|
||||
return { grupo: 1, suborden: 1, sala }; // 2XX impares, mayor a menor
|
||||
}
|
||||
if (grupo === 3) {
|
||||
if (esPar) return { grupo: 3, suborden: 0, sala }; // 3XX pares, menor a mayor
|
||||
return { grupo: 4, suborden: 1, sala }; // 3XX impares, mayor a menor
|
||||
}
|
||||
if (grupo === 4) {
|
||||
if (esPar) return { grupo: 6, suborden: 0, sala }; // 4XX pares, menor a mayor
|
||||
return { grupo: 5, suborden: 1, sala }; // 4XX impares, mayor a menor
|
||||
}
|
||||
return { grupo: 10, suborden: 1, sala };
|
||||
};
|
||||
|
||||
const oa = getOrden(pa.sala);
|
||||
const ob = getOrden(pb.sala);
|
||||
|
||||
if (oa.grupo !== ob.grupo) return oa.grupo - ob.grupo;
|
||||
if (oa.suborden === ob.suborden) {
|
||||
if (oa.suborden === 0) return (oa.sala || pa.sala) - (ob.sala || pb.sala);
|
||||
return (ob.sala || 0) - (oa.sala || 0);
|
||||
}
|
||||
return oa.suborden - ob.suborden;
|
||||
});
|
||||
const sortedCamas = sortCamas(camasFiltradas);
|
||||
|
||||
const handleOcuparCama = () => {
|
||||
if (camaSeleccionada && grupoSeleccionada && pacienteSeleccionado && diagnostico && enfermedadActual && effectiveMedico) {
|
||||
@@ -187,31 +146,38 @@ export function MapaCamas({
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Gestión de camas del servicio</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300 dark:border-green-700">
|
||||
{camas.filter(c => c.estado === 'Disponible' && !isCamaFueraDeGrupo(c, grupos)).length} Disponibles
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300 dark:border-red-700">
|
||||
{camas.filter(c => c.estado === 'Ocupada' && !isCamaFueraDeGrupo(c, grupos)).length} Ocupadas
|
||||
</Badge>
|
||||
<div className="ml-4 flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
||||
<Button variant="outline" onClick={() => {
|
||||
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-800">
|
||||
{camas.filter(c => isCamaFueraDeGrupo(c)).length} Fuera de Área
|
||||
</Badge>
|
||||
<div className="ml-0 sm:ml-4 flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
||||
{currentUser?.rol === 'admin' && (
|
||||
<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>
|
||||
<Button variant="outline" onClick={() => {
|
||||
)}
|
||||
{currentUser?.rol === 'admin' && (
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,7 +187,7 @@ export function MapaCamas({
|
||||
<CardContent className="p-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500 mb-1 block">Grupo / Área</Label>
|
||||
<Label className="text-xs text-gray-500 mb-1 block">Grupo</Label>
|
||||
<Select value={filtroSala} onValueChange={setFiltroSala}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Todos los grupos" />
|
||||
@@ -434,7 +400,7 @@ export function MapaCamas({
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{cama.estado !== 'Disponible' && canEditCama(cama.id) && (
|
||||
{cama.estado !== 'Disponible' && cama.estado !== 'Ocupada' && canEditCama(cama.id) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
@@ -445,7 +411,7 @@ export function MapaCamas({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{cama.estado !== 'Reparacion' && canEditCama(cama.id) && (
|
||||
{cama.estado !== 'Reparacion' && cama.estado !== 'Ocupada' && canEditCama(cama.id) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
@@ -456,7 +422,7 @@ export function MapaCamas({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{cama.estado !== 'Reservada' && canEditCama(cama.id) && (
|
||||
{cama.estado !== 'Reservada' && cama.estado !== 'Ocupada' && canEditCama(cama.id) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
@@ -479,7 +445,7 @@ export function MapaCamas({
|
||||
<Edit2 className="h-4 w-4 mr-1" />Editar
|
||||
</Button>
|
||||
)}
|
||||
{canEditCama(cama.id) && (
|
||||
{canEditCama(cama.id) && currentUser?.rol === 'admin' && (
|
||||
<Button variant="destructive" onClick={() => {
|
||||
onEliminarCama(cama.id);
|
||||
}}>
|
||||
@@ -514,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>
|
||||
@@ -556,7 +522,11 @@ export function MapaCamas({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Número</Label>
|
||||
<Input value={bedNumero} onChange={(e) => setBedNumero(e.target.value)} />
|
||||
<Input
|
||||
value={bedNumero}
|
||||
onChange={(e) => setBedNumero(e.target.value)}
|
||||
disabled={currentUser?.rol !== 'admin'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Tipo</Label>
|
||||
@@ -570,12 +540,18 @@ export function MapaCamas({
|
||||
<SelectItem value="Aislamiento COVID">Aislamiento COVID</SelectItem>
|
||||
<SelectItem value="Aislamiento Clostridium">Aislamiento Clostridium</SelectItem>
|
||||
<SelectItem value="Aislamiento Neutropenico">Aislamiento Neutropenico</SelectItem>
|
||||
<SelectItem value="Aislamiento TBC">Aislamiento TBC</SelectItem>
|
||||
<SelectItem value="Aislamiento Escabiosis">Aislamiento Escabiosis</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Grupo / Área</Label>
|
||||
<Select value={bedGrupoId ?? '__none'} onValueChange={(v) => setBedGrupoId(v === '__none' ? undefined : v)}>
|
||||
<Label>Grupo</Label>
|
||||
<Select
|
||||
value={bedGrupoId ?? '__none'}
|
||||
onValueChange={(v) => setBedGrupoId(v === '__none' ? undefined : v)}
|
||||
disabled={currentUser?.rol !== 'admin'}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Seleccione grupo" />
|
||||
</SelectTrigger>
|
||||
@@ -588,13 +564,16 @@ export function MapaCamas({
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
{editingBed && (
|
||||
{editingBed && currentUser?.rol === 'admin' && (
|
||||
<Button variant="destructive" onClick={() => { onEliminarCama(editingBed.id); setBedDialogOpen(false); }}>
|
||||
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 });
|
||||
@@ -602,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>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
import type { Paciente, Cama, Grupo, Internacion } from '@/types';
|
||||
import { getNombreProfesional, computeSector } from '@/lib/utils';
|
||||
import { getNombreProfesional, computeSector, sortCamas } from '@/lib/utils';
|
||||
import { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||
|
||||
interface NuevoIngresoProps {
|
||||
@@ -64,48 +64,7 @@ export function NuevoIngreso({ pacientes, camas, grupos, onIniciarInternacion, o
|
||||
return !internacionActiva;
|
||||
});
|
||||
|
||||
const parseBedNumber = (numero: string) => {
|
||||
const match = numero.match(/^(\d{3})-(\d+)$/);
|
||||
if (!match) return { sala: 0, cama: 0 };
|
||||
return {
|
||||
sala: parseInt(match[1]),
|
||||
cama: parseInt(match[2])
|
||||
};
|
||||
};
|
||||
|
||||
const sortedCamas = [...camas].sort((a, b) => {
|
||||
const pa = parseBedNumber(a.numero);
|
||||
const pb = parseBedNumber(b.numero);
|
||||
|
||||
const getOrden = (sala: number) => {
|
||||
const grupo = Math.floor(sala / 100);
|
||||
const esPar = sala % 2 === 0;
|
||||
|
||||
if (grupo === 2) {
|
||||
if (esPar) return { grupo: 2, suborden: 0, sala };
|
||||
return { grupo: 1, suborden: 1, sala };
|
||||
}
|
||||
if (grupo === 3) {
|
||||
if (esPar) return { grupo: 3, suborden: 0, sala };
|
||||
return { grupo: 4, suborden: 1, sala };
|
||||
}
|
||||
if (grupo === 4) {
|
||||
if (esPar) return { grupo: 6, suborden: 0, sala };
|
||||
return { grupo: 5, suborden: 1, sala };
|
||||
}
|
||||
return { grupo: 10, suborden: 1, sala };
|
||||
};
|
||||
|
||||
const oa = getOrden(pa.sala);
|
||||
const ob = getOrden(pb.sala);
|
||||
|
||||
if (oa.grupo !== ob.grupo) return oa.grupo - ob.grupo;
|
||||
if (oa.suborden === ob.suborden) {
|
||||
if (oa.suborden === 0) return (oa.sala || pa.sala) - (ob.sala || pb.sala);
|
||||
return (ob.sala || 0) - (oa.sala || 0);
|
||||
}
|
||||
return oa.suborden - ob.suborden;
|
||||
});
|
||||
const sortedCamas = sortCamas(camas);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Validaciones explícitas con feedback claro
|
||||
|
||||
+19
-21
@@ -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}
|
||||
>
|
||||
@@ -336,9 +336,8 @@ export function Pacientes({
|
||||
|
||||
{/* Tabla de Pacientes */}
|
||||
{pacientesFiltrados.length > 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="grid grid-cols-1 w-full">
|
||||
<div className="w-full overflow-x-auto rounded-md border bg-card text-card-foreground shadow-sm pb-2">
|
||||
<Table className="w-full min-w-max text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -402,48 +401,48 @@ export function Pacientes({
|
||||
{pacientesFiltrados.map((paciente) => {
|
||||
const internado = estaInternado(paciente.id);
|
||||
return (
|
||||
<TableRow key={paciente.id}>
|
||||
<TableCell className="font-medium text-gray-900 dark:text-white">
|
||||
<TableRow key={paciente.id} className="hover:bg-muted/50">
|
||||
<TableCell className="font-medium text-gray-900 dark:text-white whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{paciente.apellido}</span>
|
||||
{paciente.alergias && (
|
||||
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200 text-xs px-1.5 py-0">
|
||||
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200 text-xs px-1.5 py-0 shrink-0">
|
||||
<AlertTriangle className="h-2.5 w-2.5 mr-0.5" />
|
||||
Alergia
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-700 dark:text-gray-300">
|
||||
<TableCell className="text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
||||
{paciente.nombre}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-600 dark:text-gray-400 font-mono">
|
||||
<TableCell className="text-gray-600 dark:text-gray-400 font-mono whitespace-nowrap">
|
||||
{paciente.dni}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-600 dark:text-gray-400">
|
||||
<TableCell className="text-gray-600 dark:text-gray-400 whitespace-nowrap">
|
||||
{getEdad(paciente.fechaNacimiento)}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-600 dark:text-gray-400">
|
||||
<TableCell className="text-gray-600 dark:text-gray-400 whitespace-nowrap">
|
||||
{paciente.sexo}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-600 dark:text-gray-400">
|
||||
<TableCell className="text-gray-600 dark:text-gray-400 whitespace-nowrap">
|
||||
{paciente.obraSocial || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-600 dark:text-gray-400">
|
||||
<TableCell className="text-gray-600 dark:text-gray-400 whitespace-nowrap">
|
||||
{paciente.telefono || '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{internado ? (
|
||||
<Badge className="bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300 border-purple-200">
|
||||
Internado
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="bg-gray-50 text-gray-600 dark:bg-gray-800 dark:text-gray-300">
|
||||
<Badge className="bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300 border border-emerald-500 font-medium">
|
||||
Ambulatorio
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -482,8 +481,7 @@ export function Pacientes({
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<Users className="h-16 w-16 mx-auto mb-4 opacity-50" />
|
||||
@@ -491,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>
|
||||
@@ -567,7 +565,7 @@ export function Pacientes({
|
||||
Internado
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="bg-gray-50 text-gray-600 dark:bg-gray-800 dark:text-gray-300 mt-0.5">
|
||||
<Badge className="bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300 border border-emerald-500 font-medium mt-0.5">
|
||||
Ambulatorio
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
ListTodo,
|
||||
Search,
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
Bed,
|
||||
Trash2,
|
||||
FileText,
|
||||
Copy,
|
||||
CheckSquare,
|
||||
Square
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { toast } from 'sonner';
|
||||
import type { Pendiente, Internacion, Paciente, Cama } from '@/types';
|
||||
import { formatDateDDMMYYYY, getNombreProfesional, getLocalToday } from '@/lib/utils';
|
||||
import { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||
|
||||
interface PendientesSalaProps {
|
||||
pendientes: Pendiente[];
|
||||
internaciones: Internacion[];
|
||||
pacientes: Paciente[];
|
||||
camas?: Cama[];
|
||||
onActualizarPendiente: (id: string, datos: Partial<Pendiente>) => Promise<void> | void;
|
||||
onEliminarPendiente: (id: string) => Promise<void> | void;
|
||||
onVerHC: (internacionId: string) => void;
|
||||
onVolver: () => void;
|
||||
getPacienteById: (id: string) => Paciente | undefined;
|
||||
getCamaById: (id: string) => Cama | undefined;
|
||||
}
|
||||
|
||||
export function PendientesSala({
|
||||
pendientes,
|
||||
internaciones,
|
||||
onActualizarPendiente,
|
||||
onEliminarPendiente,
|
||||
onVerHC,
|
||||
onVolver,
|
||||
getPacienteById,
|
||||
getCamaById,
|
||||
}: PendientesSalaProps) {
|
||||
const { currentUser, refreshState } = useHospitalStore();
|
||||
const [busqueda, setBusqueda] = useState('');
|
||||
const [filtroCategoria, setFiltroCategoria] = useState<string>('todos');
|
||||
const [filtroEstado, setFiltroEstado] = useState<string>('pendiente');
|
||||
const [filtroFechaProgramada, setFiltroFechaProgramada] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
refreshState();
|
||||
}, [refreshState]);
|
||||
|
||||
// Active internaciones map for quick lookup
|
||||
const internacionesActivas = internaciones.filter(i => i.activa);
|
||||
const activeInternacionMap = new Map<string, Internacion>();
|
||||
internacionesActivas.forEach(i => {
|
||||
if (i.id) activeInternacionMap.set(i.id, i);
|
||||
if (i.pacienteId) activeInternacionMap.set(i.pacienteId, i);
|
||||
});
|
||||
|
||||
// Filter pending items belonging to active interned patients (or all if activeInternacionMap is empty / fallback)
|
||||
const pendientesSala = (pendientes || []).filter(p => {
|
||||
if (!p) return false;
|
||||
// If internacionId or pacienteId matches an active internacion, include it.
|
||||
// Also if no internaciones match or if we want to be robust, check if patient has any active internacion.
|
||||
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
|
||||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined);
|
||||
|
||||
// Fallback: if no explicit internacion matched but there are active internations, let's also check if patient is in active internaciones
|
||||
if (!internacion && p.pacienteId) {
|
||||
const foundByPaciente = internacionesActivas.find(i => i.pacienteId === p.pacienteId);
|
||||
return !!foundByPaciente;
|
||||
}
|
||||
|
||||
return !!internacion || (!p.internacionId && !p.pacienteId);
|
||||
});
|
||||
|
||||
const totalPendientesActivos = pendientesSala.filter(p => p.estado === 'pendiente').length;
|
||||
|
||||
// Apply search, category, status, and scheduled date filters
|
||||
const pendientesFiltrados = pendientesSala.filter(p => {
|
||||
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
|
||||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined) ||
|
||||
internacionesActivas.find(i => i.pacienteId === p.pacienteId);
|
||||
const paciente = p.pacienteId ? getPacienteById(p.pacienteId) : undefined;
|
||||
const cama = internacion ? getCamaById(internacion.camaId) : undefined;
|
||||
|
||||
const textoMatch = !busqueda ||
|
||||
(paciente && (paciente.nombre.toLowerCase().includes(busqueda.toLowerCase()) || paciente.apellido.toLowerCase().includes(busqueda.toLowerCase()) || paciente.dni.includes(busqueda))) ||
|
||||
(cama && cama.numero.toLowerCase().includes(busqueda.toLowerCase())) ||
|
||||
p.descripcion.toLowerCase().includes(busqueda.toLowerCase()) ||
|
||||
(p.observaciones && p.observaciones.toLowerCase().includes(busqueda.toLowerCase()));
|
||||
|
||||
const categoriaMatch = filtroCategoria === 'todos' || p.categoria === filtroCategoria;
|
||||
const estadoMatch = filtroEstado === 'todos' || p.estado === filtroEstado;
|
||||
|
||||
// Filter by scheduled date (fechaProgramada), not creation date
|
||||
const fechaProgMatch = !filtroFechaProgramada || p.fechaProgramada === filtroFechaProgramada;
|
||||
|
||||
return textoMatch && categoriaMatch && estadoMatch && fechaProgMatch;
|
||||
}).sort((a, b) => {
|
||||
// Sort by scheduled date if available, then creation date
|
||||
const fechaA = a.fechaProgramada || a.fechaCreacion || '';
|
||||
const fechaB = b.fechaProgramada || b.fechaCreacion || '';
|
||||
return fechaB.localeCompare(fechaA);
|
||||
});
|
||||
|
||||
const handleToggleEstado = async (p: Pendiente) => {
|
||||
const nuevoEstado = p.estado === 'realizado' ? 'pendiente' : 'realizado';
|
||||
try {
|
||||
await onActualizarPendiente(p.id, {
|
||||
estado: nuevoEstado,
|
||||
fechaRealizado: nuevoEstado === 'realizado' ? getLocalToday() : undefined,
|
||||
usuarioRealizado: nuevoEstado === 'realizado' && currentUser ? getNombreProfesional(currentUser) : undefined,
|
||||
});
|
||||
toast.success(nuevoEstado === 'realizado' ? 'Marcado como realizado' : 'Marcado como pendiente');
|
||||
} catch {
|
||||
toast.error('Error al cambiar estado');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEliminar = async (id: string) => {
|
||||
try {
|
||||
await onEliminarPendiente(id);
|
||||
toast.success('Pendiente eliminado');
|
||||
} catch {
|
||||
toast.error('Error al eliminar');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopiarPendientes = () => {
|
||||
const activos = pendientesSala.filter(p => p.estado === 'pendiente');
|
||||
if (activos.length === 0) {
|
||||
toast.error('No hay pendientes activos en la sala');
|
||||
return;
|
||||
}
|
||||
const texto = activos.map((p, idx) => {
|
||||
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
|
||||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined);
|
||||
const paciente = p.pacienteId ? getPacienteById(p.pacienteId) : undefined;
|
||||
const cama = internacion ? getCamaById(internacion.camaId) : undefined;
|
||||
const infoPaciente = paciente ? `${paciente.apellido}, ${paciente.nombre}` : 'Paciente';
|
||||
const infoCama = cama ? `[Cama ${cama.numero}]` : '';
|
||||
|
||||
let line = `${idx + 1}. ${infoCama} ${infoPaciente} - [${(p.categoria || 'General').toUpperCase()}] ${p.descripcion}`;
|
||||
if (p.fechaProgramada) line += ` (Prog: ${formatDateDDMMYYYY(p.fechaProgramada)}${p.horaProgramada ? ' ' + p.horaProgramada : ''})`;
|
||||
if (p.prioridad === 'alta') line += ' (ALTA)';
|
||||
if (p.observaciones) line += ` - Obs: ${p.observaciones}`;
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
navigator.clipboard.writeText(`PENDIENTES DE SALA:\n${texto}`);
|
||||
toast.success('Pendientes copiados al portapapeles');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 dark:text-white">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" onClick={onVolver} className="shrink-0">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<ListTodo className="h-6 w-6 text-blue-600 dark:text-blue-400 shrink-0" />
|
||||
Pendientes de Sala
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Gestión y seguimiento de pendientes de todos los pacientes internados ({totalPendientesActivos} pendientes activos)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||
<Button variant="outline" onClick={handleCopiarPendientes} className="gap-2 w-full sm:w-auto">
|
||||
<Copy className="h-4 w-4" />
|
||||
Copiar Pendientes Activos
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Card */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
className="pl-10 text-xs"
|
||||
placeholder="Buscar paciente, cama o descripción..."
|
||||
value={busqueda}
|
||||
onChange={(e) => setBusqueda(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Select value={filtroCategoria} onValueChange={setFiltroCategoria}>
|
||||
<SelectTrigger className="h-9 text-xs">
|
||||
<SelectValue placeholder="Categoría" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="todos">Todas las categorías</SelectItem>
|
||||
<SelectItem value="General">General</SelectItem>
|
||||
<SelectItem value="Estudio">Estudio</SelectItem>
|
||||
<SelectItem value="Procedimiento">Procedimiento</SelectItem>
|
||||
<SelectItem value="Laboratorio">Laboratorio</SelectItem>
|
||||
<SelectItem value="Interconsulta">Interconsulta</SelectItem>
|
||||
<SelectItem value="Tratamiento">Tratamiento</SelectItem>
|
||||
<SelectItem value="Administrativo">Administrativo</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Select value={filtroEstado} onValueChange={setFiltroEstado}>
|
||||
<SelectTrigger className="h-9 text-xs">
|
||||
<SelectValue placeholder="Estado" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="todos">Todos los estados</SelectItem>
|
||||
<SelectItem value="pendiente">Pendientes</SelectItem>
|
||||
<SelectItem value="realizado">Realizados</SelectItem>
|
||||
<SelectItem value="cancelado">Cancelados</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type="date"
|
||||
className="h-9 text-xs"
|
||||
value={filtroFechaProgramada}
|
||||
onChange={(e) => setFiltroFechaProgramada(e.target.value)}
|
||||
placeholder="Fecha programada"
|
||||
/>
|
||||
</div>
|
||||
{filtroFechaProgramada && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setFiltroFechaProgramada('')} className="h-9 px-2 text-xs text-muted-foreground">
|
||||
Limpiar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Lista de Pendientes en Tarjetas (móvil) y Tabla (escritorio) */}
|
||||
{pendientesFiltrados.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-400 bg-card rounded-lg border p-8">
|
||||
<ListTodo className="h-16 w-16 mx-auto mb-4 opacity-40 text-blue-500" />
|
||||
<p className="text-lg font-medium text-gray-700 dark:text-gray-300">No se encontraron pendientes</p>
|
||||
<p className="text-sm text-gray-500 mt-1">Pruebe cambiando los filtros de búsqueda o fecha programada.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Vista móvil en Tarjetas (block sm:hidden) */}
|
||||
<div className="space-y-3 sm:hidden">
|
||||
{pendientesFiltrados.map((p) => {
|
||||
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
|
||||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined);
|
||||
const paciente = p.pacienteId ? getPacienteById(p.pacienteId) : undefined;
|
||||
const cama = internacion ? getCamaById(internacion.camaId) : undefined;
|
||||
const esRealizado = p.estado === 'realizado';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className={`p-4 rounded-lg border bg-card shadow-sm space-y-3 transition-colors ${
|
||||
esRealizado ? 'opacity-60 bg-muted/20 border-muted' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-1 font-semibold text-xs bg-blue-50 dark:bg-blue-950/40 text-blue-700 dark:text-blue-300 px-2 py-1 rounded">
|
||||
<Bed className="h-3.5 w-3.5" />
|
||||
<span>{cama ? `Cama ${cama.numero}` : 'Sin cama'}</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs font-medium">
|
||||
{p.categoria || 'General'}
|
||||
</Badge>
|
||||
{p.prioridad === 'alta' ? (
|
||||
<Badge className="bg-red-100 text-red-800 dark:bg-red-950/80 dark:text-red-300 border-red-200 text-[11px]">
|
||||
Alta
|
||||
</Badge>
|
||||
) : p.prioridad === 'media' ? (
|
||||
<Badge className="bg-amber-100 text-amber-800 dark:bg-amber-950/80 dark:text-amber-300 border-amber-200 text-[11px]">
|
||||
Media
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[11px]">
|
||||
Baja
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 shrink-0"
|
||||
onClick={() => handleToggleEstado(p)}
|
||||
title={esRealizado ? "Marcar como pendiente" : "Marcar como realizado"}
|
||||
>
|
||||
{esRealizado ? (
|
||||
<CheckSquare className="h-5 w-5 text-emerald-600" />
|
||||
) : (
|
||||
<Square className="h-5 w-5 text-gray-400 hover:text-blue-600" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="font-semibold text-sm text-gray-900 dark:text-white">
|
||||
{paciente ? `${paciente.apellido}, ${paciente.nombre}` : 'Paciente desconocido'}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
DNI: {paciente?.dni || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/30 p-2.5 rounded text-xs space-y-1">
|
||||
<div className={`font-medium ${esRealizado ? 'line-through text-muted-foreground' : 'text-gray-800 dark:text-gray-200'}`}>
|
||||
{p.descripcion}
|
||||
</div>
|
||||
{p.observaciones && (
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
<span className="font-semibold">Obs:</span> {p.observaciones}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-1 border-t border-border/50 text-xs">
|
||||
<div>
|
||||
{p.fechaProgramada ? (
|
||||
<div className="flex items-center gap-1 text-blue-700 dark:text-blue-300 font-medium">
|
||||
<Clock className="h-3 w-3 shrink-0" />
|
||||
<span>{formatDateDDMMYYYY(p.fechaProgramada)} {p.horaProgramada ? `a las ${p.horaProgramada}` : ''}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic text-[11px]">Sin programar</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{internacion && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 px-2 text-xs text-blue-600 hover:text-blue-700 hover:bg-blue-50 gap-1"
|
||||
onClick={() => onVerHC(internacion.id)}
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
<span>HC</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 px-2 text-xs text-red-600 hover:text-red-700 hover:bg-red-50 gap-1"
|
||||
onClick={() => handleEliminar(p.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<span>Eliminar</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Vista de Tabla para Escritorio (hidden sm:block) */}
|
||||
<div className="hidden sm:block border rounded-lg bg-card overflow-hidden shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="w-full min-w-[1000px] text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px] text-center">Estado</TableHead>
|
||||
<TableHead className="w-[80px]">Cama</TableHead>
|
||||
<TableHead>Paciente</TableHead>
|
||||
<TableHead className="w-[120px]">Categoría</TableHead>
|
||||
<TableHead className="w-[100px]">Prioridad</TableHead>
|
||||
<TableHead>Descripción / Observaciones</TableHead>
|
||||
<TableHead className="w-[140px]">Fecha Programada</TableHead>
|
||||
<TableHead className="text-center w-[100px]">Acciones</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pendientesFiltrados.map((p) => {
|
||||
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
|
||||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined);
|
||||
const paciente = p.pacienteId ? getPacienteById(p.pacienteId) : undefined;
|
||||
const cama = internacion ? getCamaById(internacion.camaId) : undefined;
|
||||
const esRealizado = p.estado === 'realizado';
|
||||
|
||||
return (
|
||||
<TableRow key={p.id} className={`hover:bg-muted/50 ${esRealizado ? 'opacity-60 bg-muted/20' : ''}`}>
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handleToggleEstado(p)}
|
||||
title={esRealizado ? "Marcar como pendiente" : "Marcar como realizado"}
|
||||
>
|
||||
{esRealizado ? (
|
||||
<CheckSquare className="h-5 w-5 text-emerald-600" />
|
||||
) : (
|
||||
<Square className="h-5 w-5 text-gray-400 hover:text-blue-600" />
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell className="font-semibold text-xs whitespace-nowrap">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Bed className="h-3.5 w-3.5 text-blue-600" />
|
||||
<span>{cama ? cama.numero : '-'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<div className="font-medium text-xs">
|
||||
{paciente ? `${paciente.apellido}, ${paciente.nombre}` : 'Paciente desconocido'}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground font-mono">
|
||||
DNI: {paciente?.dni || 'N/A'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<Badge variant="outline" className="text-xs font-medium">
|
||||
{p.categoria || 'General'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{p.prioridad === 'alta' ? (
|
||||
<Badge className="bg-red-100 text-red-800 dark:bg-red-950/80 dark:text-red-300 border-red-200 text-[11px]">
|
||||
Alta
|
||||
</Badge>
|
||||
) : p.prioridad === 'media' ? (
|
||||
<Badge className="bg-amber-100 text-amber-800 dark:bg-amber-950/80 dark:text-amber-300 border-amber-200 text-[11px]">
|
||||
Media
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[11px]">
|
||||
Baja
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className={`text-xs ${esRealizado ? 'line-through text-muted-foreground' : 'font-medium'}`}>
|
||||
{p.descripcion}
|
||||
</div>
|
||||
{p.observaciones && (
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">
|
||||
Obs: {p.observaciones}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap text-xs">
|
||||
{p.fechaProgramada ? (
|
||||
<div className="flex items-center gap-1 text-blue-700 dark:text-blue-300 font-medium bg-blue-50 dark:bg-blue-950/40 px-2 py-1 rounded w-fit">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{formatDateDDMMYYYY(p.fechaProgramada)} {p.horaProgramada ? `a las ${p.horaProgramada}` : ''}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic text-[11px]">Sin programar</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{internacion && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 text-blue-600 hover:text-blue-700 hover:bg-blue-50"
|
||||
onClick={() => onVerHC(internacion.id)}
|
||||
title="Ver Historia Clínica"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleEliminar(p.id)}
|
||||
title="Eliminar pendiente"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -11,9 +12,9 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
|
||||
import { Plus, Pencil, Trash2, X } from 'lucide-react';
|
||||
import type { Indicacion, IndicacionTipo, ViaAdministracion, TipoPlanHidratacion, TipoInsulina, ATB } from '@/types';
|
||||
import { useHospitalStore } from '@/hooks/useHospitalStore';
|
||||
import { getNombreProfesional } from '@/lib/utils';
|
||||
import { getNombreProfesional, getLocalToday } from '@/lib/utils';
|
||||
|
||||
export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId, add, update, del, movimientos, onAgregarMovimiento, canEdit, atbList = [], addATB, updateATB }: {
|
||||
export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId, add, update, del, movimientos, onAgregarMovimiento, canEdit, atbList = [], addATB, updateATB, portalNode}: {
|
||||
recomendaciones: Indicacion[];
|
||||
internacionId: string;
|
||||
pacienteId?: string;
|
||||
@@ -23,6 +24,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId
|
||||
movimientos?: unknown[];
|
||||
onAgregarMovimiento?: (m: unknown) => void;
|
||||
canEdit?: boolean;
|
||||
portalNode?: HTMLDivElement | null;
|
||||
atbList?: ATB[];
|
||||
addATB?: (a: Omit<ATB, 'id'>) => Promise<unknown>;
|
||||
updateATB?: (id: string, datos: Partial<ATB>) => void;
|
||||
@@ -142,7 +144,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId
|
||||
tipo,
|
||||
estado: 'Activa',
|
||||
medicoCrea: effectiveMedico,
|
||||
fechaCrea: new Date().toISOString().split('T')[0],
|
||||
fechaCrea: getLocalToday(),
|
||||
};
|
||||
|
||||
if (tipo === 'Farmacologica' || tipo === 'Farmacologica Profilactica' || tipo === 'Farmacologica Antibiótico') {
|
||||
@@ -170,7 +172,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const fecha = now.toISOString().split('T')[0] + ' ' + now.toTimeString().split(' ')[0].substring(0, 5);
|
||||
const fecha = getLocalToday() + ' ' + now.toTimeString().split(' ')[0].substring(0, 5);
|
||||
if (edit) {
|
||||
if (edit.tipo === 'Farmacologica Antibiótico' && updateATB && addATB && atbList && pacienteId) {
|
||||
const oldDrug = edit.droga || '';
|
||||
@@ -182,7 +184,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId
|
||||
!a.fechaFinalizacion
|
||||
);
|
||||
if (activeATB) {
|
||||
const todayStr = now.toISOString().split('T')[0];
|
||||
const todayStr = getLocalToday();
|
||||
try {
|
||||
await updateATB(activeATB.id, { fechaFinalizacion: todayStr });
|
||||
await addATB({
|
||||
@@ -241,7 +243,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId
|
||||
|
||||
const handleSuspender = async (i: Indicacion, suspendioMedico: string) => {
|
||||
const now = new Date();
|
||||
const fecha = now.toISOString().split('T')[0] + ' ' + now.toTimeString().split(' ')[0].substring(0, 5);
|
||||
const fecha = getLocalToday() + ' ' + now.toTimeString().split(' ')[0].substring(0, 5);
|
||||
try {
|
||||
if (onAgregarMovimiento) {
|
||||
await onAgregarMovimiento({
|
||||
@@ -255,7 +257,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId
|
||||
}
|
||||
|
||||
if (i.tipo === 'Farmacologica Antibiótico' && updateATB && atbList) {
|
||||
const fechaFin = now.toISOString().split('T')[0];
|
||||
const fechaFin = getLocalToday();
|
||||
const activeATB = atbList.find(a =>
|
||||
a.internacionId === internacionId &&
|
||||
a.antibiotico.toLowerCase() === (i.droga || '').toLowerCase() &&
|
||||
@@ -283,7 +285,18 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId
|
||||
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex justify-between items-center">
|
||||
{portalNode ? createPortal(
|
||||
<>
|
||||
{canEdit && <Button size="sm" variant="outline" onClick={() => { reset(); setDialog(true); }}>
|
||||
<Plus className="h-4 w-4 mr-1.5" />Nueva Indicación
|
||||
</Button>}
|
||||
<Button size="sm" variant="ghost" onClick={() => setShowHistorial(!showHistorial)}>
|
||||
{showHistorial ? 'Ocultar Historial' : `Ver Historial (${movs.length})`}
|
||||
</Button>
|
||||
</>,
|
||||
portalNode
|
||||
) : (
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
{canEdit && <Button size="sm" variant="outline" onClick={() => { reset(); setDialog(true); }}>
|
||||
<Plus className="h-4 w-4 mr-1.5" />Nueva Indicación
|
||||
</Button>}
|
||||
@@ -291,6 +304,7 @@ export function SeccionIndicaciones({ recomendaciones, internacionId, pacienteId
|
||||
{showHistorial ? 'Ocultar Historial' : `Ver Historial (${movs.length})`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={dialog} onOpenChange={setDialog}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+63
-4
@@ -18,6 +18,7 @@ export interface Paciente {
|
||||
nacionalidad?: string;
|
||||
medicacionHabitual?: string;
|
||||
historiaClinica?: string;
|
||||
notas?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +34,7 @@ export interface Cama {
|
||||
numero: string;
|
||||
grupoId?: string;
|
||||
areaId?: string;
|
||||
tipo: 'General' | 'Aislamiento KPC' | 'Aislamiento COVID' | 'Aislamiento Clostridium' | 'Aislamiento Neutropenico';
|
||||
tipo: 'General' | 'Aislamiento KPC' | 'Aislamiento COVID' | 'Aislamiento Clostridium' | 'Aislamiento Neutropenico' | 'Aislamiento TBC' | 'Aislamiento Escabiosis';
|
||||
estado: 'Disponible' | 'Ocupada' | 'Reparacion' | 'Reservada';
|
||||
pacienteId?: string;
|
||||
internacionId?: string;
|
||||
@@ -55,7 +56,8 @@ export interface Internacion {
|
||||
antecedentesEnfermedadActual?: string;
|
||||
diagnosticoEgreso?: string;
|
||||
medicoIngresante: string;
|
||||
motivoEgreso?: 'Alta médica' | 'Alta voluntaria' | 'Derivación' | 'Fallecimiento' | 'Otro';
|
||||
motivoEgreso?: 'Alta médica' | 'Egreso Voluntario' | 'Derivación' | 'Obito' | 'Pase servicio' | 'Otro';
|
||||
servicioAlQuePasa?: string;
|
||||
activa: boolean;
|
||||
apache?: string;
|
||||
derivacion?: string;
|
||||
@@ -118,6 +120,16 @@ export interface ResultadoLaboratorio {
|
||||
estado: 'Normal' | 'Alto' | 'Bajo' | 'Crítico';
|
||||
}
|
||||
|
||||
export interface OtroLaboratorio {
|
||||
id: string;
|
||||
pacienteId: string;
|
||||
internacionId?: string;
|
||||
fecha: string;
|
||||
hora?: string;
|
||||
observaciones: string;
|
||||
categoria?: 'Perfil Lipídico' | 'Perfil Tiroideo' | 'Autoinmunidad' | 'Toracocentesis' | 'Líquido Ascitico' | 'LCR' | 'Otros' | string;
|
||||
}
|
||||
|
||||
export interface Glucemia {
|
||||
id: string;
|
||||
pacienteId: string;
|
||||
@@ -147,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;
|
||||
@@ -155,7 +174,7 @@ export interface Cultivo {
|
||||
fechaToma: string;
|
||||
protocolo?: string;
|
||||
fechaResultado?: string;
|
||||
tipoMuestra: 'HMCx2' | 'RC' | 'PC' | 'UC' | 'LCR' | 'LP' | 'LAsc' | 'LAbd' | 'Coleccion';
|
||||
tipoMuestra: string;
|
||||
germen?: string;
|
||||
sensible?: string;
|
||||
resistente?: string;
|
||||
@@ -236,7 +255,7 @@ export interface MovimientoIndicacion {
|
||||
observaciones?: string;
|
||||
}
|
||||
|
||||
export type Vista = 'dashboard' | 'camas' | 'pacientes' | 'internaciones' | 'evoluciones' | 'laboratorios' | 'acidobase' | 'cultivos' | 'historiaclinica' | 'nuevoingreso' | 'editaringreso' | 'usuarios';
|
||||
export type Vista = 'dashboard' | 'camas' | 'pacientes' | 'internaciones' | 'evoluciones' | 'laboratorios' | 'acidobase' | 'cultivos' | 'historiaclinica' | 'nuevoingreso' | 'editaringreso' | 'usuarios' | 'pendientessala' | 'sistema';
|
||||
|
||||
export type RolUsuario = 'admin' | 'medico' | 'enfermero';
|
||||
|
||||
@@ -254,3 +273,43 @@ export interface Usuario {
|
||||
areaId?: string;
|
||||
fechaCreacion: string;
|
||||
}
|
||||
|
||||
export interface DeterminacionDefinicion {
|
||||
id: string;
|
||||
nombre: string;
|
||||
claves: string[];
|
||||
unidad: string;
|
||||
esPrincipal?: boolean;
|
||||
esAdicional?: boolean;
|
||||
rangoReferencia?: string;
|
||||
descripcion?: string;
|
||||
}
|
||||
|
||||
export interface GrupoDeterminacionLaboratorio {
|
||||
id: string;
|
||||
nombreGrupo: string;
|
||||
descripcion?: string;
|
||||
activo: boolean;
|
||||
orden?: number;
|
||||
determinaciones: DeterminacionDefinicion[];
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Pendiente {
|
||||
id: string;
|
||||
pacienteId: string;
|
||||
internacionId?: string;
|
||||
descripcion: string;
|
||||
prioridad: 'alta' | 'media' | 'baja';
|
||||
estado: 'pendiente' | 'realizado' | 'cancelado';
|
||||
fechaCreacion: string;
|
||||
horaCreacion?: string;
|
||||
fechaRealizado?: string;
|
||||
categoria?: 'General' | 'Estudio' | 'Procedimiento' | 'Laboratorio' | 'Interconsulta' | 'Tratamiento' | 'Administrativo' | string;
|
||||
fechaProgramada?: string;
|
||||
horaProgramada?: string;
|
||||
usuarioId?: string;
|
||||
usuarioNombre?: string;
|
||||
observaciones?: string;
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,12 +1,11 @@
|
||||
import path from "path"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
import { inspectAttr } from 'kimi-plugin-inspect-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [inspectAttr(), react()],
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
Reference in New Issue
Block a user