feat: sistema de notas clínicas para enfermero (ficha + notas libres) y fix lab_reportes métricas

This commit is contained in:
Lizandro Guarnizo
2026-03-25 13:14:25 -05:00
parent 4ba890a0d5
commit 5f695b5604
8 changed files with 1180 additions and 1 deletions
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* POST /api/lab/delete_nota_domicilio.php
* Elimina una nota libre de domicilio.
* Las notas tipo='clinica' NO se pueden eliminar (solo editar/vaciar).
*
* Body JSON: { id }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
try {
$datos = inputJson();
$id = (int)($datos['id'] ?? 0);
if (!$id) jsonError('id requerido');
$db = Database::getInstance();
$nota = $db->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);
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* GET /api/lab/get_notas_domicilio.php
* Devuelve todas las notas de un domicilio.
*
* Parámetros GET:
* domicilio_id (requerido)
*
* Respuesta: { success, data: [ nota, … ] }
* Orden: ficha clínica primero, luego libres por created_at ASC
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
$domId = (int)($_GET['domicilio_id'] ?? 0);
if (!$domId) jsonError('domicilio_id requerido');
$db = Database::getInstance();
// Verificar acceso: enfermero solo puede ver si está asignado
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);
}
$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);
}
+125
View File
@@ -0,0 +1,125 @@
<?php
/**
* POST /api/lab/save_nota_domicilio.php
* Crea o actualiza una nota de domicilio.
*
* Body JSON:
* { domicilio_id, tipo } — campos comunes (para crear)
* { id, tipo, … } — actualizar por id
*
* Campos tipo='clinica':
* antecedentes, medicamentos, acudiente_nombre, acudiente_documento
*
* Campos tipo='libre':
* titulo (requerido), cuerpo, imagen_path
*
* Reglas:
* · Solo puede existir UNA nota tipo='clinica' por domicilio.
* Si ya existe y no viene `id`, se hace UPSERT automático.
* · Las notas 'libre' son ilimitadas.
* · Solo el enfermero dueño o un admin puede editar/crear.
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
// Tags HTML permitidos en el cuerpo de notas libres (desde contenteditable)
const ALLOWED_HTML_TAGS = '<b><i><u><strong><em><s><strike><ul><ol><li><br><p><div>';
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);
}
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* POST /api/lab/upload_nota_imagen.php
* Sube una imagen adjunta a una nota libre de domicilio.
*
* POST multipart/form-data con campo 'file'
* Devuelve: { success, imagen_path, file_size }
*/
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../classes/Database.php';
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
requireAuthentication();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => 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'],
]);