diff --git a/api/lab/upload_orden.php b/api/lab/upload_orden.php
index 527e4ee..5f9a5b5 100644
--- a/api/lab/upload_orden.php
+++ b/api/lab/upload_orden.php
@@ -1,99 +1,132 @@
false, 'error' => 'Método no permitido']);
- exit;
-}
-
-if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
- $errCode = $_FILES['file']['error'] ?? -1;
- echo json_encode(['success' => false, 'error' => 'No se recibió ningún archivo (código ' . $errCode . ')']);
- exit;
-}
-
-$file = $_FILES['file'];
-
-// Validar tamaño máximo: 10 MB
-if ($file['size'] > 10 * 1024 * 1024) {
- echo json_encode(['success' => false, 'error' => 'El archivo supera el tamaño máximo permitido (10 MB)']);
- exit;
-}
-
-// Validar tipo MIME real (no confiar solo en la extensión del cliente)
$allowedMimes = [
- 'image/jpeg',
- 'image/png',
- 'image/gif',
- 'image/webp',
- 'application/pdf',
- 'application/msword',
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/gif' => 'gif',
+ 'image/webp' => 'webp',
+ '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',
];
-$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 . ')']);
- exit;
-}
-
-// Generar nombre único y seguro
-$origExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
-$origExt = preg_replace('/[^a-z0-9]/', '', $origExt);
-if (!$origExt) {
- $mimeToExt = [
- 'image/jpeg' => 'jpg',
- 'image/png' => 'png',
- 'image/gif' => 'gif',
- 'image/webp' => 'webp',
- 'application/pdf' => 'pdf',
- 'application/msword' => 'doc',
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
+// Normalizar: acepta files[] o file (retrocompat)
+$rawFiles = [];
+if (!empty($_FILES['files']['name'][0])) {
+ // files[] (múltiples)
+ $count = count($_FILES['files']['name']);
+ for ($i = 0; $i < $count; $i++) {
+ $rawFiles[] = [
+ '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'])) {
+ // file (singular, retrocompat)
+ $rawFiles[] = [
+ 'name' => $_FILES['file']['name'],
+ 'tmp_name' => $_FILES['file']['tmp_name'],
+ 'error' => $_FILES['file']['error'],
+ 'size' => $_FILES['file']['size'],
];
- $origExt = $mimeToExt[$mime] ?? 'bin';
}
-$newName = 'orden_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $origExt;
+if (empty($rawFiles)) {
+ jsonError('No se recibió ningún archivo');
+}
$uploadDir = realpath(__DIR__ . '/../../uploads/media');
if (!$uploadDir) {
- // Intentar crear el directorio si no existe
@mkdir(__DIR__ . '/../../uploads/media', 0755, true);
$uploadDir = realpath(__DIR__ . '/../../uploads/media');
}
-
if (!$uploadDir || !is_writable($uploadDir)) {
- echo json_encode(['success' => false, 'error' => 'El directorio de uploads no está disponible']);
- exit;
+ jsonError('El directorio de uploads no está disponible');
}
-$destPath = $uploadDir . DIRECTORY_SEPARATOR . $newName;
+$domicilioId = isset($_POST['domicilio_id']) ? (int)$_POST['domicilio_id'] : 0;
+$pacienteId = isset($_POST['paciente_id']) ? (int)$_POST['paciente_id'] : 0;
+$om = ($domicilioId && $pacienteId) ? new OrdenMedica() : null;
+$adminId = adminId();
-if (!move_uploaded_file($file['tmp_name'], $destPath)) {
- echo json_encode(['success' => false, 'error' => 'Error al guardar el archivo en el servidor']);
- exit;
+$archivos = [];
+
+foreach ($rawFiles as $file) {
+ if ($file['error'] !== UPLOAD_ERR_OK) {
+ continue; // saltar archivos con error de upload
+ }
+
+ // Validar tamaño máximo: 10 MB
+ if ($file['size'] > 10 * 1024 * 1024) {
+ jsonError('El archivo "' . basename($file['name']) . '" supera el límite de 10 MB');
+ }
+
+ // Validar tipo MIME real
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $mime = finfo_file($finfo, $file['tmp_name']);
+ finfo_close($finfo);
+
+ if (!array_key_exists($mime, $allowedMimes)) {
+ jsonError('Tipo de archivo no permitido: ' . $mime);
+ }
+
+ // Generar nombre único y seguro
+ $origExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+ $origExt = preg_replace('/[^a-z0-9]/', '', $origExt);
+ if (!$origExt) {
+ $origExt = $allowedMimes[$mime];
+ }
+
+ $newName = 'orden_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $origExt;
+ $destPath = $uploadDir . DIRECTORY_SEPARATOR . $newName;
+
+ if (!move_uploaded_file($file['tmp_name'], $destPath)) {
+ jsonError('Error al guardar el archivo en el servidor');
+ }
+
+ $ordenId = null;
+ if ($om) {
+ // Crear registro en lab_ordenes_medicas vinculado al domicilio
+ $ordenId = $om->crear([
+ 'paciente_id' => $pacienteId,
+ 'domicilio_id' => $domicilioId,
+ 'local_file' => $newName,
+ ], $adminId);
+ }
+
+ $archivos[] = [
+ 'local_file' => $newName,
+ 'file_name' => $file['name'],
+ 'file_size' => $file['size'],
+ 'mime' => $mime,
+ 'orden_id' => $ordenId,
+ ];
}
-echo json_encode([
- 'success' => true,
- 'local_file' => $newName,
- 'file_name' => $file['name'],
- 'file_size' => $file['size'],
- 'mime' => $mime,
+if (empty($archivos)) {
+ jsonError('No se pudo procesar ningún archivo');
+}
+
+jsonOk([
+ 'archivos' => $archivos,
+ 'local_file' => $archivos[0]['local_file'], // retrocompat
]);
diff --git a/enfermero_portal.php b/enfermero_portal.php
index 47cf718..653a326 100644
--- a/enfermero_portal.php
+++ b/enfermero_portal.php
@@ -58,6 +58,7 @@ $hoy = date('Y-m-d');
.domcard.estado-en_domicilio { border-color:#20c997; }
.domcard.estado-completado { border-color:#198754; opacity:.75; }
.domcard.estado-cancelado { border-color:#dc3545; opacity:.6; }
+ .domcard.estado-reprogramado { border-color:#6f42c1; opacity:.65; }
/* ── Badges estado ── */
.badge-programado { background:#6c757d; }
@@ -66,6 +67,7 @@ $hoy = date('Y-m-d');
.badge-en_domicilio { background:#20c997; }
.badge-completado { background:#198754; }
.badge-cancelado { background:#dc3545; }
+ .badge-reprogramado { background:#6f42c1; }
/* ── Botones acción ── */
.btn-accion { font-size:.78rem; padding:.28rem .65rem; border-radius:20px; }
@@ -434,6 +436,7 @@ const ESTADOS_LABEL = {
en_domicilio: { label:'En domicilio', icon:'🏠', cls:'badge-en_domicilio' },
completado: { label:'Completado', icon:'🎉', cls:'badge-completado' },
cancelado: { label:'Cancelado', icon:'❌', cls:'badge-cancelado' },
+ reprogramado: { label:'Reprogramado', icon:'📅', cls:'badge-reprogramado' },
};
// Transiciones que el enfermero puede hacer
@@ -507,7 +510,7 @@ const portal = {
return;
}
const ACTIVOS = ['programado','confirmado','en_camino','en_domicilio'];
- const CERRADOS = ['completado','cancelado'];
+ const CERRADOS = ['completado','cancelado','reprogramado'];
const sorted = [...this._agenda].sort((a,b) =>
(a.hora_programada||'99:99').localeCompare(b.hora_programada||'99:99'));
const activos = sorted.filter(i => ACTIVOS.includes(i.domicilio_estado||'programado'));
@@ -516,9 +519,12 @@ const portal = {
let html = activos.map((item, idx) => this._cardHTML(item, idx === 0)).join('');
if (cerrados.length) {
+ const secLabel = cerrados.some(i => !['completado','cancelado'].includes(i.domicilio_estado||''))
+ ? `Cerrados / Reprogramados (${cerrados.length})`
+ : `Finalizados (${cerrados.length})`;
html += `
@@ -1225,14 +1231,16 @@ const formVer = {
placeholder="Instrucciones adicionales para el servicio…">
-
+
-
-
+
+
@@ -1336,6 +1344,9 @@ const agendaNueva = {
document.getElementById('na-valor-dom').value = '';
document.getElementById('na-valor-cop').value = '';
document.getElementById('na-fecha').value = document.getElementById('portal-fecha').value;
+ // Reset archivo adjunto
+ document.getElementById('na-orden-file').value = '';
+ document.getElementById('na-orden-preview').innerHTML = '';
document.getElementById('na-sugerencias').classList.add('d-none');
document.getElementById('na-paciente-elegido').classList.add('d-none');
document.getElementById('na-paciente-buscar').classList.remove('d-none');
@@ -1447,6 +1458,20 @@ const agendaNueva = {
document.getElementById('na-direccion').focus();
},
+ _previsualizarArchivos(files) {
+ const prev = document.getElementById('na-orden-preview');
+ prev.innerHTML = '';
+ for (const f of files) {
+ const isImg = f.type.startsWith('image/');
+ const chip = document.createElement('span');
+ chip.className = 'badge bg-light text-dark border';
+ chip.style.cssText = 'font-size:.74rem;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block';
+ chip.title = `${f.name} (${(f.size/1024).toFixed(0)} KB)`;
+ chip.innerHTML = `${esc(f.name)}`;
+ prev.appendChild(chip);
+ }
+ },
+
limpiarPaciente() {
document.getElementById('na-paciente-id').value = '';
document.getElementById('na-paciente-buscar').value = '';
@@ -1568,19 +1593,20 @@ const agendaNueva = {
const d = await r.json();
if (!d.success) throw new Error(d.error || 'Error al guardar');
- // Subir archivo adjunto si el usuario seleccionó uno
+ // Subir archivos adjuntos si el usuario seleccionó alguno
const archivoInput = document.getElementById('na-orden-file');
if (archivoInput?.files?.length && d.id) {
const fd = new FormData();
- fd.append('file', archivoInput.files[0]);
+ for (const f of archivoInput.files) fd.append('files[]', f);
fd.append('domicilio_id', d.id);
+ fd.append('paciente_id', pacId);
try {
const rFile = await fetch('api/lab/upload_orden.php', { method: 'POST', body: fd });
// Si falla el upload no bloqueamos; el domicilio ya fue creado
const dFile = await rFile.json();
- if (!dFile.success) console.warn('No se pudo adjuntar el archivo:', dFile.error);
+ if (!dFile.success) console.warn('No se pudo adjuntar archivos:', dFile.error);
} catch (eFile) {
- console.warn('Error al subir archivo:', eFile);
+ console.warn('Error al subir archivos:', eFile);
}
}
diff --git a/lab_domicilios.php b/lab_domicilios.php
index df94c62..17498a3 100644
--- a/lab_domicilios.php
+++ b/lab_domicilios.php
@@ -398,36 +398,21 @@ $domIdParam = (int)($_GET['id'] ?? 0);
-
-
-
-
![Orden]()
-
-
-
-
-
-
+
+
- Sin orden adjunta
+ Sin archivos adjuntos
-
-
-
Imagen, PDF o DOC — máx. 10 MB
+
+
Imágenes, PDF, DOC, XLS — máx. 10 MB por archivo
@@ -590,91 +575,85 @@ const modal = new bootstrap.Modal('#modalDomicilio');
let _modalRespForm = null;
let _modalAsignarEnf = null;
-// ── Estado orden en el modal ───────────────────────────────────────────────
-let _domOrderFile = null; // File pendiente de subir
-let _domCurrentLocalFile = null; // local_file actual de la orden
-let _domRemoveOrder = false; // el usuario quiere desvincularse de la orden
+// ── Estado órdenes en el modal (multi-archivo) ────────────────────────────
+let _domNewFiles = []; // File[] nuevos pendientes de subir
+let _domCurrentFiles = []; // [{orden_id, local_file}] ya guardados
+let _domRemovedOrdenIds = []; // orden_ids a desvincular al guardar
-function _domResetOrden() {
- _domOrderFile = null;
- _domCurrentLocalFile = null;
- _domRemoveOrder = false;
- document.getElementById('dom-orden-id-form').value = '';
- document.getElementById('dom-orden-input').value = '';
- _domRenderOrdenPreview();
+function _domResetOrdenes() {
+ _domNewFiles = [];
+ _domCurrentFiles = [];
+ _domRemovedOrdenIds = [];
+ document.getElementById('dom-orden-input').value = '';
+ _domRenderOrdenes();
+}
+// Alias para retrocompat con llamadas _domResetOrden()
+const _domResetOrden = _domResetOrdenes;
+
+function _domRenderOrdenes() {
+ const list = document.getElementById('dom-orden-list');
+ const empty = document.getElementById('dom-orden-empty');
+ const btn = document.getElementById('dom-orden-btn-lbl');
+ const chipStyle = 'width:72px;height:72px;border:1px solid #dee2e6;border-radius:6px;background:#f8f9fa;display:flex;align-items:center;justify-content:center;overflow:hidden';
+ const delStyle = 'position:absolute;top:-5px;right:-5px;width:18px;height:18px;border-radius:50%;background:#dc3545;color:#fff;border:none;font-size:.7rem;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0';
+
+ list.innerHTML = '';
+
+ _domCurrentFiles.forEach((f, i) => {
+ const lf = f.local_file || '';
+ const isImg = /\.(jpe?g|png|gif|webp)$/i.test(lf);
+ const thumb = isImg
+ ? `
`
+ : ``;
+ const fname = lf.split('/').pop();
+ list.insertAdjacentHTML('beforeend', `
+
+
${thumb}
+
${esc(fname)}
+
+
`);
+ });
+
+ _domNewFiles.forEach((f, i) => {
+ const isImg = f.type.startsWith('image/');
+ const thumb = isImg
+ ? `
`
+ : ``;
+ list.insertAdjacentHTML('beforeend', `
+
+
${thumb}
+
${esc(f.name)}
+
nuevo
+
+
`);
+ });
+
+ const total = _domCurrentFiles.length + _domNewFiles.length;
+ empty.classList.toggle('d-none', total > 0);
+ btn.textContent = total > 0 ? 'Agregar más archivos' : 'Adjuntar archivos';
}
-function _domRenderOrdenPreview() {
- const preview = document.getElementById('dom-orden-preview');
- const empty = document.getElementById('dom-orden-empty');
- const img = document.getElementById('dom-orden-img');
- const icon = document.getElementById('dom-orden-icon');
- const fname = document.getElementById('dom-orden-fname');
- const fsize = document.getElementById('dom-orden-fsize');
- const btnQuitar = document.getElementById('dom-orden-quitar');
- const btnLbl = document.getElementById('dom-orden-btn-lbl');
-
- if (_domOrderFile) {
- // Archivo local seleccionado aún no subido
- const isImg = _domOrderFile.type.startsWith('image/');
- if (isImg) {
- img.src = URL.createObjectURL(_domOrderFile);
- img.style.display = '';
- icon.classList.add('d-none');
- } else {
- img.style.display = 'none';
- icon.classList.remove('d-none');
+function domSeleccionarOrdenFiles(input) {
+ for (const file of Array.from(input.files)) {
+ if (file.size > 10 * 1024 * 1024) {
+ alert(`"${file.name}" supera los 10 MB permitidos`);
+ continue;
}
- fname.textContent = _domOrderFile.name;
- fsize.textContent = (_domOrderFile.size / 1024).toFixed(1) + ' KB — pendiente de guardar';
- preview.classList.remove('d-none');
- empty.classList.add('d-none');
- btnQuitar.classList.remove('d-none');
- btnLbl.textContent = 'Cambiar archivo';
- } else if (_domCurrentLocalFile && !_domRemoveOrder) {
- // Archivo ya guardado en servidor
- const isImg = /\.(jpe?g|png|gif|webp)$/i.test(_domCurrentLocalFile);
- if (isImg) {
- img.src = 'uploads/media/' + encodeURIComponent(_domCurrentLocalFile);
- img.style.display = '';
- icon.classList.add('d-none');
- } else {
- img.style.display = 'none';
- icon.classList.remove('d-none');
- }
- fname.textContent = _domCurrentLocalFile;
- fsize.textContent = 'Archivo guardado';
- preview.classList.remove('d-none');
- empty.classList.add('d-none');
- btnQuitar.classList.remove('d-none');
- btnLbl.textContent = 'Cambiar archivo';
- } else {
- preview.classList.add('d-none');
- empty.classList.remove('d-none');
- btnQuitar.classList.add('d-none');
- btnLbl.textContent = 'Adjuntar orden';
+ _domNewFiles.push(file);
}
+ input.value = '';
+ _domRenderOrdenes();
}
-function domSeleccionarOrdenFile(input) {
- const file = input.files[0];
- if (!file) return;
- if (file.size > 10 * 1024 * 1024) {
- alert('El archivo supera los 10 MB permitidos');
- input.value = '';
- return;
- }
- _domOrderFile = file;
- _domRemoveOrder = false;
- _domRenderOrdenPreview();
+function domQuitarExistente(i) {
+ const removed = _domCurrentFiles.splice(i, 1)[0];
+ if (removed?.orden_id) _domRemovedOrdenIds.push(removed.orden_id);
+ _domRenderOrdenes();
}
-function domQuitarOrden() {
- _domOrderFile = null;
- _domRemoveOrder = true;
- document.getElementById('dom-orden-id-form').value = '';
- document.getElementById('dom-orden-input').value = '';
- _domRenderOrdenPreview();
+function domQuitarNuevo(i) {
+ _domNewFiles.splice(i, 1);
+ _domRenderOrdenes();
}
const COLOR_DOM = { programado:'secondary', confirmado:'info', en_camino:'primary', en_domicilio:'warning', completado:'success', cancelado:'danger', reprogramado:'dark' };
const PUEDE_ESCRIBIR = = $puedeEscribir ? 'true' : 'false' ?>;
@@ -1255,11 +1234,15 @@ async function editarDomicilio(id) {
document.getElementById('dom-copago-lab').value = dom.copago_laboratorio || '';
document.getElementById('dom-notas').value = dom.notas_admin || '';
- // Orden médica adjunta
- _domResetOrden();
- document.getElementById('dom-orden-id-form').value = dom.orden_id || '';
- _domCurrentLocalFile = dom.orden_local_file || null;
- _domRenderOrdenPreview();
+ // Órdenes médicas adjuntas (multi-archivo)
+ _domResetOrdenes();
+ let archivosExistentes = dom.archivos_ordenes || [];
+ if (!archivosExistentes.length && dom.orden_local_file) {
+ // Retrocompat: domicilio viejo con solo orden_id FK
+ archivosExistentes = [{ orden_id: dom.orden_id || null, local_file: dom.orden_local_file }];
+ }
+ _domCurrentFiles = archivosExistentes;
+ _domRenderOrdenes();
modal.show();
}
@@ -1277,57 +1260,52 @@ async function guardarDomicilio() {
const datos = Object.fromEntries(new FormData(form).entries());
if (!datos.id) delete datos.id;
- // Tomar orden_id del campo oculto real (no el parámetro URL)
- datos.orden_id = document.getElementById('dom-orden-id-form').value || null;
+ delete datos.orden_id; // archivos se vinculan por domicilio_id; no pasamos FK
delete datos.orden_id_url;
- // ── Gestión del archivo de orden médica ──────────────────────────────
- if (_domRemoveOrder) {
- datos.orden_id = null;
- } else if (_domOrderFile) {
- // Subir el archivo primero
- const fd = new FormData();
- fd.append('file', _domOrderFile);
- const upR = await fetch('api/lab/upload_orden.php', { method:'POST', body: fd });
- const upD = await upR.json();
- if (!upD.success) {
- mostrarToast('Error subiendo el archivo: ' + (upD.error || 'desconocido'), 'danger');
- return;
- }
- // Actualizar o crear la orden médica
- const orId = document.getElementById('dom-orden-id-form').value;
- const orPayload = orId
- ? { id: parseInt(orId), local_file: upD.local_file }
- : { paciente_id: parseInt(pacienteId), local_file: upD.local_file };
- const orR = await fetch('api/lab/save_orden.php', {
- method:'POST', headers:{'Content-Type':'application/json'},
- body: JSON.stringify(orPayload),
- });
- const orD = await orR.json();
- if (!orD.success) {
- mostrarToast('Error guardando la orden: ' + (orD.error || 'desconocido'), 'danger');
- return;
- }
- datos.orden_id = orD.id ?? orD.data?.id ?? (parseInt(orId) || null);
+ // Desvincular archivos que el usuario eliminó
+ for (const ordenId of _domRemovedOrdenIds) {
+ try {
+ await fetch('api/lab/save_orden.php', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id: ordenId, domicilio_id: null }),
+ });
+ } catch (_e) { /* silencioso */ }
}
- // ── Guardar domicilio ────────────────────────────────────────────────
- if (!datos.orden_id) delete datos.orden_id;
+ _domRemovedOrdenIds = [];
+ // Guardar domicilio
const r = await fetch('api/lab/save_domicilio.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(datos),
});
const d = await r.json();
- if (d.success) {
- modal.hide();
- mostrarToast(d.message, 'success');
- const editadoId = datos.id ? parseInt(datos.id) : (d.id || null);
- _domResetOrden();
- await cargarLista(paginaActual);
- if (editadoId) verDomicilio(editadoId);
- } else {
+ if (!d.success) {
mostrarToast(d.error||'Error', 'danger');
+ return;
}
+
+ const domicilioId = datos.id ? parseInt(datos.id) : (d.id || null);
+
+ // Subir nuevos archivos vinculados al domicilio
+ if (_domNewFiles.length && domicilioId) {
+ const fd = new FormData();
+ for (const f of _domNewFiles) fd.append('files[]', f);
+ fd.append('domicilio_id', domicilioId);
+ fd.append('paciente_id', pacienteId);
+ try {
+ const upR = await fetch('api/lab/upload_orden.php', { method: 'POST', body: fd });
+ const upD = await upR.json();
+ if (!upD.success) mostrarToast('No se pudieron subir algunos archivos: ' + (upD.error || ''), 'warning');
+ } catch (_e) { mostrarToast('Error al subir archivos', 'warning'); }
+ }
+ _domNewFiles = [];
+
+ modal.hide();
+ mostrarToast(d.message, 'success');
+ _domResetOrdenes();
+ await cargarLista(paginaActual);
+ if (domicilioId) verDomicilio(domicilioId);
}
// Autocompletado de paciente