From 5f695b5604cf676f8bdaae77da51f3e9619c2b85 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:14:25 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20sistema=20de=20notas=20cl=C3=ADnicas=20?= =?UTF-8?q?para=20enfermero=20(ficha=20+=20notas=20libres)=20y=20fix=20lab?= =?UTF-8?q?=5Freportes=20m=C3=A9tricas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/lab/delete_nota_domicilio.php | 44 + api/lab/get_notas_domicilio.php | 46 + api/lab/save_nota_domicilio.php | 125 +++ api/lab/upload_nota_imagen.php | 68 ++ enfermero_portal.php | 843 ++++++++++++++++++ lab_reportes.php | 2 +- .../20260325_lab_12_domicilio_notas.sql | 28 + migrations/run_20260325_notas.php | 25 + 8 files changed, 1180 insertions(+), 1 deletion(-) create mode 100644 api/lab/delete_nota_domicilio.php create mode 100644 api/lab/get_notas_domicilio.php create mode 100644 api/lab/save_nota_domicilio.php create mode 100644 api/lab/upload_nota_imagen.php create mode 100644 migrations/20260325_lab_12_domicilio_notas.sql create mode 100644 migrations/run_20260325_notas.php diff --git a/api/lab/delete_nota_domicilio.php b/api/lab/delete_nota_domicilio.php new file mode 100644 index 0000000..5a7d2b6 --- /dev/null +++ b/api/lab/delete_nota_domicilio.php @@ -0,0 +1,44 @@ +fetch('SELECT * FROM lab_domicilio_notas WHERE id = ?', [$id]); + if (!$nota) jsonError('Nota no encontrada', 404); + + if ($nota['tipo'] === 'clinica') { + jsonError('La ficha clínica no puede eliminarse, solo editarse'); + } + + // Solo el dueño o admin puede borrar + if (userRole() === 'enfermero' && (int)$nota['enfermera_id'] !== enfermeraId()) { + jsonError('Sin permiso para eliminar esta nota', 403); + } + + // Eliminar imagen adjunta si existe + if (!empty($nota['imagen_path'])) { + $imgPath = realpath(__DIR__ . '/../../uploads/media/' . $nota['imagen_path']); + if ($imgPath && file_exists($imgPath)) { + @unlink($imgPath); + } + } + + $db->delete('lab_domicilio_notas', 'id = ?', [$id]); + jsonOk(['id' => $id], 'Nota eliminada'); + +} catch (Exception $e) { + error_log('[delete_nota_domicilio] ' . $e->getMessage()); + jsonError($e->getMessage(), 500); +} diff --git a/api/lab/get_notas_domicilio.php b/api/lab/get_notas_domicilio.php new file mode 100644 index 0000000..5aad63a --- /dev/null +++ b/api/lab/get_notas_domicilio.php @@ -0,0 +1,46 @@ +fetch( + "SELECT id FROM lab_asignaciones + WHERE domicilio_id = ? AND enfermera_id = ?", + [$domId, $enfId] + ); + if (!$asig) jsonError('Sin acceso a este domicilio', 403); + } + + $notas = $db->fetchAll( + "SELECT n.*, e.nombre_completo AS enfermera_nombre + FROM lab_domicilio_notas n + LEFT JOIN lab_enfermeras e ON e.id = n.enfermera_id + WHERE n.domicilio_id = ? + ORDER BY FIELD(n.tipo,'clinica','libre'), n.created_at ASC", + [$domId] + ); + + jsonOk(['data' => $notas]); + +} catch (Exception $e) { + error_log('[get_notas_domicilio] ' . $e->getMessage()); + jsonError($e->getMessage(), 500); +} diff --git a/api/lab/save_nota_domicilio.php b/api/lab/save_nota_domicilio.php new file mode 100644 index 0000000..af22994 --- /dev/null +++ b/api/lab/save_nota_domicilio.php @@ -0,0 +1,125 @@ +

    1. '; + +try { + $datos = inputJson(); + $db = Database::getInstance(); + + $tipo = $datos['tipo'] ?? ''; + if (!in_array($tipo, ['clinica', 'libre'], true)) { + jsonError('tipo debe ser "clinica" o "libre"'); + } + + // ── Actualizar nota existente ───────────────────────────────────────── + if (!empty($datos['id'])) { + $id = (int)$datos['id']; + $nota = $db->fetch('SELECT * FROM lab_domicilio_notas WHERE id = ?', [$id]); + if (!$nota) jsonError('Nota no encontrada', 404); + + // Verificar propiedad + if (userRole() === 'enfermero' && (int)$nota['enfermera_id'] !== enfermeraId()) { + jsonError('No puede editar esta nota', 403); + } + + $campos = _extraerCampos($datos, $tipo); + $db->update('lab_domicilio_notas', $campos, 'id = ?', [$id]); + jsonOk(['id' => $id], 'Nota actualizada'); + } + + // ── Crear nota nueva (o upsert clínica) ─────────────────────────────── + $domId = (int)($datos['domicilio_id'] ?? 0); + if (!$domId) jsonError('domicilio_id requerido'); + + // Verificar que el domicilio existe + $dom = $db->fetch('SELECT id FROM lab_domicilios WHERE id = ?', [$domId]); + if (!$dom) jsonError('Domicilio no encontrado', 404); + + // Verificar acceso + if (userRole() === 'enfermero') { + $enfId = enfermeraId(); + $asig = $db->fetch( + "SELECT id FROM lab_asignaciones WHERE domicilio_id = ? AND enfermera_id = ?", + [$domId, $enfId] + ); + if (!$asig) jsonError('Sin acceso a este domicilio', 403); + } + + $enfId = (userRole() === 'enfermero') ? enfermeraId() : (int)($datos['enfermera_id'] ?? 0); + + // Para nota clínica: solo puede haber una por domicilio → UPDATE si ya existe + if ($tipo === 'clinica') { + $existente = $db->fetch( + 'SELECT id FROM lab_domicilio_notas WHERE domicilio_id = ? AND tipo = "clinica"', + [$domId] + ); + $campos = _extraerCampos($datos, 'clinica'); + if ($existente) { + $db->update('lab_domicilio_notas', $campos, 'id = ?', [(int)$existente['id']]); + jsonOk(['id' => (int)$existente['id']], 'Ficha clínica actualizada'); + } else { + $campos['domicilio_id'] = $domId; + $campos['enfermera_id'] = $enfId; + $campos['tipo'] = 'clinica'; + $id = $db->insert('lab_domicilio_notas', $campos); + jsonOk(['id' => $id], 'Ficha clínica creada'); + } + } + + // Nota libre + if (empty(trim($datos['titulo'] ?? ''))) jsonError('El título es obligatorio'); + $campos = _extraerCampos($datos, 'libre'); + $campos['domicilio_id'] = $domId; + $campos['enfermera_id'] = $enfId; + $campos['tipo'] = 'libre'; + $id = $db->insert('lab_domicilio_notas', $campos); + jsonOk(['id' => $id], 'Nota creada'); + +} catch (Exception $e) { + error_log('[save_nota_domicilio] ' . $e->getMessage()); + jsonError($e->getMessage(), 500); +} + +// ── Helper interno ──────────────────────────────────────────────────────── +function _extraerCampos(array $datos, string $tipo): array { + if ($tipo === 'clinica') { + return array_filter([ + 'antecedentes' => isset($datos['antecedentes']) ? trim($datos['antecedentes']) : null, + 'medicamentos' => isset($datos['medicamentos']) ? trim($datos['medicamentos']) : null, + 'acudiente_nombre' => isset($datos['acudiente_nombre']) ? trim($datos['acudiente_nombre']) : null, + 'acudiente_documento' => isset($datos['acudiente_documento']) ? trim($datos['acudiente_documento']) : null, + ], fn($v) => $v !== null); + } + // libre + $cuerpo = isset($datos['cuerpo']) + ? strip_tags((string)$datos['cuerpo'], ALLOWED_HTML_TAGS) + : null; + return array_filter([ + 'titulo' => isset($datos['titulo']) ? mb_substr(trim($datos['titulo']), 0, 200) : null, + 'cuerpo' => $cuerpo ?: null, + 'imagen_path' => isset($datos['imagen_path']) ? preg_replace('/[^a-zA-Z0-9._\-]/', '', (string)$datos['imagen_path']) : null, + ], fn($v) => $v !== null); +} diff --git a/api/lab/upload_nota_imagen.php b/api/lab/upload_nota_imagen.php new file mode 100644 index 0000000..46cec3d --- /dev/null +++ b/api/lab/upload_nota_imagen.php @@ -0,0 +1,68 @@ + false, 'error' => 'Método no permitido']); + exit; +} + +if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { + $code = $_FILES['file']['error'] ?? -1; + echo json_encode(['success' => false, 'error' => "No se recibió imagen (código $code)"]); + exit; +} + +$file = $_FILES['file']; + +// Máximo 5 MB para imágenes de notas +if ($file['size'] > 5 * 1024 * 1024) { + echo json_encode(['success' => false, 'error' => 'La imagen no puede superar 5 MB']); + exit; +} + +// Validar MIME real (solo imágenes) +$allowedMimes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']; +$finfo = finfo_open(FILEINFO_MIME_TYPE); +$mime = finfo_file($finfo, $file['tmp_name']); +finfo_close($finfo); + +if (!in_array($mime, $allowedMimes, true)) { + echo json_encode(['success' => false, 'error' => "Tipo de archivo no permitido ($mime). Solo imágenes."]); + exit; +} + +// Extensión segura +$mimeToExt = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif']; +$ext = $mimeToExt[$mime] ?? 'jpg'; +$newName = 'nota_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext; + +$uploadDir = __DIR__ . '/../../uploads/media'; +if (!is_dir($uploadDir)) { + @mkdir($uploadDir, 0755, true); +} +$destPath = realpath($uploadDir) . '/' . $newName; + +if (!move_uploaded_file($file['tmp_name'], $destPath)) { + echo json_encode(['success' => false, 'error' => 'Error al guardar el archivo en el servidor']); + exit; +} + +echo json_encode([ + 'success' => true, + 'imagen_path' => $newName, + 'file_size' => $file['size'], +]); diff --git a/enfermero_portal.php b/enfermero_portal.php index dd9e7c0..773ff73 100644 --- a/enfermero_portal.php +++ b/enfermero_portal.php @@ -117,6 +117,55 @@ $hoy = date('Y-m-d'); .pago-badge-exento { background:#e0e7ff; color:#3730a3; border:1px solid #818cf8; } .cobro-box { background:#f0fdf4; border:1px solid #86efac; border-radius:8px; padding:.6rem .9rem; } .cobro-box.sin-cobro { background:#f9fafb; border-color:#e5e7eb; color:#9ca3af; } + + /* ── Notas clínicas ── */ + .nota-toggle-btn { text-align:left; font-size:.78rem; font-weight:700; + background:#fef9c3; border:1px solid #fde68a; color:#92400e; + border-radius:8px; padding:7px 10px; width:100%; cursor:pointer; + display:flex; align-items:center; gap:6px; transition:.15s; } + .nota-toggle-btn:active { transform:scale(.98); } + .nota-chevron { margin-left:auto; transition:transform .2s; font-size:.7rem; } + .nota-chevron.open { transform:rotate(180deg); } + .notas-body { padding:0; margin-top:4px; } + .nota-libre-card { background:#fff; border:1px solid #e5e7eb; border-radius:8px; + padding:9px 11px; margin-bottom:6px; } + .nota-libre-card .nota-titulo { font-weight:700; font-size:.82rem; color:#374151; } + .nota-libre-card .nota-cuerpo { font-size:.78rem; color:#4b5563; margin-top:4px; + word-break:break-word; } + .nota-libre-card .nota-fecha { font-size:.69rem; color:#9ca3af; margin-top:4px; } + .nota-ficha-card { background:#fff7ed; border:1px solid #fdba74; border-radius:10px; + padding:9px 11px; margin-bottom:6px; } + .nota-ficha-card .ficha-header { display:flex; justify-content:space-between; + align-items:center; margin-bottom:6px; } + .nota-ficha-card .ficha-titulo { font-weight:700; font-size:.82rem; color:#9a3412; } + /* Bottom sheet */ + #nota-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,.45); z-index:1050; } + #nota-sheet { display:none; position:fixed; bottom:0; left:0; right:0; z-index:1055; + background:#fff; border-radius:20px 20px 0 0; max-height:88vh; + overflow-y:auto; box-shadow:0 -4px 24px rgba(0,0,0,.18); + padding-bottom:env(safe-area-inset-bottom, 16px); } + #nota-sheet .sheet-handle { display:flex; justify-content:center; padding:10px 0 2px; } + #nota-sheet .sheet-handle span { width:40px; height:4px; background:#e0e0e0; border-radius:2px; display:block; } + #nota-sheet-inner { padding:8px 16px 28px; } + /* Toolbar formato */ + .fmt-toolbar { display:flex; gap:4px; overflow-x:auto; padding-bottom:2px; margin-bottom:6px; } + .fmt-btn { min-width:34px; border:1px solid #e5e7eb; background:#fff; border-radius:5px; + font-size:.78rem; padding:4px 8px; cursor:pointer; white-space:nowrap; } + .fmt-btn:active { background:#f0f4ff; } + /* Campos de nota */ + .nota-input { width:100%; border:1px solid #d1d5db; border-radius:8px; + padding:8px 12px; font-size:.88rem; font-family:inherit; resize:vertical; } + .nota-input:focus { outline:2px solid #0d6efd; border-color:transparent; } + .nota-editor { min-height:90px; border:1px solid #d1d5db; border-radius:8px; + padding:8px 12px; font-size:.85rem; outline:none; background:#fff; + word-break:break-word; -webkit-user-select:text; cursor:text; } + .nota-editor:focus { outline:2px solid #0d6efd; border-color:transparent; } + .nota-label { font-size:.78rem; font-weight:700; color:#374151; display:block; margin-bottom:4px; } + .nota-section { margin-bottom:14px; } + .nota-acudiente-bloque { background:#fff0f0; border:1px solid #fca5a5; + border-radius:10px; padding:10px; } + .nota-btn-guardar { width:100%; border:none; border-radius:10px; padding:13px; + font-weight:800; font-size:.92rem; color:#fff; cursor:pointer; } @@ -550,6 +599,21 @@ const portal = { ${seSuf ? `
      ${seSuf}
      ` : ''} + +
      + +
      +
      +
      +
      ${btnsAccion} @@ -734,6 +798,426 @@ function renderOrdenesEnfermero(item) { }).join(''); } +// ═══════════════════════════════════════════════════════════════ +// notasManager — Sistema de notas clínicas por domicilio +// ═══════════════════════════════════════════════════════════════ +const notasManager = { + _cache: {}, // domId → { clinica: nota|null, libres: [] } + _editDomId: null, + _editNota: null, // nota completa o null (nueva) + _editTipo: null, // 'clinica' | 'libre' + _editPacNac: '', + + // ── Edad ───────────────────────────────────────────────────────────── + _esmenor(nacStr) { + if (!nacStr) return null; // desconocido + const dif = Date.now() - new Date(nacStr).getTime(); + return Math.floor(dif / (365.25 * 24 * 3600 * 1000)) < 18; + }, + + // ── Toggle sección ──────────────────────────────────────────────────── + async toggle(domId, btn) { + const body = document.getElementById(`notas-body-${domId}`); + const chevron = btn.querySelector('.nota-chevron'); + if (!body) return; + const abrir = body.classList.contains('d-none'); + body.classList.toggle('d-none', !abrir); + chevron?.classList.toggle('open', abrir); + if (abrir && !this._cache[domId]) { + await this.cargar(domId); + } else if (abrir) { + this._renderBody(domId); + } + }, + + // ── Cargar desde API ───────────────────────────────────────────────── + async cargar(domId) { + const body = document.getElementById(`notas-body-${domId}`); + if (!body) return; + body.innerHTML = '
      Cargando…
      '; + try { + const r = await fetch(`api/lab/get_notas_domicilio.php?domicilio_id=${domId}`); + const d = await r.json(); + if (!d.success) throw new Error(d.error || 'Error al cargar notas'); + this._cache[domId] = { + clinica: d.data.find(n => n.tipo === 'clinica') || null, + libres: d.data.filter(n => n.tipo === 'libre'), + }; + this._renderBody(domId); + // Badge contador + const total = d.data.length; + const badge = document.getElementById(`nota-count-${domId}`); + if (badge) { badge.textContent = total; badge.style.display = total ? '' : 'none'; } + } catch (e) { + if (body) body.innerHTML = `
      ${esc(e.message)}
      `; + } + }, + + // ── Render cuerpo expandido ────────────────────────────────────────── + _renderBody(domId) { + const body = document.getElementById(`notas-body-${domId}`); + if (!body) return; + const pacNac = body.dataset.pacNac || ''; + const menor = this._esmenor(pacNac); + const cache = this._cache[domId] || { clinica: null, libres: [] }; + + body.innerHTML = ` +
      + ${this._fichaHTML(cache.clinica, domId, menor)} + ${cache.libres.map(n => this._libreHTML(n)).join('')} + +
      `; + }, + + _fichaHTML(nota, domId, menor) { + const vacio = !nota; + const tieneAcud = nota?.acudiente_nombre || nota?.acudiente_documento; + return `
      +
      +
      + Ficha clínica + ${menor === true ? ' · menor de edad' : ''} +
      + +
      + ${vacio + ? `
      Sin información registrada
      ` + : `
      + ${nota.antecedentes ? `
      Antecedentes
      ${esc(nota.antecedentes).replace(/\n/g,'
      ')}
      ` : ''} + ${nota.medicamentos ? `
      Medicamentos
      ${esc(nota.medicamentos).replace(/\n/g,'
      ')}
      ` : ''} + ${tieneAcud ? `
      Acudiente
      +
      ${esc(nota.acudiente_nombre||'—')} · ${esc(nota.acudiente_documento||'—')}
      ` : ''} +
      `} +
      `; + }, + + _libreHTML(nota) { + const domId = nota.domicilio_id; + return `
      +
      +
      ${esc(nota.titulo||'Nota')}
      +
      + + +
      +
      + ${nota.cuerpo ? `
      ${nota.cuerpo}
      ` : ''} + ${nota.imagen_path ? `` : ''} +
      ${_fmtFechaCorta(nota.created_at||'')}
      +
      `; + }, + + // ── Abrir bottom sheet: Ficha clínica ──────────────────────────────── + abrirClinica(domId) { + this._editDomId = domId; + this._editTipo = 'clinica'; + this._editNota = this._cache[domId]?.clinica || null; + const bodyEl = document.getElementById(`notas-body-${domId}`); + this._editPacNac = bodyEl?.dataset?.pacNac || ''; + const menor = this._esmenor(this._editPacNac); + const nota = this._editNota; + const inner = document.getElementById('nota-sheet-inner'); + + inner.innerHTML = ` +
      +
      + Ficha clínica +
      + +
      + +
      + + +
      +
      + + +
      + +
      +
      + Datos del acudiente + ${menor === true + ? 'obligatorio (paciente menor de edad)' + : ' (si aplica)'} +
      +
      + + +
      +
      + + +
      +
      + +
      + +
      `; + + this._abrirSheet(); + }, + + // ── Abrir bottom sheet: Nota libre nueva ──────────────────────────── + abrirLibre(domId) { + const bodyEl = document.getElementById(`notas-body-${domId}`); + this._editDomId = domId; + this._editTipo = 'libre'; + this._editNota = null; + this._editPacNac = bodyEl?.dataset?.pacNac || ''; + this._renderFormLibre(null); + this._abrirSheet(); + }, + + // ── Abrir bottom sheet: Editar nota libre ─────────────────────────── + abrirEditarLibre(domId, notaId) { + const nota = (this._cache[domId]?.libres||[]).find(n => +n.id === +notaId); + const bodyEl = document.getElementById(`notas-body-${domId}`); + this._editDomId = domId; + this._editTipo = 'libre'; + this._editNota = nota || null; + this._editPacNac = bodyEl?.dataset?.pacNac || ''; + this._renderFormLibre(nota); + this._abrirSheet(); + }, + + _renderFormLibre(nota) { + const inner = document.getElementById('nota-sheet-inner'); + inner.innerHTML = ` +
      +
      + ${nota ? 'Editar nota' : 'Nueva nota'} +
      + +
      + +
      + + +
      + +
      + +
      + + + + + + +
      +
      +
      + +
      + +
      + ${nota?.imagen_path ? `
      + + +
      ` : ''} +
      + + +
      + + `; + + // Cargar HTML en contenteditable + if (nota?.cuerpo) { + requestAnimationFrame(() => { + const el = document.getElementById('nl-cuerpo'); + if (el) el.innerHTML = nota.cuerpo; + }); + } + }, + + _fmt(cmd) { document.getElementById('nl-cuerpo')?.focus(); document.execCommand(cmd, false, null); }, + _insertLista() { document.getElementById('nl-cuerpo')?.focus(); document.execCommand('insertUnorderedList', false, null); }, + _limpiarFormato() { document.getElementById('nl-cuerpo')?.focus(); document.execCommand('removeFormat', false, null); }, + + _previsualizarImg(input) { + if (!input.files?.[0]) return; + const reader = new FileReader(); + reader.onload = e => { + const prev = document.getElementById('nl-preview'); + if (prev) prev.innerHTML = `
      + + +
      `; + }; + reader.readAsDataURL(input.files[0]); + }, + + _quitarImg() { + document.getElementById('nl-preview').innerHTML = ''; + const inp = document.getElementById('nl-img-input'); + if (inp) inp.value = ''; + const h = document.getElementById('nl-img-guardada'); + if (h) h.value = ''; + }, + + // ── Guardar ────────────────────────────────────────────────────────── + async guardar() { + const btnId = this._editTipo === 'clinica' ? 'nc-guardar-btn' : 'nl-guardar-btn'; + const btn = document.getElementById(btnId); + const labs = { clinica: 'Guardar ficha clínica', libre: 'Guardar nota' }; + if (btn) { btn.disabled = true; btn.innerHTML = 'Guardando…'; } + + try { + let imagenPath = null; + + if (this._editTipo === 'libre') { + const imgInput = document.getElementById('nl-img-input'); + if (imgInput?.files?.[0]) { + imagenPath = await this._subirImagen(imgInput.files[0]); + } else { + imagenPath = document.getElementById('nl-img-guardada')?.value || null; + } + } + + let payload = { domicilio_id: this._editDomId, tipo: this._editTipo }; + if (this._editNota?.id) payload.id = +this._editNota.id; + + if (this._editTipo === 'clinica') { + payload.antecedentes = document.getElementById('nc-antecedentes')?.value.trim() || null; + payload.medicamentos = document.getElementById('nc-medicamentos')?.value.trim() || null; + const menor = this._esmenor(this._editPacNac); + const acNombre = document.getElementById('nc-acud-nombre')?.value.trim() || null; + const acDoc = document.getElementById('nc-acud-doc')?.value.trim() || null; + if (menor === true && (!acNombre || !acDoc)) { + alert('El nombre y documento del acudiente son obligatorios para pacientes menores de edad.'); + if (btn) { btn.disabled = false; btn.innerHTML = `${labs.clinica}`; } + return; + } + payload.acudiente_nombre = acNombre; + payload.acudiente_documento = acDoc; + } else { + const titulo = document.getElementById('nl-titulo')?.value.trim(); + if (!titulo) { + alert('El título de la nota es obligatorio.'); + if (btn) { btn.disabled = false; btn.innerHTML = `${labs.libre}`; } + return; + } + payload.titulo = titulo; + payload.cuerpo = document.getElementById('nl-cuerpo')?.innerHTML.trim() || null; + payload.imagen_path = imagenPath; + } + + const r = await fetch('api/lab/save_nota_domicilio.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const d = await r.json(); + if (!d.success) throw new Error(d.error || 'Error al guardar'); + + delete this._cache[this._editDomId]; + this.cerrarSheet(); + await this.cargar(this._editDomId); + } catch (e) { + alert('Error: ' + e.message); + if (btn) { btn.disabled = false; btn.innerHTML = `Reintentar`; } + } + }, + + async _subirImagen(file) { + const fd = new FormData(); + fd.append('file', file); + const r = await fetch('api/lab/upload_nota_imagen.php', { method: 'POST', body: fd }); + const d = await r.json(); + if (!d.success) throw new Error(d.error || 'Error al subir imagen'); + return d.imagen_path; + }, + + async eliminar(notaId, domId) { + if (!confirm('¿Eliminar esta nota?')) return; + try { + const r = await fetch('api/lab/delete_nota_domicilio.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: notaId }), + }); + const d = await r.json(); + if (!d.success) throw new Error(d.error || 'Error al eliminar'); + delete this._cache[domId]; + await this.cargar(domId); + } catch (e) { + alert('Error: ' + e.message); + } + }, + + _abrirSheet() { + const sheet = document.getElementById('nota-sheet'); + const backdrop = document.getElementById('nota-backdrop'); + if (!sheet || !backdrop) return; + sheet.style.transform = 'translateY(100%)'; + sheet.style.transition = ''; + sheet.style.display = 'block'; + backdrop.style.display = 'block'; + requestAnimationFrame(() => { + sheet.style.transition = 'transform .32s cubic-bezier(.4,0,.2,1)'; + sheet.style.transform = 'translateY(0)'; + }); + }, + + cerrarSheet() { + const sheet = document.getElementById('nota-sheet'); + const backdrop = document.getElementById('nota-backdrop'); + if (!sheet) return; + sheet.style.transition = 'transform .28s cubic-bezier(.4,0,.2,1)'; + sheet.style.transform = 'translateY(100%)'; + setTimeout(() => { + sheet.style.display = 'none'; + if (backdrop) backdrop.style.display = 'none'; + }, 300); + }, +}; + +function _fmtFechaCorta(s) { + if (!s) return ''; + return new Date(s).toLocaleString('es-CO', { day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit' }); +} + document.addEventListener('DOMContentLoaded', () => { portal.init(); agendaNueva.init(); @@ -840,6 +1324,13 @@ const formVer = { }; + +
      +
      +
      +
      +
      + +
      `; + if (fc) { + if (fc.antecedentes) html += `
      Antecedentes: ${esc(fc.antecedentes)}
      `; + if (fc.medicamentos) html += `
      Medicamentos: ${esc(fc.medicamentos)}
      `; + if (fc.acudiente_nombre) html += `
      Acudiente: ${esc(fc.acudiente_nombre)} · ${esc(fc.acudiente_documento||'—')}
      `; + } else { + html += `
      Sin ficha clínica registrada
      `; + } + if (esMenor && (!fc || !fc.acudiente_nombre)) { + html += `
      Paciente menor de edad — acudiente requerido
      `; + } + html += `
      `; + + // 2. Notas libres + data.libres.forEach(n => { + html += `
      +
      +
      ${esc(n.titulo||'Sin título')}
      +
      + + +
      +
      +
      ${n.cuerpo ? n.cuerpo.replace(/\n/g,'
      ') : ''}
      + ${n.imagen_path ? `` : ''} +
      ${esc(n.created_at||'')}
      +
      `; + }); + + html += ``; + + body.innerHTML = html; + }, + + // ── Bottom sheet: Ficha clínica ──────────────────────────────────────── + abrirFicha(domId) { + this._domId = domId; + this._notaId = 'ficha'; + this._imagen = null; + + const body = document.getElementById(`notas-body-${domId}`); + const pac = body ? body.dataset.pacNac || '' : ''; + const esMenor= pac ? this._esmenor(pac) : false; + const fc = (this._cache[domId] || {}).clinica || {}; + + document.getElementById('nota-sheet-title').textContent = 'Ficha clínica'; + document.getElementById('nota-sheet-inner').innerHTML = ` +
      + + +
      +
      + + +
      +
      +
      Datos del acudiente (paciente menor de edad)
      + + + + +
      + ${!esMenor ? `` : ''} + `; + + if (!esMenor && fc.acudiente_nombre) { + document.getElementById('ns-acudiente-bloque').classList.remove('d-none'); + } + this._abrirSheet(); + }, + + // ── Bottom sheet: Nota libre ─────────────────────────────────────────── + abrirLibre(domId, notaId) { + this._domId = domId; + this._notaId = notaId; + this._imagen = null; + + let titulo = '', cuerpo = ''; + if (notaId) { + const n = (this._cache[domId]?.libres || []).find(x => x.id == notaId); + if (n) { titulo = n.titulo || ''; cuerpo = n.cuerpo || ''; } + } + + document.getElementById('nota-sheet-title').textContent = notaId ? 'Editar nota' : 'Nueva nota'; + document.getElementById('nota-sheet-inner').innerHTML = ` +
      + + +
      +
      + +
      + + + + + + +
      +
      ${cuerpo}
      +
      +
      + +
      + + +
      +
      +
      + `; + + this._abrirSheet(); + }, + + // ── Guardar ficha clínica ───────────────────────────────────────────── + async _guardarFicha() { + const esMenor = !document.getElementById('ns-acudiente-bloque')?.classList.contains('d-none'); + const antecedentes = document.getElementById('ns-antecedentes')?.value.trim() || ''; + const medicamentos = document.getElementById('ns-medicamentos')?.value.trim() || ''; + const acudNombre = document.getElementById('ns-acud-nombre')?.value.trim() || ''; + const acudDoc = document.getElementById('ns-acud-doc')?.value.trim() || ''; + + if (esMenor && (!acudNombre || !acudDoc)) { + alert('El paciente es menor de edad — completa nombre y documento del acudiente.'); + return; + } + try { + const r = await fetch('api/lab/save_nota_domicilio.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tipo: 'clinica', + domicilio_id: this._domId, + antecedentes, medicamentos, + acudiente_nombre: acudNombre || null, + acudiente_documento: acudDoc || null, + }), + }); + const d = await r.json(); + if (!d.success) throw new Error(d.error || 'Error al guardar'); + delete this._cache[this._domId]; + this._cerrarSheet(); + await this._renderSeccion(this._domId); + } catch(e) { alert('Error: ' + e.message); } + }, + + // ── Guardar nota libre ───────────────────────────────────────────────── + async _guardarLibre() { + const titulo = document.getElementById('ns-titulo')?.value.trim() || ''; + const cuerpo = document.getElementById('ns-cuerpo')?.innerHTML || ''; + if (!titulo) { alert('Escribe un título para la nota.'); return; } + + let imagenPath = null; + if (this._imagen) { + const fd = new FormData(); + fd.append('file', this._imagen); + const ru = await fetch('api/lab/upload_nota_imagen.php', { method:'POST', body: fd }); + const du = await ru.json(); + if (!du.success) { alert('No se pudo subir la imagen: ' + (du.error||'')); return; } + imagenPath = du.local_file; + } + + try { + const payload = { + tipo: 'libre', + domicilio_id: this._domId, + titulo, + cuerpo, + imagen_path: imagenPath, + }; + if (this._notaId) payload.id = this._notaId; + + const r = await fetch('api/lab/save_nota_domicilio.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const d = await r.json(); + if (!d.success) throw new Error(d.error || 'Error al guardar'); + delete this._cache[this._domId]; + this._cerrarSheet(); + await this._renderSeccion(this._domId); + } catch(e) { alert('Error: ' + e.message); } + }, + + // ── Eliminar nota libre ──────────────────────────────────────────────── + async eliminar(notaId, domId) { + if (!confirm('¿Eliminar esta nota?')) return; + try { + const r = await fetch('api/lab/delete_nota_domicilio.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: notaId }), + }); + const d = await r.json(); + if (!d.success) throw new Error(d.error); + delete this._cache[domId]; + await this._renderSeccion(domId); + } catch(e) { alert('Error: ' + e.message); } + }, + + // ── Helpers de UI ────────────────────────────────────────────────────── + _abrirSheet() { + document.getElementById('nota-backdrop').style.display = 'block'; + document.getElementById('nota-sheet').style.display = 'block'; + document.body.style.overflow = 'hidden'; + }, + _cerrarSheet() { + document.getElementById('nota-backdrop').style.display = 'none'; + document.getElementById('nota-sheet').style.display = 'none'; + document.body.style.overflow = ''; + this._domId = null; this._notaId = null; this._imagen = null; + }, + _fmt(cmd) { document.execCommand(cmd, false, null); document.getElementById('ns-cuerpo')?.focus(); }, + _toggleAcudiente(on) { + document.getElementById('ns-acudiente-bloque')?.classList.toggle('d-none', !on); + }, + _prevImagen(input) { + const file = input.files?.[0]; + if (!file) return; + this._imagen = file; + const reader = new FileReader(); + reader.onload = e => { + const prev = document.getElementById('ns-img-preview'); + if (prev) prev.innerHTML = ` +
      ${esc(file.name)}
      `; + }; + reader.readAsDataURL(file); + }, + _verImg(src) { + const w = window.open('', '_blank'); + if (w) { w.document.write(``); } + }, + _esmenor(fechaNac) { + if (!fechaNac) return false; + const d = new Date(fechaNac); + const hoy = new Date(); + const años = hoy.getFullYear() - d.getFullYear() - + (hoy < new Date(hoy.getFullYear(), d.getMonth(), d.getDate()) ? 1 : 0); + return años < 18; + }, +}; + +
      + + diff --git a/lab_reportes.php b/lab_reportes.php index 3e9391b..326c8d0 100644 --- a/lab_reportes.php +++ b/lab_reportes.php @@ -433,7 +433,7 @@ async function cargarMetricas(desde, hasta) { const r = await fetch(`api/lab/get_metricas.php?desde=${desde}&hasta=${hasta}`); const d = await r.json(); if (!d.success) return; - const m = d.data; + const m = d; // jsonOk() hace array_merge → datos en nivel raíz // ── KPIs ── const kpi = m.kpi || {}; diff --git a/migrations/20260325_lab_12_domicilio_notas.sql b/migrations/20260325_lab_12_domicilio_notas.sql new file mode 100644 index 0000000..21fe633 --- /dev/null +++ b/migrations/20260325_lab_12_domicilio_notas.sql @@ -0,0 +1,28 @@ +-- Migración: Sistema de notas clínicas por domicilio +-- Permite al enfermero registrar: +-- · Una ficha clínica (antecedentes, medicamentos, acudiente si es menor) +-- · N notas libres adicionales con título, texto enriquecido e imagen adjunta + +CREATE TABLE IF NOT EXISTS `lab_domicilio_notas` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `domicilio_id` INT(11) NOT NULL COMMENT 'FK lab_domicilios.id', + `enfermera_id` INT(11) NOT NULL COMMENT 'FK lab_enfermeras.id — quien registró', + `tipo` ENUM('clinica','libre') NOT NULL DEFAULT 'libre', + -- Campos exclusivos de tipo='clinica' + `antecedentes` TEXT DEFAULT NULL COMMENT 'Antecedentes clínicos del paciente', + `medicamentos` TEXT DEFAULT NULL COMMENT 'Medicamentos actuales del paciente', + `acudiente_nombre` VARCHAR(200) DEFAULT NULL COMMENT 'Nombre del acudiente (obligatorio si es menor)', + `acudiente_documento` VARCHAR(50) DEFAULT NULL COMMENT 'Documento del acudiente', + -- Campos exclusivos de tipo='libre' + `titulo` VARCHAR(200) DEFAULT NULL COMMENT 'Título de la nota libre', + `cuerpo` TEXT DEFAULT NULL COMMENT 'Cuerpo de la nota (HTML básico desde contenteditable)', + `imagen_path` VARCHAR(300) DEFAULT NULL COMMENT 'Nombre del archivo de imagen en uploads/media/', + -- Timestamps + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_notas_domicilio` (`domicilio_id`), + KEY `idx_notas_enfermera` (`enfermera_id`), + KEY `idx_notas_tipo` (`domicilio_id`, `tipo`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Notas clínicas y libres registradas por enfermeros durante un servicio de domicilio'; diff --git a/migrations/run_20260325_notas.php b/migrations/run_20260325_notas.php new file mode 100644 index 0000000..2cc452c --- /dev/null +++ b/migrations/run_20260325_notas.php @@ -0,0 +1,25 @@ +getConnection(); +$sql = file_get_contents(__DIR__ . '/20260325_lab_12_domicilio_notas.sql'); + +// Ejecutar la sentencia CREATE TABLE +try { + $db->exec($sql); + echo "✅ Tabla lab_domicilio_notas creada correctamente.\n"; +} catch (PDOException $e) { + if (str_contains($e->getMessage(), 'already exists')) { + echo "⚠️ La tabla ya existe (omitido).\n"; + } else { + echo "❌ ERROR: " . $e->getMessage() . "\n"; + exit(1); + } +} + +echo "Migración completada.\n";