Agregar tabla interconsultas: DB, store, UI con CRUD completo
This commit is contained in:
@@ -0,0 +1,753 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Generador HC Ingreso – Santojanni</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #f0f4f8; display: flex;
|
||||
flex-direction: column; align-items: center; padding: 40px; }
|
||||
h1 { font-size: 1.2rem; color: #1a3a5c; margin-bottom: 8px; }
|
||||
p { color: #555; font-size: .9rem; margin-bottom: 24px; }
|
||||
button { background: #1a3a5c; color: #fff; border: none; padding: 14px 36px;
|
||||
font-size: 1rem; border-radius: 6px; cursor: pointer; }
|
||||
button:hover { background: #25547a; }
|
||||
#status { margin-top: 16px; font-size: .85rem; color: #444; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Historia Clínica de Ingreso – Hospital Donación F. Santojanni</h1>
|
||||
<p>Hace clic en el botón para generar el PDF con las 5 páginas del formulario en blanco.</p>
|
||||
<button onclick="generarPDF()">Generar PDF</button>
|
||||
<div id="status"></div>
|
||||
|
||||
<script>
|
||||
/* ============================================================
|
||||
CONSTANTES GLOBALES
|
||||
============================================================ */
|
||||
const A4W = 595.28;
|
||||
const A4H = 841.89;
|
||||
const ML = 25; // margen izquierdo
|
||||
const MR = 25; // margen derecho
|
||||
const INNER = A4W - ML - MR; // ancho útil: 545.28
|
||||
|
||||
// Colores
|
||||
const NEGRO = PDFLib.rgb(0, 0, 0);
|
||||
const GRIS_HEAD = PDFLib.rgb(0.82, 0.82, 0.82); // fondo encabezados de sección
|
||||
const GRIS_LABEL = PDFLib.rgb(0.93, 0.93, 0.93); // fondo celdas etiqueta
|
||||
const BLANCO = PDFLib.rgb(1, 1, 1);
|
||||
|
||||
/* ============================================================
|
||||
HELPERS
|
||||
============================================================ */
|
||||
|
||||
/** Dibuja un rectángulo con borde negro (relleno opcional). */
|
||||
function rect(page, x, y, w, h, { fill = null, borderWidth = 0.5 } = {}) {
|
||||
if (fill) page.drawRectangle({ x, y, width: w, height: h, color: fill });
|
||||
page.drawRectangle({ x, y, width: w, height: h,
|
||||
borderColor: NEGRO, borderWidth, color: fill ?? PDFLib.rgb(1,1,1) });
|
||||
}
|
||||
|
||||
/** Dibuja texto centrado horizontalmente en un rango x→x+w. */
|
||||
function textCenter(page, txt, x, y, w, { font, size = 8, color = NEGRO } = {}) {
|
||||
const tw = font.widthOfTextAtSize(txt, size);
|
||||
page.drawText(txt, { x: x + (w - tw) / 2, y, size, font, color });
|
||||
}
|
||||
|
||||
/** Caja de sección con fondo gris y título centrado en negrita. */
|
||||
function sectionHeader(page, label, x, y, w, h = 14, { bold, regular } = {}) {
|
||||
rect(page, x, y, w, h, { fill: GRIS_HEAD });
|
||||
textCenter(page, label, x, y + 3, w, { font: bold, size: 8 });
|
||||
return y - h; // retorna la Y del borde inferior de la caja
|
||||
}
|
||||
|
||||
/** Celda con etiqueta izquierda (gris) y área de escritura (blanca). */
|
||||
function labelCell(page, label, x, y, labelW, totalW, h, { regular, bold, fontSize = 7 } = {}) {
|
||||
// fondo etiqueta
|
||||
page.drawRectangle({ x, y, width: labelW, height: h, color: GRIS_LABEL });
|
||||
page.drawRectangle({ x, y, width: labelW, height: h,
|
||||
borderColor: NEGRO, borderWidth: 0.4, color: GRIS_LABEL });
|
||||
// área escribible
|
||||
page.drawRectangle({ x: x + labelW, y, width: totalW - labelW, height: h,
|
||||
borderColor: NEGRO, borderWidth: 0.4, color: BLANCO });
|
||||
page.drawText(label, { x: x + 2, y: y + 2.5, size: fontSize, font: regular, color: NEGRO });
|
||||
}
|
||||
|
||||
/** Línea horizontal simple. */
|
||||
function hLine(page, x, y, w) {
|
||||
page.drawLine({ start: { x, y }, end: { x: x + w, y }, thickness: 0.4, color: NEGRO });
|
||||
}
|
||||
|
||||
/** Línea vertical simple. */
|
||||
function vLine(page, x, y1, y2) {
|
||||
page.drawLine({ start: { x, y: y1 }, end: { x, y: y2 }, thickness: 0.4, color: NEGRO });
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CABECERA COMÚN (páginas 1-5)
|
||||
============================================================ */
|
||||
function drawPageHeader(page, pageNum, bold, regular) {
|
||||
const top = A4H - 18;
|
||||
|
||||
// Logo placeholder izquierdo (círculo con "BA")
|
||||
page.drawCircle({ x: ML + 12, y: top, size: 11, color: PDFLib.rgb(0.2, 0.4, 0.7) });
|
||||
page.drawText("BA", { x: ML + 6, y: top - 4, size: 7, font: bold, color: BLANCO });
|
||||
|
||||
// Texto central
|
||||
const titulo1 = "HOSPITAL DONACIÓN F. SANTOJANNI – DIVISIÓN CLÍNICA MÉDICA";
|
||||
const titulo2 = "HISTORIA CLÍNICA DE INGRESO";
|
||||
const tw1 = bold.widthOfTextAtSize(titulo1, 9);
|
||||
const tw2 = bold.widthOfTextAtSize(titulo2, 9);
|
||||
page.drawText(titulo1, { x: (A4W - tw1) / 2, y: top + 4, size: 9, font: bold, color: NEGRO });
|
||||
page.drawText(titulo2, { x: (A4W - tw2) / 2, y: top - 6, size: 9, font: bold, color: NEGRO });
|
||||
|
||||
// Logo placeholder derecho
|
||||
page.drawCircle({ x: A4W - ML - 12, y: top, size: 11, color: PDFLib.rgb(0.2, 0.4, 0.7) });
|
||||
page.drawText("H", { x: A4W - ML - 15, y: top - 4, size: 7, font: bold, color: BLANCO });
|
||||
|
||||
// Número de página
|
||||
const pTxt = `${pageNum}`;
|
||||
page.drawText(pTxt, { x: (A4W - regular.widthOfTextAtSize(pTxt, 8)) / 2,
|
||||
y: 15, size: 8, font: regular, color: NEGRO });
|
||||
|
||||
// Línea separadora
|
||||
hLine(page, ML, A4H - 30, INNER);
|
||||
|
||||
return A4H - 38; // Y de inicio del contenido
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PÁGINA 1 – Datos filiatorios / Motivo / Enfermedad actual
|
||||
============================================================ */
|
||||
async function buildPage1(pdfDoc, bold, regular) {
|
||||
const page = pdfDoc.addPage([A4W, A4H]);
|
||||
let y = drawPageHeader(page, 1, bold, regular);
|
||||
|
||||
/* --- DATOS FILIATORIOS --- */
|
||||
y = sectionHeader(page, "DATOS FILIATORIOS", ML, y, INNER, 14, { bold, regular });
|
||||
|
||||
// Fila 1: fecha ingreso hospital | fecha ingreso clínica médica | habitación
|
||||
const colW = [INNER * 0.30, INNER * 0.42, INNER * 0.28];
|
||||
const rowH = 12;
|
||||
let cx = ML;
|
||||
["FECHA DE INGRESO AL HOSPITAL (DD/MM/AA)", "FECHA DE INGRESO A CLÍNICA MÉDICA (DD/MM/AA)", "HABITACIÓN"].forEach((lbl, i) => {
|
||||
rect(page, cx, y - rowH, colW[i], rowH);
|
||||
page.drawText(lbl, { x: cx + 2, y: y - rowH + 3.5, size: 5.5, font: regular, color: NEGRO });
|
||||
cx += colW[i];
|
||||
});
|
||||
// Fecha clínica médica pre-cargada (16/04/2026)
|
||||
const fechaTxt = "16/04/2026";
|
||||
const mid = ML + colW[0];
|
||||
page.drawText(fechaTxt, { x: mid + (colW[1] - bold.widthOfTextAtSize(fechaTxt, 9)) / 2,
|
||||
y: y - rowH + 3, size: 9, font: bold, color: NEGRO });
|
||||
y -= rowH;
|
||||
|
||||
// Fila 2: Apellido y nombre | DNI/CI
|
||||
const mitad = INNER / 2;
|
||||
labelCell(page, "APELLIDO y NOMBRE", ML, y - rowH, 45, mitad, rowH, { regular, bold });
|
||||
labelCell(page, "DNI / C.I", ML + mitad, y - rowH, 30, mitad, rowH, { regular, bold });
|
||||
y -= rowH;
|
||||
|
||||
// Fila 3
|
||||
labelCell(page, "FECHA DE NACIMIENTO", ML, y - rowH, 50, mitad, rowH, { regular, bold });
|
||||
labelCell(page, "HISTORIA CLINICA", ML + mitad, y - rowH, 42, mitad, rowH, { regular, bold });
|
||||
y -= rowH;
|
||||
|
||||
// Fila 4
|
||||
labelCell(page, "EDAD", ML, y - rowH, 22, mitad, rowH, { regular, bold });
|
||||
labelCell(page, "OBRA SOCIAL", ML + mitad, y - rowH, 42, mitad, rowH, { regular, bold });
|
||||
y -= rowH;
|
||||
|
||||
// Fila 5
|
||||
labelCell(page, "NACIONALIDAD", ML, y - rowH, 42, mitad, rowH, { regular, bold });
|
||||
// DERIVADO DE: con valor "Guardia Externa"
|
||||
labelCell(page, "DERIVADO DE:", ML + mitad, y - rowH, 42, mitad, rowH, { regular, bold });
|
||||
page.drawText("Guardia Externa", { x: ML + mitad + 44, y: y - rowH + 3,
|
||||
size: 7, font: regular, color: NEGRO });
|
||||
y -= rowH;
|
||||
|
||||
// Fila 6
|
||||
labelCell(page, "DIRECCIÓN", ML, y - rowH, 30, mitad, rowH, { regular, bold });
|
||||
labelCell(page, "APACHE / MORTALIDAD", ML + mitad, y - rowH, 55, mitad, rowH, { regular, bold });
|
||||
y -= rowH;
|
||||
|
||||
// Fila 7: Teléfono de contacto (ancho completo)
|
||||
labelCell(page, "Teléfono de contacto:", ML, y - rowH, 60, INNER, rowH, { regular, bold });
|
||||
y -= rowH + 4;
|
||||
|
||||
/* --- MOTIVO DE CONSULTA --- */
|
||||
y = sectionHeader(page, "MOTIVO DE CONSULTA", ML, y, INNER, 13, { bold, regular });
|
||||
const motivoH = 24;
|
||||
rect(page, ML, y - motivoH, INNER, motivoH);
|
||||
y -= motivoH + 4;
|
||||
|
||||
/* --- ENFERMEDAD ACTUAL --- */
|
||||
y = sectionHeader(page, "ENFERMEDAD ACTUAL", ML, y, INNER, 13, { bold, regular });
|
||||
const eaH = 220;
|
||||
rect(page, ML, y - eaH, INNER, eaH);
|
||||
y -= eaH + 4;
|
||||
|
||||
/* --- ANTECEDENTES DE ENFERMEDAD ACTUAL --- */
|
||||
y = sectionHeader(page, "ANTECEDENTES DE ENFERMEDAD ACTUAL", ML, y, INNER, 13, { bold, regular });
|
||||
const antH = Math.max(y - 30, 40); // lo que quede en la página
|
||||
rect(page, ML, y - antH, INNER, antH);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PÁGINA 2 – Antecedentes personales / Medicación / Hábitos
|
||||
============================================================ */
|
||||
async function buildPage2(pdfDoc, bold, regular) {
|
||||
const page = pdfDoc.addPage([A4W, A4H]);
|
||||
let y = drawPageHeader(page, 2, bold, regular);
|
||||
|
||||
/* --- ANTECEDENTES PERSONALES --- */
|
||||
y = sectionHeader(page, "ANTECEDENTES PERSONALES", ML, y, INNER, 13, { bold, regular });
|
||||
|
||||
const apSections = [
|
||||
{ label: "CLÍNICOS", h: 38 },
|
||||
{ label: "QUIRÚRGICOS /\nTRAUMATOLÓGICOS", h: 38 },
|
||||
{ label: "GINECO-OBSTÉTRICOS", h: 38 },
|
||||
{ label: "ALÉRGICOS /\nTRANSFUSIONALES", h: 38 },
|
||||
];
|
||||
const labelW = 80;
|
||||
apSections.forEach(sec => {
|
||||
rect(page, ML, y - sec.h, labelW, sec.h, { fill: GRIS_LABEL });
|
||||
rect(page, ML + labelW, y - sec.h, INNER - labelW, sec.h);
|
||||
// texto label multilinea centrado verticalmente
|
||||
sec.label.split("\n").forEach((ln, i) => {
|
||||
page.drawText(ln, { x: ML + 2, y: y - sec.h / 2 + (sec.label.includes("\n") ? (i === 0 ? 5 : -4) : -2),
|
||||
size: 7, font: regular, color: NEGRO });
|
||||
});
|
||||
y -= sec.h;
|
||||
});
|
||||
y -= 4;
|
||||
|
||||
/* --- MEDICACIÓN HABITUAL --- */
|
||||
y = sectionHeader(page, "MEDICACIÓN HABITUAL", ML, y, INNER, 13, { bold, regular });
|
||||
const medH = 9;
|
||||
const medColW = INNER / 2;
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const col = i <= 10 ? 0 : 1;
|
||||
const row = i <= 10 ? i - 1 : i - 11;
|
||||
const cx2 = ML + col * medColW;
|
||||
const cy = y - row * medH - medH;
|
||||
// número
|
||||
rect(page, cx2, cy, 10, medH);
|
||||
page.drawText(`${i}`, { x: cx2 + 2, y: cy + 2, size: 6.5, font: regular, color: NEGRO });
|
||||
// área
|
||||
rect(page, cx2 + 10, cy, medColW - 10, medH);
|
||||
}
|
||||
y -= 10 * medH + 4;
|
||||
|
||||
/* --- HÁBITOS + ANTECEDENTES HEREDOFAMILIARES (lado a lado) --- */
|
||||
const halfW = INNER / 2 - 2;
|
||||
const habX = ML;
|
||||
const aheX = ML + halfW + 4;
|
||||
|
||||
// Hábitos
|
||||
y = sectionHeader(page, "HÁBITOS", habX, y, halfW, 13, { bold, regular });
|
||||
// tabla de hábitos
|
||||
const habRows = [
|
||||
["Apetito", "Conservado", "Sí"],
|
||||
["Sueño", "Conservado", "Sí"],
|
||||
["Catarsis", "Conservado", "Sí"],
|
||||
["Diuresis", "Conservado", "Sí"],
|
||||
];
|
||||
let hy = y;
|
||||
const hRowH = 9;
|
||||
habRows.forEach(([item, val, check]) => {
|
||||
const widths = [35, 40, 12, halfW - 87];
|
||||
let hx = habX;
|
||||
[item, val, check, "Aclare:"].forEach((txt, i) => {
|
||||
rect(page, hx, hy - hRowH, widths[i], hRowH);
|
||||
page.drawText(txt, { x: hx + 2, y: hy - hRowH + 2.5, size: 6, font: regular, color: NEGRO });
|
||||
hx += widths[i];
|
||||
});
|
||||
hy -= hRowH;
|
||||
});
|
||||
hy -= 2;
|
||||
// Tabaquismo / Alcoholismo / Drogas
|
||||
const addictionRows = [
|
||||
{ label: "Tabaquismo", check: "Sí", unit: "paq/y" },
|
||||
{ label: "Alcoholismo", check: "No", unit: "gr/d" },
|
||||
{ label: "Drogas\nilícitas", check: "No", unit: "Cuales" },
|
||||
];
|
||||
addictionRows.forEach(row => {
|
||||
const aw = [40, 14, halfW - 54];
|
||||
let ax = habX;
|
||||
[row.label, row.check, row.unit + " Desde/Hasta"].forEach((txt, i) => {
|
||||
rect(page, ax, hy - hRowH * 1.5, aw[i], hRowH * 1.5);
|
||||
page.drawText(txt.split("\n")[0], { x: ax + 2, y: hy - hRowH * 1.5 + 6, size: 6, font: regular, color: NEGRO });
|
||||
if (txt.includes("\n")) page.drawText(txt.split("\n")[1], { x: ax + 2, y: hy - hRowH * 1.5 + 1, size: 6, font: regular, color: NEGRO });
|
||||
ax += aw[i];
|
||||
});
|
||||
hy -= hRowH * 1.5;
|
||||
});
|
||||
|
||||
// Antecedentes heredofamiliares (misma Y de inicio)
|
||||
let aheY = y + 13; // misma línea que "HÁBITOS" header
|
||||
aheY = sectionHeader(page, "ANTECEDENTES HEREDOFAMILIARES", aheX, aheY, halfW, 13, { bold, regular });
|
||||
|
||||
const aheRows = [
|
||||
{ label: "Madre", extra: ["Vive", "No", "Años:"] },
|
||||
{ label: "Patología:" },
|
||||
{ label: "Padre", extra: ["Vive", "No", "Años:"] },
|
||||
{ label: "Patología:" },
|
||||
{ label: "Hermanos", extra: ["N.º", "", "Años:"] },
|
||||
{ label: "Patología:" },
|
||||
{ label: "Hijos", extra: ["N.º", "", "Años:"] },
|
||||
{ label: "Patología:" },
|
||||
{ label: "Comentarios" },
|
||||
];
|
||||
const aRowH = 10;
|
||||
aheRows.forEach(row => {
|
||||
rect(page, aheX, aheY - aRowH, halfW, aRowH);
|
||||
page.drawText(row.label, { x: aheX + 2, y: aheY - aRowH + 2.5, size: 6.5, font: regular, color: NEGRO });
|
||||
if (row.extra) {
|
||||
const ex = row.extra;
|
||||
const ew = [(halfW - 50) / 3, (halfW - 50) / 3, (halfW - 50) / 3];
|
||||
let ex2 = aheX + 50;
|
||||
ex.forEach((txt, i) => {
|
||||
rect(page, ex2, aheY - aRowH, ew[i], aRowH);
|
||||
page.drawText(txt, { x: ex2 + 2, y: aheY - aRowH + 2.5, size: 6, font: regular, color: NEGRO });
|
||||
ex2 += ew[i];
|
||||
});
|
||||
}
|
||||
aheY -= aRowH;
|
||||
});
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PÁGINA 3 – Contactos / Vacunas / Determinantes / Examen físico
|
||||
============================================================ */
|
||||
async function buildPage3(pdfDoc, bold, regular) {
|
||||
const page = pdfDoc.addPage([A4W, A4H]);
|
||||
let y = drawPageHeader(page, 3, bold, regular);
|
||||
|
||||
const halfW = INNER / 2 - 2;
|
||||
/* --- CONTACTOS + VACUNACIÓN (lado a lado) --- */
|
||||
// Headers a la misma Y
|
||||
const yStart = y;
|
||||
sectionHeader(page, "CONTACTOS", ML, yStart, halfW, 13, { bold, regular });
|
||||
sectionHeader(page, "VACUNACIÓN/SEROLOGÍAS", ML + halfW + 4, yStart, halfW, 13, { bold, regular });
|
||||
y -= 13;
|
||||
|
||||
// Contactos
|
||||
const contactos = [
|
||||
{ label: "Animales", val: "No", extra: "Cuáles?" },
|
||||
{ label: "TBC", val: "No", extra: "Cuándo?" },
|
||||
{ label: "Chagas", val: "No", extra: "Cómo?" },
|
||||
{ label: "HIV", val: "No", extra: "Conducta\nriesgo?" },
|
||||
{ label: "Otros", val: "No", extra: "Cuál/es?" },
|
||||
];
|
||||
let cy2 = y;
|
||||
const cRowH = 16;
|
||||
contactos.forEach(c => {
|
||||
const cw = [40, 18, halfW - 58];
|
||||
let cx2 = ML;
|
||||
[c.label, c.val, c.extra].forEach((txt, i) => {
|
||||
rect(page, cx2, cy2 - cRowH, cw[i], cRowH);
|
||||
txt.split("\n").forEach((ln, li) => {
|
||||
page.drawText(ln, { x: cx2 + 2, y: cy2 - cRowH + (cRowH / 2) + 1 - li * 7, size: 6.5, font: regular, color: NEGRO });
|
||||
});
|
||||
cx2 += cw[i];
|
||||
});
|
||||
cy2 -= cRowH;
|
||||
});
|
||||
|
||||
// Vacunación
|
||||
const vacX = ML + halfW + 4;
|
||||
let vy = y;
|
||||
// cabecera sub
|
||||
const vacHeaders = ["Dosis", "Ultima", "Nombre comercial"];
|
||||
const vacW = [halfW * 0.22, halfW * 0.22, halfW * 0.56];
|
||||
// fila "Vacuna COVID" + sub-encabezados
|
||||
const vacRowH = 10;
|
||||
rect(page, vacX, vy - vacRowH, halfW * 0.3, vacRowH, { fill: GRIS_LABEL });
|
||||
page.drawText("Vacuna COVID", { x: vacX + 2, y: vy - vacRowH + 2.5, size: 6.5, font: regular, color: NEGRO });
|
||||
let vx = vacX + halfW * 0.3;
|
||||
vacHeaders.forEach((h, i) => {
|
||||
rect(page, vx, vy - vacRowH, vacW[i], vacRowH, { fill: GRIS_LABEL });
|
||||
page.drawText(h, { x: vx + 2, y: vy - vacRowH + 2.5, size: 6, font: regular, color: NEGRO });
|
||||
vx += vacW[i];
|
||||
});
|
||||
vy -= vacRowH;
|
||||
// valor dosis 3
|
||||
rect(page, vacX, vy - vacRowH, halfW * 0.3, vacRowH, { fill: GRIS_LABEL });
|
||||
rect(page, vacX + halfW * 0.3, vy - vacRowH, vacW[0], vacRowH);
|
||||
page.drawText("3", { x: vacX + halfW * 0.3 + 4, y: vy - vacRowH + 2.5, size: 7, font: bold, color: NEGRO });
|
||||
rect(page, vacX + halfW * 0.3 + vacW[0], vy - vacRowH, vacW[1] + vacW[2], vacRowH);
|
||||
vy -= vacRowH;
|
||||
|
||||
const vacRows = [
|
||||
{ label: "Antigripal", check: "No" },
|
||||
{ label: "Antineumocócica", check: "No" },
|
||||
{ label: "Doble Adultos", check: "No" },
|
||||
{ label: "Otras", check: "" },
|
||||
];
|
||||
const vacRH = 10;
|
||||
vacRows.forEach(row => {
|
||||
rect(page, vacX, vy - vacRH, halfW * 0.3, vacRH, { fill: GRIS_LABEL });
|
||||
page.drawText(row.label, { x: vacX + 2, y: vy - vacRH + 2.5, size: 6.5, font: regular, color: NEGRO });
|
||||
rect(page, vacX + halfW * 0.3, vy - vacRH, vacW[0], vacRH);
|
||||
page.drawText(row.check, { x: vacX + halfW * 0.3 + 2, y: vy - vacRH + 2.5, size: 7, font: regular, color: NEGRO });
|
||||
rect(page, vacX + halfW * 0.3 + vacW[0], vy - vacRH, vacW[1] + vacW[2], vacRH);
|
||||
vy -= vacRH;
|
||||
});
|
||||
// Serologías
|
||||
rect(page, vacX, vy - vacRH, halfW * 0.3, vacRH, { fill: GRIS_LABEL });
|
||||
page.drawText("Serologías", { x: vacX + 2, y: vy - vacRH + 2.5, size: 6.5, font: regular, color: NEGRO });
|
||||
rect(page, vacX + halfW * 0.3, vy - vacRH, halfW * 0.7, vacRH);
|
||||
const resultTxt = "Resultado";
|
||||
page.drawText(resultTxt, { x: vacX + halfW * 0.3 + (halfW * 0.7 - regular.widthOfTextAtSize(resultTxt, 7)) / 2, y: vy - vacRH + 2.5, size: 7, font: regular, color: NEGRO });
|
||||
vy -= vacRH;
|
||||
// 2 filas vacías de serologías
|
||||
for (let i = 0; i < 2; i++) {
|
||||
rect(page, vacX, vy - vacRH, halfW, vacRH);
|
||||
vy -= vacRH;
|
||||
}
|
||||
|
||||
y = Math.min(cy2, vy) - 4;
|
||||
|
||||
/* --- DETERMINANTES SOCIO-EPIDEMIOLÓGICOS --- */
|
||||
y = sectionHeader(page, "DETERMINANTES SOCIO-EPIDEMIOLÓGICOS", ML, y, INNER, 13, { bold, regular });
|
||||
|
||||
const detRows = [
|
||||
{ label: "LUGAR DE NACIMIENTO" },
|
||||
{ label: "RESIDENCIA PASADA Y ACTUAL" },
|
||||
{ label: "VIVIENDA", special: "vivienda" },
|
||||
{ label: "NIVEL DE ESCOLARIDAD ALCANZADO" },
|
||||
{ label: "OCUPACIÓN/OFICIO/PROFESIÓN" },
|
||||
{ label: "VIAJES RECIENTES" },
|
||||
];
|
||||
const detH = 12;
|
||||
const detLW = 90;
|
||||
detRows.forEach(row => {
|
||||
if (row.special === "vivienda") {
|
||||
// dos sub-filas: Material/Sanitarios + Agua/Cloacas
|
||||
rect(page, ML, y - detH, detLW, detH * 2);
|
||||
page.drawText(row.label, { x: ML + 2, y: y - detH + 2.5, size: 7, font: regular, color: NEGRO });
|
||||
// sub-fila 1
|
||||
const subW = (INNER - detLW) / 4;
|
||||
const subLabels1 = ["Material", "No", "Sanitarios completos", "No"];
|
||||
let sx = ML + detLW;
|
||||
subLabels1.forEach((lbl, i) => {
|
||||
const fill = (i % 2 === 0) ? GRIS_LABEL : null;
|
||||
rect(page, sx, y - detH, subW, detH, { fill: fill || BLANCO });
|
||||
page.drawText(lbl, { x: sx + 2, y: y - detH + 3, size: 6.5, font: regular, color: NEGRO });
|
||||
sx += subW;
|
||||
});
|
||||
y -= detH;
|
||||
// sub-fila 2
|
||||
const subLabels2 = ["Agua Potable", "No", "Cloacas", "No"];
|
||||
sx = ML + detLW;
|
||||
subLabels2.forEach((lbl, i) => {
|
||||
const fill = (i % 2 === 0) ? GRIS_LABEL : null;
|
||||
rect(page, sx, y - detH, subW, detH, { fill: fill || BLANCO });
|
||||
page.drawText(lbl, { x: sx + 2, y: y - detH + 3, size: 6.5, font: regular, color: NEGRO });
|
||||
sx += subW;
|
||||
});
|
||||
y -= detH;
|
||||
} else {
|
||||
rect(page, ML, y - detH, detLW, detH, { fill: GRIS_LABEL });
|
||||
page.drawText(row.label, { x: ML + 2, y: y - detH + 3, size: 7, font: regular, color: NEGRO });
|
||||
rect(page, ML + detLW, y - detH, INNER - detLW, detH);
|
||||
y -= detH;
|
||||
}
|
||||
});
|
||||
y -= 4;
|
||||
|
||||
/* --- EXAMEN FÍSICO --- */
|
||||
y = sectionHeader(page, "EXAMEN FÍSICO", ML, y, INNER, 13, { bold, regular });
|
||||
|
||||
// Fila 1: TA / FC / FR / Temp / Sat / FiO2
|
||||
const efRow1 = [
|
||||
{ label: "TENSIÓN ARTERIAL", w: INNER * 0.18 },
|
||||
{ label: "FRECUENCIA\nCARDÍACA", w: INNER * 0.14 },
|
||||
{ label: "FRECUENCIA\nRESPIRATORIA", w: INNER * 0.14 },
|
||||
{ label: "TEMPERATURA\nAXILAR", w: INNER * 0.14 },
|
||||
{ label: "SATUROMETRÍA", w: INNER * 0.14 },
|
||||
{ label: "FIO2(FRACCIÓN\nINSPIRADA DE OXIGENO)", w: INNER * 0.26 },
|
||||
];
|
||||
const efH1 = 22;
|
||||
const efH1b = 12;
|
||||
let ex = ML;
|
||||
efRow1.forEach(col => {
|
||||
rect(page, ex, y - efH1, col.w, efH1 / 2, { fill: GRIS_LABEL });
|
||||
col.label.split("\n").forEach((ln, i) => {
|
||||
page.drawText(ln, { x: ex + 2, y: y - efH1 / 2 + (col.label.includes("\n") ? (i === 0 ? 3 : -4) : -1),
|
||||
size: 5.5, font: regular, color: NEGRO });
|
||||
});
|
||||
rect(page, ex, y - efH1, col.w, efH1);
|
||||
ex += col.w;
|
||||
});
|
||||
y -= efH1 + 2;
|
||||
|
||||
// Fila 2: SNG / SV / AVP / AVC / Peso-Talla-IMC
|
||||
const efRow2 = [
|
||||
{ label: "SONDA\nNASOGÁSTRICA", val: "No", w: INNER * 0.14 },
|
||||
{ label: "SONDA\nVESICAL", val: "Sonda Vesical",w: INNER * 0.16 },
|
||||
{ label: "ACCESO VENOSO\nPERIFÉRICO", val: "Si - MSI", w: INNER * 0.16 },
|
||||
{ label: "ACCESO VENOSO CENTRAL", val: "No", w: INNER * 0.27 },
|
||||
{ label: "PESO / TALLA / IMC", val: "", w: INNER * 0.27 },
|
||||
];
|
||||
const ef2H = 18;
|
||||
ex = ML;
|
||||
efRow2.forEach(col => {
|
||||
rect(page, ex, y - ef2H, col.w, ef2H / 2, { fill: GRIS_LABEL });
|
||||
col.label.split("\n").forEach((ln, li) => {
|
||||
page.drawText(ln, { x: ex + 2, y: y - ef2H / 2 + (col.label.includes("\n") ? (li === 0 ? 3 : -4) : -1),
|
||||
size: 5.5, font: regular, color: NEGRO });
|
||||
});
|
||||
rect(page, ex, y - ef2H, col.w, ef2H);
|
||||
page.drawText(col.val, { x: ex + 2, y: y - ef2H + 3, size: 6.5, font: regular, color: NEGRO });
|
||||
ex += col.w;
|
||||
});
|
||||
y -= ef2H + 2;
|
||||
|
||||
// Sistemas
|
||||
const sistemas = [
|
||||
"SISTEMA\nNERVIOSO\nCENTRAL",
|
||||
"CARDIOVASCULAR",
|
||||
"RESPIRATORIO",
|
||||
"DIGESTIVO",
|
||||
"GENITO/URINARIO",
|
||||
"SISTEMA OSTEO –\nMUSCULO -\nARTICULAR",
|
||||
];
|
||||
const sysLW = 60;
|
||||
const sysH = 28;
|
||||
sistemas.forEach(sis => {
|
||||
rect(page, ML, y - sysH, sysLW, sysH, { fill: GRIS_LABEL });
|
||||
sis.split("\n").forEach((ln, i) => {
|
||||
const lines = sis.split("\n").length;
|
||||
const baseY = y - sysH + sysH / 2 - 3;
|
||||
page.drawText(ln, { x: ML + 2, y: baseY + (lines - 1 - i) * 7, size: 6.5, font: regular, color: NEGRO });
|
||||
});
|
||||
rect(page, ML + sysLW, y - sysH, INNER - sysLW, sysH);
|
||||
y -= sysH;
|
||||
});
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PÁGINA 4 – Laboratorio / Bacteriología / ECG / Estudios
|
||||
============================================================ */
|
||||
async function buildPage4(pdfDoc, bold, regular) {
|
||||
const page = pdfDoc.addPage([A4W, A4H]);
|
||||
let y = drawPageHeader(page, 4, bold, regular);
|
||||
|
||||
/* --- LABORATORIO --- */
|
||||
y = sectionHeader(page, "LABORATORIO", ML, y, INNER, 13, { bold, regular });
|
||||
|
||||
// Parámetros en columnas de 3 grupos (label + espacio)
|
||||
const labParams = [
|
||||
["HTO (%)", "GLUC (mg/dl)", "Na+ (mEq/L)", "BT (mg/dl)", "TP (%)"],
|
||||
["HB (G/DL)", "UREA (mg/dl)", "K+ (mEq/L)", "BD (mg/dl)", "KPTT (seg)"],
|
||||
["GB (X10⁹/L)", "CREAT (mg/dl)", "Cl- (mEq/L)", "GOT (UI/L)", "RIN"],
|
||||
["PQ (X10⁹/L)", "", "P- (mEq/L)", "GPT (UI/L)", "PROT (g/dL)"],
|
||||
["", "", "Mg+ (mEq/L)", "FAL (UI/L)", "ALB (g/dL)"],
|
||||
];
|
||||
const labCols = 5;
|
||||
const labColW = INNER / labCols;
|
||||
const labRowH = 11;
|
||||
const labLabelW = labColW * 0.55;
|
||||
|
||||
labParams.forEach((row, ri) => {
|
||||
let lx = ML;
|
||||
row.forEach((param, ci) => {
|
||||
if (param) {
|
||||
rect(page, lx, y - labRowH, labLabelW, labRowH, { fill: GRIS_LABEL });
|
||||
page.drawText(param, { x: lx + 2, y: y - labRowH + 3, size: 5.5, font: regular, color: NEGRO });
|
||||
rect(page, lx + labLabelW, y - labRowH, labColW - labLabelW, labRowH);
|
||||
}
|
||||
lx += labColW;
|
||||
});
|
||||
y -= labRowH;
|
||||
});
|
||||
|
||||
// EAB
|
||||
y -= 2;
|
||||
const eabLabels = ["EAB", "PH", "CO2", "O2", "HCO3", "EB", "SAT", "A.L", "FIO2"];
|
||||
const eabW = [28, 30, 30, 30, 35, 30, 30, 30, 30];
|
||||
const eabH = 11;
|
||||
let eabX = ML;
|
||||
eabLabels.forEach((lbl, i) => {
|
||||
rect(page, eabX, y - eabH, eabW[i], eabH / 2, { fill: GRIS_LABEL });
|
||||
page.drawText(lbl, { x: eabX + 2, y: y - eabH / 2 + 1, size: 6, font: bold, color: NEGRO });
|
||||
rect(page, eabX, y - eabH, eabW[i], eabH);
|
||||
eabX += eabW[i];
|
||||
});
|
||||
y -= eabH + 2;
|
||||
|
||||
// OTROS / FENA
|
||||
rect(page, ML, y, INNER, 13, { fill: GRIS_LABEL });
|
||||
page.drawText("OTROS (INCLUYE LÍQUIDOS)", { x: ML + 2, y: y + 4, size: 7, font: bold, color: NEGRO });
|
||||
y -= 0;
|
||||
rect(page, ML, y - 36, INNER, 36);
|
||||
page.drawText("FENA", { x: ML + 2, y: y - 8, size: 7, font: regular, color: NEGRO });
|
||||
y -= 36 + 13 + 4;
|
||||
|
||||
/* --- BACTERIOLOGÍA --- */
|
||||
y = sectionHeader(page, "BACTERIOLOGÍA", ML, y, INNER, 13, { bold, regular });
|
||||
const bactHeaders = ["Fecha", "Cultivo", "Resultado", "Sensibilidad"];
|
||||
const bactW = [INNER * 0.18, INNER * 0.27, INNER * 0.27, INNER * 0.28];
|
||||
const bactH = 10;
|
||||
let bx = ML;
|
||||
bactHeaders.forEach((h, i) => {
|
||||
rect(page, bx, y - bactH, bactW[i], bactH, { fill: GRIS_LABEL });
|
||||
textCenter(page, h, bx, y - bactH + 2, bactW[i], { font: bold, size: 7 });
|
||||
bx += bactW[i];
|
||||
});
|
||||
y -= bactH;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
bx = ML;
|
||||
bactHeaders.forEach((_, j) => { rect(page, bx, y - bactH, bactW[j], bactH); bx += bactW[j]; });
|
||||
y -= bactH;
|
||||
}
|
||||
y -= 4;
|
||||
|
||||
/* --- ECG --- */
|
||||
y = sectionHeader(page, "ELECTROCARDIOGRAMA", ML, y, INNER, 13, { bold, regular });
|
||||
|
||||
// Fila con fecha + interpretación
|
||||
const ecgH = 10;
|
||||
const ecgLW = 28;
|
||||
const ecgRW = INNER / 2;
|
||||
const ecgParams = [
|
||||
["FECHA", "", ecgRW],
|
||||
["Ritmo", "PR"],
|
||||
["FC", "Onda T"],
|
||||
["Eje", "QT"],
|
||||
["Onda P",""],
|
||||
["QRS", ""],
|
||||
];
|
||||
// Primera fila: fecha + "INTERPRETACIÓN DIAGNÓSTICA:" (columna derecha grande)
|
||||
rect(page, ML, y - ecgH, ecgLW, ecgH, { fill: GRIS_LABEL });
|
||||
page.drawText("FECHA", { x: ML + 2, y: y - ecgH + 3, size: 7, font: bold, color: NEGRO });
|
||||
rect(page, ML + ecgLW, y - ecgH, ecgRW - ecgLW, ecgH);
|
||||
rect(page, ML + ecgRW, y - ecgH, INNER - ecgRW, ecgH, { fill: GRIS_LABEL });
|
||||
page.drawText("INTERPRETACIÓN DIAGNÓSTICA:", { x: ML + ecgRW + 2, y: y - ecgH + 3, size: 7, font: bold, color: NEGRO });
|
||||
y -= ecgH;
|
||||
|
||||
const ecgLeft = [["Ritmo"],["FC"],["Eje"],["Onda P"],["QRS"]];
|
||||
const ecgRight = [["PR"],["Onda T"],["QT"],[""],[""]];
|
||||
const ecgDataH = 10;
|
||||
const lhW = INNER / 4;
|
||||
// zona interpretación (rectángulo derecho que abarca todas las filas)
|
||||
rect(page, ML + ecgRW, y - ecgDataH * 5, INNER - ecgRW, ecgDataH * 5);
|
||||
ecgLeft.forEach((row, i) => {
|
||||
rect(page, ML, y - ecgDataH, lhW * 0.5, ecgDataH, { fill: GRIS_LABEL });
|
||||
page.drawText(row[0], { x: ML + 2, y: y - ecgDataH + 3, size: 7, font: regular, color: NEGRO });
|
||||
rect(page, ML + lhW * 0.5, y - ecgDataH, lhW - lhW * 0.5, ecgDataH);
|
||||
if (ecgRight[i][0]) {
|
||||
rect(page, ML + lhW, y - ecgDataH, lhW * 0.5, ecgDataH, { fill: GRIS_LABEL });
|
||||
page.drawText(ecgRight[i][0], { x: ML + lhW + 2, y: y - ecgDataH + 3, size: 7, font: regular, color: NEGRO });
|
||||
rect(page, ML + lhW * 1.5, y - ecgDataH, lhW - lhW * 0.5, ecgDataH);
|
||||
}
|
||||
y -= ecgDataH;
|
||||
});
|
||||
y -= 4;
|
||||
|
||||
/* --- ESTUDIOS COMPLEMENTARIOS --- */
|
||||
y = sectionHeader(page, "ESTUDIOS COMPLEMENTARIOS", ML, y, INNER, 13, { bold, regular });
|
||||
const estHeaders = ["Fecha", "Estudio", "Descripción"];
|
||||
const estW = [INNER * 0.14, INNER * 0.16, INNER * 0.70];
|
||||
let estX = ML;
|
||||
estHeaders.forEach((h, i) => {
|
||||
rect(page, estX, y - ecgH, estW[i], ecgH, { fill: GRIS_LABEL });
|
||||
page.drawText(h, { x: estX + 2, y: y - ecgH + 3, size: 7, font: bold, color: NEGRO });
|
||||
estX += estW[i];
|
||||
});
|
||||
y -= ecgH;
|
||||
const estRowH = 22;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
estX = ML;
|
||||
estHeaders.forEach((_, j) => { rect(page, estX, y - estRowH, estW[j], estRowH); estX += estW[j]; });
|
||||
// Fecha marcadas: /03/ y /04/
|
||||
if (i < 2) {
|
||||
page.drawText(i === 0 ? "/03/." : "/04/.", { x: ML + 2, y: y - estRowH + 8, size: 10, font: bold, color: NEGRO });
|
||||
}
|
||||
y -= estRowH;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PÁGINA 5 – Diagnóstico / Plan diagnóstico / Plan terapéutico
|
||||
============================================================ */
|
||||
async function buildPage5(pdfDoc, bold, regular) {
|
||||
const page = pdfDoc.addPage([A4W, A4H]);
|
||||
let y = drawPageHeader(page, 5, bold, regular);
|
||||
|
||||
/* --- IMPRESIÓN DIAGNÓSTICA POR SINDROME --- */
|
||||
y = sectionHeader(page, "IMPRESIÓN DIAGNÓSTICA POR SINDROME", ML, y, INNER, 13, { bold, regular });
|
||||
const idH = 200;
|
||||
rect(page, ML, y - idH, INNER, idH);
|
||||
y -= idH + 4;
|
||||
|
||||
/* --- PLAN DIAGNÓSTICO --- */
|
||||
y = sectionHeader(page, "PLAN DIAGNÓSTICO", ML, y, INNER, 13, { bold, regular });
|
||||
const pdH = 40;
|
||||
rect(page, ML, y - pdH, INNER, pdH);
|
||||
y -= pdH + 4;
|
||||
|
||||
/* --- DIAGNÓSTICO AL INGRESO --- */
|
||||
y = sectionHeader(page, "DIAGNÓSTICO AL INGRESO", ML, y, INNER, 13, { bold, regular });
|
||||
const diH = 40;
|
||||
rect(page, ML, y - diH, INNER, diH);
|
||||
y -= diH + 4;
|
||||
|
||||
/* --- PLAN TERAPÉUTICO --- */
|
||||
y = sectionHeader(page, "PLAN TERAPÈUTICO", ML, y, INNER, 13, { bold, regular });
|
||||
const ptH = 9;
|
||||
const ptColW = INNER / 2;
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const col = i <= 10 ? 0 : 1;
|
||||
const row = i <= 10 ? i - 1 : i - 11;
|
||||
const ptX = ML + col * ptColW;
|
||||
const ptY = y - row * ptH - ptH;
|
||||
rect(page, ptX, ptY, 10, ptH);
|
||||
page.drawText(`${i}`, { x: ptX + 2, y: ptY + 2, size: 6.5, font: regular, color: NEGRO });
|
||||
rect(page, ptX + 10, ptY, ptColW - 10, ptH);
|
||||
}
|
||||
|
||||
// Firma
|
||||
const firmaY = y - 10 * ptH - 20;
|
||||
page.drawText("......................................", {
|
||||
x: A4W - ML - 120, y: firmaY, size: 8, font: regular, color: NEGRO
|
||||
});
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
MAIN – ensambla todo y descarga
|
||||
============================================================ */
|
||||
async function generarPDF() {
|
||||
const status = document.getElementById("status");
|
||||
status.textContent = "Generando PDF...";
|
||||
try {
|
||||
const pdfDoc = await PDFLib.PDFDocument.create();
|
||||
const regular = await pdfDoc.embedFont(PDFLib.StandardFonts.Helvetica);
|
||||
const bold = await pdfDoc.embedFont(PDFLib.StandardFonts.HelveticaBold);
|
||||
const fonts = { regular, bold };
|
||||
|
||||
await buildPage1(pdfDoc, bold, regular);
|
||||
await buildPage2(pdfDoc, bold, regular);
|
||||
await buildPage3(pdfDoc, bold, regular);
|
||||
await buildPage4(pdfDoc, bold, regular);
|
||||
await buildPage5(pdfDoc, bold, regular);
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const blob = new Blob([pdfBytes], { type: "application/pdf" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "HC_Ingreso_Santojanni.pdf";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
status.textContent = "✅ PDF generado y descargado correctamente.";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
status.textContent = "❌ Error al generar el PDF. Ver consola para detalles.";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -157,6 +157,22 @@ try {
|
||||
// La columna ya existe
|
||||
}
|
||||
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS interconsultas (
|
||||
id TEXT PRIMARY KEY,
|
||||
pacienteId TEXT NOT NULL,
|
||||
internacionId TEXT NOT NULL,
|
||||
fecha TEXT,
|
||||
servicioInterconsultado TEXT,
|
||||
realizada INTEGER DEFAULT 0,
|
||||
respuestaInterconsulta TEXT
|
||||
)
|
||||
`);
|
||||
} catch (e) {
|
||||
console.log('Tabla interconsultas ya existe o error:', e.message);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
@@ -206,6 +222,7 @@ app.get('/api/state', (req, res) => {
|
||||
acidosBase: queryAll('SELECT * FROM acidosbase'),
|
||||
cultivos: queryAll('SELECT * FROM cultivos'),
|
||||
estudiosComplementarios: queryAll('SELECT * FROM estudiosComplementarios'),
|
||||
interconsultas: queryAll('SELECT * FROM interconsultas').map(ic => ({ ...ic, realizada: !!ic.realizada })),
|
||||
vistaActual: 'dashboard',
|
||||
currentInternacionId: null
|
||||
};
|
||||
@@ -288,6 +305,14 @@ if (state.laboratorios?.length) {
|
||||
}
|
||||
}
|
||||
|
||||
run('DELETE FROM interconsultas');
|
||||
if (state.interconsultas?.length) {
|
||||
const stmt = db.prepare('INSERT INTO interconsultas (id, pacienteId, internacionId, fecha, servicioInterconsultado, realizada, respuestaInterconsulta) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
||||
for (const ic of state.interconsultas) {
|
||||
stmt.run(ic.id, ic.pacienteId, ic.internacionId, ic.fecha, ic.servicioInterconsultado, ic.realizada ? 1 : 0, ic.respuestaInterconsulta || null);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ function AppContent() {
|
||||
acidosBase={store.getAcidosBaseByPaciente(paciente.id)}
|
||||
cultivos={store.getCultivosByPaciente(paciente.id)}
|
||||
estudiosComplementarios={store.estudiosComplementarios.filter(e => e.pacienteId === paciente.id)}
|
||||
interconsultas={store.interconsultas.filter(ic => ic.internacionId === internacion.id)}
|
||||
onAgregarEvolucion={store.agregarEvolucion}
|
||||
onActualizarEvolucion={store.actualizarEvolucion}
|
||||
onEliminarEvolucion={store.eliminarEvolucion}
|
||||
@@ -159,6 +160,9 @@ function AppContent() {
|
||||
onAgregarEstudioComplementario={(e) => store.agregarEstudioComplementario({ ...e, internacionId: internacion.id })}
|
||||
onActualizarEstudioComplementario={store.actualizarEstudioComplementario}
|
||||
onEliminarEstudioComplementario={store.eliminarEstudioComplementario}
|
||||
onAgregarInterconsulta={(ic) => store.agregarInterconsulta({ ...ic, internacionId: internacion.id })}
|
||||
onActualizarInterconsulta={store.actualizarInterconsulta}
|
||||
onEliminarInterconsulta={store.eliminarInterconsulta}
|
||||
onActualizarInternacion={store.actualizarInternacion}
|
||||
onActualizarCama={store.actualizarCama}
|
||||
onVolver={() => store.setVista('internaciones')}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
AcidoBase,
|
||||
Cultivo,
|
||||
EstudioComplementario,
|
||||
Interconsulta,
|
||||
Vista
|
||||
} from '@/types';
|
||||
|
||||
@@ -35,6 +36,7 @@ interface HospitalState {
|
||||
acidosBase: AcidoBase[];
|
||||
cultivos: Cultivo[];
|
||||
estudiosComplementarios: EstudioComplementario[];
|
||||
interconsultas: Interconsulta[];
|
||||
vistaActual: Vista;
|
||||
currentInternacionId?: string | null;
|
||||
}
|
||||
@@ -51,6 +53,7 @@ const defaultState = (): HospitalState => ({
|
||||
acidosBase: [],
|
||||
cultivos: [],
|
||||
estudiosComplementarios: [],
|
||||
interconsultas: [],
|
||||
vistaActual: 'dashboard',
|
||||
camas: []
|
||||
});
|
||||
@@ -349,6 +352,30 @@ const finalizarInternacion = useCallback((internacionId: string, datos: {
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Acciones de interconsultas
|
||||
const agregarInterconsulta = useCallback((interconsulta: Omit<Interconsulta, 'id'>) => {
|
||||
const nuevaInterconsulta: Interconsulta = { ...interconsulta, id: generateUUID() };
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
interconsultas: [...prev.interconsultas, nuevaInterconsulta],
|
||||
}));
|
||||
return nuevaInterconsulta.id;
|
||||
}, []);
|
||||
|
||||
const actualizarInterconsulta = useCallback((id: string, datos: Partial<Interconsulta>) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
interconsultas: prev.interconsultas.map(ic => ic.id === id ? { ...ic, ...datos } : ic),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const eliminarInterconsulta = useCallback((id: string) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
interconsultas: prev.interconsultas.filter(ic => ic.id !== id),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Acciones de areas
|
||||
const agregarArea = useCallback((area: Omit<Area, 'id'>) => {
|
||||
const nuevaArea: Area = { ...area, id: generateUUID() };
|
||||
@@ -472,6 +499,9 @@ const getEstadisticas = useCallback(() => {
|
||||
agregarEstudioComplementario,
|
||||
actualizarEstudioComplementario,
|
||||
eliminarEstudioComplementario,
|
||||
agregarInterconsulta,
|
||||
actualizarInterconsulta,
|
||||
eliminarInterconsulta,
|
||||
agregarArea,
|
||||
actualizarArea,
|
||||
eliminarArea,
|
||||
|
||||
@@ -50,7 +50,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import type { Laboratorio, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario } from '@/types';
|
||||
import type { Laboratorio, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario, Interconsulta } from '@/types';
|
||||
import { formatDateDDMMYYYY } from '@/lib/utils';
|
||||
|
||||
interface HistoriaClinicaProps {
|
||||
@@ -63,6 +63,7 @@ interface HistoriaClinicaProps {
|
||||
acidosBase: AcidoBase[];
|
||||
cultivos: Cultivo[];
|
||||
estudiosComplementarios: EstudioComplementario[];
|
||||
interconsultas: Interconsulta[];
|
||||
onAgregarEvolucion: (evolucion: Omit<Evolucion, 'id'>) => void;
|
||||
onActualizarEvolucion: (id: string, datos: Partial<Evolucion>) => void;
|
||||
onEliminarEvolucion: (id: string) => void;
|
||||
@@ -79,6 +80,9 @@ interface HistoriaClinicaProps {
|
||||
onAgregarEstudioComplementario: (estudio: Omit<EstudioComplementario, 'id'>) => void;
|
||||
onActualizarEstudioComplementario: (id: string, datos: Partial<EstudioComplementario>) => void;
|
||||
onEliminarEstudioComplementario: (id: string) => void;
|
||||
onAgregarInterconsulta: (interconsulta: Omit<Interconsulta, 'id'>) => void;
|
||||
onActualizarInterconsulta: (id: string, datos: Partial<Interconsulta>) => void;
|
||||
onEliminarInterconsulta: (id: string) => void;
|
||||
onActualizarInternacion: (id: string, datos: Partial<Internacion>) => void;
|
||||
onActualizarCama: (id: string, datos: Partial<Cama>) => void;
|
||||
onVolver: () => void;
|
||||
@@ -168,6 +172,7 @@ export function HistoriaClinica({
|
||||
acidosBase,
|
||||
cultivos,
|
||||
estudiosComplementarios,
|
||||
interconsultas,
|
||||
onAgregarEvolucion,
|
||||
onActualizarEvolucion,
|
||||
onEliminarEvolucion,
|
||||
@@ -183,6 +188,9 @@ export function HistoriaClinica({
|
||||
onAgregarEstudioComplementario,
|
||||
onActualizarEstudioComplementario,
|
||||
onEliminarEstudioComplementario,
|
||||
onAgregarInterconsulta,
|
||||
onActualizarInterconsulta,
|
||||
onEliminarInterconsulta,
|
||||
onActualizarInternacion,
|
||||
onActualizarCama,
|
||||
onVolver,
|
||||
@@ -538,10 +546,14 @@ export function HistoriaClinica({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="interconsultas" className="mt-4">
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<Users className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>Sección Interconsultas en desarrollo</p>
|
||||
</div>
|
||||
<SeccionInterconsultas
|
||||
interconsultas={interconsultas as Interconsulta[]}
|
||||
pacienteId={paciente.id}
|
||||
internacionId={internacion.id}
|
||||
add={onAgregarInterconsulta}
|
||||
update={onActualizarInterconsulta}
|
||||
del={onEliminarInterconsulta}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -2006,4 +2018,148 @@ function SeccionEstudiosComplementarios({ estudios, internacionId, pacienteId, a
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SeccionInterconsultas({ interconsultas, pacienteId, internacionId, add, update, del }: {
|
||||
interconsultas: Interconsulta[];
|
||||
pacienteId: string;
|
||||
internacionId: string;
|
||||
add: (ic: Omit<Interconsulta, 'id'>) => void;
|
||||
update: (id: string, datos: Partial<Interconsulta>) => void;
|
||||
del: (id: string) => void;
|
||||
}) {
|
||||
const [dialog, setDialog] = useState(false);
|
||||
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [servicioInterconsultado, setServicioInterconsultado] = useState('');
|
||||
const [realizada, setRealizada] = useState(false);
|
||||
const [respuestaInterconsulta, setRespuestaInterconsulta] = useState('');
|
||||
const [edit, setEdit] = useState<Interconsulta | null>(null);
|
||||
|
||||
const reset = () => {
|
||||
setFecha(new Date().toISOString().split('T')[0]);
|
||||
setServicioInterconsultado('');
|
||||
setRealizada(false);
|
||||
setRespuestaInterconsulta('');
|
||||
setEdit(null);
|
||||
};
|
||||
|
||||
const handleGuardar = () => {
|
||||
if (!servicioInterconsultado) return;
|
||||
if (edit) {
|
||||
update(edit.id, { fecha, servicioInterconsultado, realizada, respuestaInterconsulta });
|
||||
} else {
|
||||
add({ pacienteId, internacionId, fecha, servicioInterconsultado, realizada, respuestaInterconsulta });
|
||||
}
|
||||
setDialog(false);
|
||||
reset();
|
||||
};
|
||||
|
||||
const loadEdit = (ic: Interconsulta) => {
|
||||
setEdit(ic);
|
||||
setFecha(ic.fecha);
|
||||
setServicioInterconsultado(ic.servicioInterconsultado);
|
||||
setRealizada(ic.realizada);
|
||||
setRespuestaInterconsulta(ic.respuestaInterconsulta || '');
|
||||
setDialog(true);
|
||||
};
|
||||
|
||||
const icsFiltrados = interconsultas.filter(ic => ic.internacionId === internacionId);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nueva IC
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialog} onOpenChange={setDialog}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{edit ? 'Editar' : 'Nueva'} Interconsulta</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Fecha</Label>
|
||||
<Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Servicio Interconsultado</Label>
|
||||
<Input
|
||||
value={servicioInterconsultado}
|
||||
onChange={e => setServicioInterconsultado(e.target.value)}
|
||||
placeholder="Ej: Cardiología, Neurología, Infectología, etc."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="realizada"
|
||||
checked={realizada}
|
||||
onChange={e => setRealizada(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<Label htmlFor="realizada">Realizada</Label>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Respuesta</Label>
|
||||
<textarea
|
||||
className="w-full p-2 border rounded-md text-sm min-h-[100px]"
|
||||
value={respuestaInterconsulta}
|
||||
onChange={e => setRespuestaInterconsulta(e.target.value)}
|
||||
placeholder="Ingrese la respuesta de la interconsulta..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<Button variant="outline" onClick={() => setDialog(false)}>Cancelar</Button>
|
||||
<Button onClick={handleGuardar} disabled={!servicioInterconsultado}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Guardar
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{icsFiltrados.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<Users className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No hay interconsultas</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{icsFiltrados.sort((a, b) => new Date(b.fecha).getTime() - new Date(a.fecha).getTime()).map(ic => (
|
||||
<Card key={ic.id} className="hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Calendar className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm font-medium">{ic.fecha}</span>
|
||||
{ic.realizada && (
|
||||
<Badge variant="secondary" className="ml-2">Realizada</Badge>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">{ic.servicioInterconsultado}</h4>
|
||||
{ic.respuestaInterconsulta && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1 whitespace-pre-wrap">{ic.respuestaInterconsulta}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1 self-start">
|
||||
<Button size="sm" variant="outline" onClick={() => loadEdit(ic)}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="text-red-600" onClick={() => del(ic.id)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -152,4 +152,14 @@ export interface EstudioComplementario {
|
||||
resultado: string;
|
||||
}
|
||||
|
||||
export interface Interconsulta {
|
||||
id: string;
|
||||
pacienteId: string;
|
||||
internacionId: string;
|
||||
fecha: string;
|
||||
servicioInterconsultado: string;
|
||||
realizada: boolean;
|
||||
respuestaInterconsulta?: string;
|
||||
}
|
||||
|
||||
export type Vista = 'dashboard' | 'camas' | 'pacientes' | 'internaciones' | 'evoluciones' | 'laboratorios' | 'acidobase' | 'cultivos' | 'historiaclinica' | 'nuevoingreso' | 'editaringreso';
|
||||
|
||||
Reference in New Issue
Block a user