diff --git a/api/lab/save_nota_domicilio.php b/api/lab/save_nota_domicilio.php
index af22994..c666d82 100644
--- a/api/lab/save_nota_domicilio.php
+++ b/api/lab/save_nota_domicilio.php
@@ -117,9 +117,28 @@ function _extraerCampos(array $datos, string $tipo): array {
$cuerpo = isset($datos['cuerpo'])
? strip_tags((string)$datos['cuerpo'], ALLOWED_HTML_TAGS)
: null;
+
+ // Validar y limpiar archivos (array de {nombre,path,mime,size})
+ $archivos = null;
+ if (!empty($datos['archivos']) && is_array($datos['archivos'])) {
+ $limpios = [];
+ foreach ($datos['archivos'] as $a) {
+ $path = preg_replace('/[^a-zA-Z0-9._\-]/', '', (string)($a['path'] ?? ''));
+ if (!$path) continue;
+ $limpios[] = [
+ 'nombre' => mb_substr(strip_tags((string)($a['nombre'] ?? $path)), 0, 100),
+ 'path' => $path,
+ 'mime' => preg_replace('/[^a-zA-Z0-9\/\-+.]/', '', (string)($a['mime'] ?? '')),
+ 'size' => (int)($a['size'] ?? 0),
+ ];
+ }
+ if ($limpios) $archivos = json_encode($limpios, JSON_UNESCAPED_UNICODE);
+ }
+
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,
+ 'archivos' => $archivos,
], fn($v) => $v !== null);
}
diff --git a/api/lab/upload_nota_imagen.php b/api/lab/upload_nota_imagen.php
index 46cec3d..022814e 100644
--- a/api/lab/upload_nota_imagen.php
+++ b/api/lab/upload_nota_imagen.php
@@ -1,10 +1,12 @@
false, 'error' => "No se recibió imagen (código $code)"]);
- exit;
+$allowedMimes = [
+ // Imágenes
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/webp' => 'webp',
+ 'image/gif' => 'gif',
+ // Documentos
+ 'application/pdf' => 'pdf',
+ 'application/msword' => 'doc',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
+ 'application/vnd.ms-excel' => 'xls',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
+ 'text/plain' => 'txt',
+ 'text/csv' => 'csv',
+];
+
+// Normalizar: acepta 'file' (uno) o 'files[]' (varios)
+$incoming = [];
+if (!empty($_FILES['files']['name'][0])) {
+ // múltiples
+ for ($i = 0; $i < count($_FILES['files']['name']); $i++) {
+ $incoming[] = [
+ 'name' => $_FILES['files']['name'][$i],
+ 'tmp_name' => $_FILES['files']['tmp_name'][$i],
+ 'error' => $_FILES['files']['error'][$i],
+ 'size' => $_FILES['files']['size'][$i],
+ ];
+ }
+} elseif (!empty($_FILES['file']['tmp_name'])) {
+ $incoming[] = [
+ 'name' => $_FILES['file']['name'],
+ 'tmp_name' => $_FILES['file']['tmp_name'],
+ 'error' => $_FILES['file']['error'],
+ 'size' => $_FILES['file']['size'],
+ ];
}
-$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']);
+if (empty($incoming)) {
+ echo json_encode(['success' => false, 'error' => 'No se recibieron archivos']);
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;
+$resultado = [];
+$finfo = finfo_open(FILEINFO_MIME_TYPE);
+
+foreach ($incoming as $file) {
+ if ($file['error'] !== UPLOAD_ERR_OK) {
+ echo json_encode(['success' => false, 'error' => "Error al recibir archivo: código {$file['error']}"]);
+ finfo_close($finfo);
+ exit;
+ }
+ if ($file['size'] > 10 * 1024 * 1024) {
+ echo json_encode(['success' => false, 'error' => "El archivo '{$file['name']}' supera 10 MB"]);
+ finfo_close($finfo);
+ exit;
+ }
+
+ $mime = finfo_file($finfo, $file['tmp_name']);
+ if (!isset($allowedMimes[$mime])) {
+ echo json_encode(['success' => false, 'error' => "Tipo no permitido ($mime). Imágenes, PDF o documentos Office."]);
+ finfo_close($finfo);
+ exit;
+ }
+
+ $ext = $allowedMimes[$mime];
+ $newName = 'nota_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
+ $dest = realpath($uploadDir) . '/' . $newName;
+
+ if (!move_uploaded_file($file['tmp_name'], $dest)) {
+ echo json_encode(['success' => false, 'error' => 'Error al guardar el archivo en el servidor']);
+ finfo_close($finfo);
+ exit;
+ }
+
+ $nombreOriginal = mb_substr(preg_replace('/[^\w.\-áéíóúÁÉÍÓÚñÑ ]/u', '', $file['name']), 0, 100);
+
+ $resultado[] = [
+ 'nombre' => $nombreOriginal ?: $newName,
+ 'path' => $newName,
+ 'mime' => $mime,
+ 'size' => $file['size'],
+ ];
}
+finfo_close($finfo);
+
echo json_encode([
- 'success' => true,
- 'imagen_path' => $newName,
- 'file_size' => $file['size'],
+ 'success' => true,
+ 'archivos' => $resultado,
+ // retrocompatibilidad: si solo fue uno, devolver también imagen_path
+ 'imagen_path'=> count($resultado) === 1 ? $resultado[0]['path'] : null,
]);
diff --git a/enfermero_portal.php b/enfermero_portal.php
index 3bd6acf..47cf718 100644
--- a/enfermero_portal.php
+++ b/enfermero_portal.php
@@ -1931,9 +1931,7 @@ const notasManager = {
${n.cuerpo ? n.cuerpo.replace(/\n/g,'
') : ''}
- ${n.imagen_path ? `
` : ''}
+ ${_renderArchivosNota(n)}
${esc(n.created_at||'')}
`;
});
@@ -1948,9 +1946,10 @@ const notasManager = {
// ── Bottom sheet: Ficha clínica ────────────────────────────────────────
abrirFicha(domId) {
- this._domId = domId;
- this._notaId = 'ficha';
- this._imagen = null;
+ this._domId = domId;
+ this._notaId = 'ficha';
+ this._imagen = null;
+ this._archivos = [];
const body = document.getElementById(`notas-body-${domId}`);
const pac = body ? body.dataset.pacNac || '' : '';
@@ -1992,9 +1991,10 @@ const notasManager = {
// ── Bottom sheet: Nota libre ───────────────────────────────────────────
abrirLibre(domId, notaId) {
- this._domId = domId;
- this._notaId = notaId;
- this._imagen = null;
+ this._domId = domId;
+ this._notaId = notaId;
+ this._imagen = null;
+ this._archivos = [];
let titulo = '', cuerpo = '';
if (notaId) {
@@ -2023,20 +2023,25 @@ const notasManager = {
data-placeholder="Escribe aquí la observación...">${cuerpo}
-
-