feat: multi-archivo órdenes visible en enfermero, domicilios y órdenes médicas
- OrdenMedica::filtrarCampos(): agrega domicilio_id al whitelist - Domicilio::obtener(): consulta secundaria archivos_ordenes[] con fallback - Enfermera::agenda(): bulk-query archivos_ordenes[] por domicilio_id - api/lab/link_ordenes_domicilio.php: API POST vincula orden_ids a domicilio_id - conversations.php guardar(): trackea allOrdenIds[], llama link API post-save - lab_domicilios.php: renderOrdenesDetail() multi-archivo en detalle - enfermero_portal.php: renderOrdenesEnfermero() multi-archivo en cards - lab_ordenes.php: botón 'Ver domicilio' cuando domicilio_id está vinculado - migrations/: ALTER TABLE lab_ordenes_medicas ADD domicilio_id + índice
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /api/lab/link_ordenes_domicilio.php
|
||||
* Vincula múltiples órdenes médicas a un domicilio (domicilio_id).
|
||||
* Body JSON: { domicilio_id: int, orden_ids: int[] }
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($input['domicilio_id']) || empty($input['orden_ids']) || !is_array($input['orden_ids'])) {
|
||||
echo json_encode(['success' => false, 'error' => 'domicilio_id y orden_ids[] requeridos']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$domicilioId = (int)$input['domicilio_id'];
|
||||
$ordenIds = array_map('intval', $input['orden_ids']);
|
||||
$ordenIds = array_filter($ordenIds, fn($v) => $v > 0);
|
||||
|
||||
if (!$domicilioId || empty($ordenIds)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Valores inválidos']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$placeholders = implode(',', array_fill(0, count($ordenIds), '?'));
|
||||
$params = array_merge([$domicilioId], array_values($ordenIds));
|
||||
|
||||
$db->execute(
|
||||
"UPDATE lab_ordenes_medicas SET domicilio_id = ? WHERE id IN ($placeholders)",
|
||||
$params
|
||||
);
|
||||
|
||||
echo json_encode(['success' => true, 'vinculadas' => count($ordenIds)]);
|
||||
} catch (Exception $e) {
|
||||
error_log('[link_ordenes_domicilio] ' . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'error' => 'Error interno']);
|
||||
}
|
||||
@@ -111,7 +111,7 @@ class Domicilio {
|
||||
* Domicilio por ID con todos los datos relacionados.
|
||||
*/
|
||||
public function obtener(int $id): ?array {
|
||||
return $this->db->fetch("
|
||||
$dom = $this->db->fetch("
|
||||
SELECT
|
||||
d.*,
|
||||
p.nombre_completo AS paciente_nombre,
|
||||
@@ -133,7 +133,23 @@ class Domicilio {
|
||||
LEFT JOIN lab_enfermeras e ON e.id = a.enfermera_id
|
||||
LEFT JOIN admin_users adm ON adm.id = d.creado_por
|
||||
WHERE d.id = ?
|
||||
", [$id]) ?: null;
|
||||
", [$id]);
|
||||
if (!$dom) return null;
|
||||
|
||||
// Todos los archivos de órdenes vinculados a este domicilio
|
||||
$archivos = $this->db->fetchAll(
|
||||
"SELECT id AS orden_id, local_file
|
||||
FROM lab_ordenes_medicas
|
||||
WHERE domicilio_id = ? AND local_file IS NOT NULL
|
||||
ORDER BY id ASC",
|
||||
[$id]
|
||||
);
|
||||
// Fallback para domicilios creados antes de la migración (usan orden_id FK)
|
||||
if (empty($archivos) && !empty($dom['orden_local_file'])) {
|
||||
$archivos = [['orden_id' => $dom['orden_id'], 'local_file' => $dom['orden_local_file']]];
|
||||
}
|
||||
$dom['archivos_ordenes'] = $archivos;
|
||||
return $dom;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,7 +91,7 @@ class Enfermera {
|
||||
*/
|
||||
public function agenda(int $enfermeraId, string $fecha = ''): array {
|
||||
$fecha = $fecha ?: date('Y-m-d');
|
||||
return $this->db->fetchAll("
|
||||
$rows = $this->db->fetchAll("
|
||||
SELECT
|
||||
d.id AS domicilio_id,
|
||||
d.fecha_programada,
|
||||
@@ -151,6 +151,52 @@ class Enfermera {
|
||||
AND d.fecha_programada = ?
|
||||
ORDER BY d.hora_programada ASC
|
||||
", [$enfermeraId, $fecha]);
|
||||
|
||||
// Bulk-load todos los archivos de órdenes vinculados a estos domicilios
|
||||
$domIds = array_values(array_filter(array_column($rows, 'domicilio_id')));
|
||||
if (!empty($domIds)) {
|
||||
$ph = implode(',', array_fill(0, count($domIds), '?'));
|
||||
$archivos = $this->db->fetchAll(
|
||||
"SELECT domicilio_id, id AS orden_id, local_file
|
||||
FROM lab_ordenes_medicas
|
||||
WHERE domicilio_id IN ($ph) AND local_file IS NOT NULL
|
||||
ORDER BY id ASC",
|
||||
$domIds
|
||||
);
|
||||
$archivosPorDom = [];
|
||||
foreach ($archivos as $a) {
|
||||
$archivosPorDom[$a['domicilio_id']][] = $a;
|
||||
}
|
||||
foreach ($rows as &$row) {
|
||||
$did = $row['domicilio_id'];
|
||||
if (!empty($archivosPorDom[$did])) {
|
||||
$row['archivos_ordenes'] = $archivosPorDom[$did];
|
||||
} elseif (!empty($row['orden_local_file'])) {
|
||||
// Fallback: domicilio creado antes de la migración
|
||||
$row['archivos_ordenes'] = [[
|
||||
'orden_id' => $row['orden_id_display'] ?? $row['orden_id'],
|
||||
'local_file' => $row['orden_local_file'],
|
||||
]];
|
||||
} else {
|
||||
$row['archivos_ordenes'] = [];
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
} else {
|
||||
foreach ($rows as &$row) {
|
||||
if (!empty($row['orden_local_file'])) {
|
||||
$row['archivos_ordenes'] = [[
|
||||
'orden_id' => $row['orden_id_display'] ?? $row['orden_id'],
|
||||
'local_file' => $row['orden_local_file'],
|
||||
]];
|
||||
} else {
|
||||
$row['archivos_ordenes'] = [];
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -306,7 +306,7 @@ class OrdenMedica {
|
||||
private function filtrarCampos(array $datos): array {
|
||||
$permitidos = [
|
||||
'paciente_id', 'conversation_id', 'whatsapp_media_id', 'local_file',
|
||||
'estado', 'medico_nombre', 'medico_registro', 'fecha_orden',
|
||||
'domicilio_id', 'estado', 'medico_nombre', 'medico_registro', 'fecha_orden',
|
||||
'diagnostico', 'examenes_solicitados', 'requiere_ayuno',
|
||||
'horas_ayuno', 'indicaciones', 'revisada_por', 'revisada_at',
|
||||
'autorizada_por', 'autorizada_at', 'comentario_revision', 'notas_admin',
|
||||
|
||||
+15
-2
@@ -7439,6 +7439,7 @@ const labDomicilio = (() => {
|
||||
try {
|
||||
const convIdNum = (_convId && !String(_convId).startsWith('__user:'))
|
||||
? parseInt(_convId) : null;
|
||||
const allOrdenIds = [];
|
||||
for (let _fi = 0; _fi < _localFiles.length; _fi++) {
|
||||
const orRes = await fetch('api/lab/crear_desde_whatsapp.php', {
|
||||
method: 'POST',
|
||||
@@ -7453,10 +7454,12 @@ const labDomicilio = (() => {
|
||||
}),
|
||||
});
|
||||
const orData = await orRes.json();
|
||||
if (orData.success && orData.orden_id && _fi === 0) {
|
||||
datos.orden_id = orData.orden_id;
|
||||
if (orData.success && orData.orden_id) {
|
||||
allOrdenIds.push(orData.orden_id);
|
||||
if (_fi === 0) datos.orden_id = orData.orden_id;
|
||||
}
|
||||
}
|
||||
datos._allOrdenIds = allOrdenIds;
|
||||
} catch (_) { /* no bloquea */ }
|
||||
}
|
||||
|
||||
@@ -7469,6 +7472,16 @@ const labDomicilio = (() => {
|
||||
const d = await r.json();
|
||||
|
||||
if (d.success) {
|
||||
// Vincular todas las órdenes al domicilio recién creado
|
||||
const _oids = datos._allOrdenIds;
|
||||
if (Array.isArray(_oids) && _oids.length > 0 && d.id) {
|
||||
fetch('api/lab/link_ordenes_domicilio.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ domicilio_id: d.id, orden_ids: _oids }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// Actualizar _pacNombre si el usuario editó el nombre en el input
|
||||
const _editedNombre = fv('labdom-pac-nombre');
|
||||
if (_editedNombre) _pacNombre = _editedNombre;
|
||||
|
||||
+29
-17
@@ -531,23 +531,8 @@ const portal = {
|
||||
<!-- Cobro y pago -->
|
||||
${cobroHtml}
|
||||
|
||||
<!-- Orden médica -->
|
||||
${item.orden_local_file ? `
|
||||
<div class="mt-2 mb-2">
|
||||
<!-- Vista previa solo para imágenes (falla silenciosamente para PDFs) -->
|
||||
<img src="${resOrden(item.orden_local_file)}"
|
||||
id="ord-img-${item.orden_id}"
|
||||
style="max-width:100%;max-height:180px;border-radius:8px;border:1px solid #ddd;cursor:zoom-in;display:block"
|
||||
alt="Orden"
|
||||
onclick="window.open('${resOrden(item.orden_local_file)}','_blank')"
|
||||
onerror="this.style.display='none'">
|
||||
<!-- Botón siempre visible: funciona para imágenes Y PDFs -->
|
||||
<a href="${resOrden(item.orden_local_file)}" target="_blank"
|
||||
class="btn btn-sm btn-outline-danger w-100 mt-1">
|
||||
<i class="fas fa-file-medical me-1"></i>
|
||||
Ver / Descargar orden #${item.orden_id_display || item.orden_id || ''}
|
||||
</a>
|
||||
</div>` : ''}
|
||||
<!-- Órdenes médicas (multi-archivo) -->
|
||||
${renderOrdenesEnfermero(item)}
|
||||
<!-- Servicios extra registrados -->
|
||||
${seSuf ? `<div class="d-flex flex-wrap gap-1 mb-2">${seSuf}</div>` : ''}
|
||||
|
||||
@@ -708,6 +693,33 @@ function resOrden(f) {
|
||||
return `api/lab/descargar_orden.php?file=${encodeURIComponent(f)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza todas las órdenes médicas adjuntas a un domicilio.
|
||||
* Soporta multi-archivo (archivos_ordenes[]) con fallback a orden_local_file.
|
||||
*/
|
||||
function renderOrdenesEnfermero(item) {
|
||||
const archivos = item.archivos_ordenes || [];
|
||||
if (!archivos.length) return '';
|
||||
return archivos.map(a => {
|
||||
const lf = a.local_file || '';
|
||||
const url = resOrden(lf);
|
||||
const oid = a.orden_id || '';
|
||||
if (!lf) return '';
|
||||
const isImg = /\.(jpe?g|png|gif|webp)$/i.test(lf);
|
||||
return `<div class="mt-2 mb-2">
|
||||
${isImg ? `<img src="${url}"
|
||||
style="max-width:100%;max-height:180px;border-radius:8px;border:1px solid #ddd;cursor:zoom-in;display:block"
|
||||
alt="Orden" onclick="window.open('${url}','_blank')"
|
||||
onerror="this.style.display='none'">` : ''}
|
||||
<a href="${url}" target="_blank"
|
||||
class="btn btn-sm btn-outline-danger w-100 mt-1">
|
||||
<i class="fas fa-file-medical me-1"></i>
|
||||
Ver / Descargar orden${oid ? ' #' + oid : ''}
|
||||
</a>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
portal.init();
|
||||
agendaNueva.init();
|
||||
|
||||
+45
-28
@@ -685,34 +685,7 @@ async function verDomicilio(id) {
|
||||
</div>`}
|
||||
</div>
|
||||
<!-- Orden médica -->
|
||||
${dom.orden_id ? `
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted text-uppercase">Orden médica</label>
|
||||
${(() => {
|
||||
const lf = dom.orden_local_file || '';
|
||||
const isImg = /\.(jpe?g|png|gif|webp)$/i.test(lf);
|
||||
const url = lf ? 'uploads/media/' + encodeURIComponent(lf) : '';
|
||||
if (lf && isImg) return `
|
||||
<a href="${url}" target="_blank" title="Ver imagen completa">
|
||||
<img src="${url}" alt="Orden médica"
|
||||
style="max-width:100%;max-height:180px;object-fit:contain;border-radius:6px;border:1px solid #dee2e6;display:block;cursor:zoom-in">
|
||||
</a>
|
||||
<a href="lab_ordenes.php?id=${dom.orden_id}" class="d-block small mt-1 text-muted">
|
||||
<i class="fas fa-external-link-alt me-1"></i>Ver ficha orden #${dom.orden_id}
|
||||
</a>`;
|
||||
if (lf) return `
|
||||
<a href="api/lab/descargar_orden.php?file=${encodeURIComponent(lf)}" target="_blank"
|
||||
class="btn btn-sm btn-outline-secondary w-100 mb-1">
|
||||
<i class="fas fa-file-download me-1"></i>Abrir / Descargar archivo
|
||||
</a>
|
||||
<a href="lab_ordenes.php?id=${dom.orden_id}" class="d-block small text-muted">
|
||||
<i class="fas fa-external-link-alt me-1"></i>Ver ficha orden #${dom.orden_id}
|
||||
</a>`;
|
||||
return `<a href="lab_ordenes.php?id=${dom.orden_id}" class="d-block small">
|
||||
<i class="fas fa-file-medical me-1"></i>Ver orden #${dom.orden_id}
|
||||
</a>`;
|
||||
})()}
|
||||
</div>` : ''}
|
||||
${renderOrdenesDetail(dom)}
|
||||
<!-- Botones de flujo -->
|
||||
<div class="d-flex gap-2 flex-wrap">${btns}</div>
|
||||
</div>
|
||||
@@ -998,6 +971,50 @@ function verHoy() {
|
||||
|
||||
// ── Utils ──────────────────────────────────────────────────────────────────
|
||||
const esc = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c]));
|
||||
|
||||
/**
|
||||
* Renderiza el bloque de órdenes médicas (multi-archivo) en el detalle de domicilio.
|
||||
*/
|
||||
function renderOrdenesDetail(dom) {
|
||||
const archivos = dom.archivos_ordenes || [];
|
||||
if (!archivos.length && !dom.orden_id) return '';
|
||||
// Si no hay archivos cargados pero sí hay orden_id, mostramos solo el link
|
||||
if (!archivos.length) {
|
||||
return `<div class="mb-3">
|
||||
<label class="form-label small text-muted text-uppercase">Órdenes médicas</label>
|
||||
<a href="lab_ordenes.php?id=${dom.orden_id}" class="d-block small">
|
||||
<i class="fas fa-file-medical me-1"></i>Ver orden #${dom.orden_id}
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
const items = archivos.map(a => {
|
||||
const lf = a.local_file || '';
|
||||
const isImg = /\.(jpe?g|png|gif|webp)$/i.test(lf);
|
||||
const url = lf ? ('uploads/media/' + encodeURIComponent(lf)) : '';
|
||||
const ficha = `<a href="lab_ordenes.php?id=${a.orden_id}" class="d-block small text-muted mt-1">
|
||||
<i class="fas fa-external-link-alt me-1"></i>Orden #${a.orden_id}
|
||||
</a>`;
|
||||
if (lf && isImg) return `
|
||||
<div class="mb-2">
|
||||
<a href="${url}" target="_blank" title="Ver imagen completa">
|
||||
<img src="${url}" alt="Orden médica"
|
||||
style="max-width:100%;max-height:180px;object-fit:contain;border-radius:6px;border:1px solid #dee2e6;display:block;cursor:zoom-in">
|
||||
</a>${ficha}
|
||||
</div>`;
|
||||
if (lf) return `
|
||||
<div class="mb-2">
|
||||
<a href="api/lab/descargar_orden.php?file=${encodeURIComponent(lf)}" target="_blank"
|
||||
class="btn btn-sm btn-outline-secondary w-100 mb-1">
|
||||
<i class="fas fa-file-download me-1"></i>Abrir / Descargar archivo
|
||||
</a>${ficha}
|
||||
</div>`;
|
||||
return `<div class="mb-2">${ficha}</div>`;
|
||||
}).join('');
|
||||
return `<div class="mb-3">
|
||||
<label class="form-label small text-muted text-uppercase">Órdenes médicas (${archivos.length})</label>
|
||||
${items}
|
||||
</div>`;
|
||||
}
|
||||
function mostrarToast(msg, tipo='success') {
|
||||
const div = document.createElement('div');
|
||||
div.className = `alert alert-${tipo} alert-dismissible position-fixed bottom-0 end-0 m-3`;
|
||||
|
||||
@@ -316,6 +316,14 @@ async function verOrden(id) {
|
||||
<div class="bg-light p-2 rounded small">${esc(o.indicaciones).replace(/\n/g,'<br>')}</div>
|
||||
</div>` : ''}
|
||||
|
||||
${o.domicilio_id ? `
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted text-uppercase">Domicilio vinculado</label>
|
||||
<a href="lab_domicilios.php?id=${o.domicilio_id}" class="btn btn-sm btn-outline-primary w-100">
|
||||
<i class="fas fa-house-medical me-1"></i>Ver domicilio #${o.domicilio_id}
|
||||
</a>
|
||||
</div>` : ''}
|
||||
|
||||
${o.comentario_revision ? `
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted text-uppercase">Comentario revisión</label>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Migración: agrega domicilio_id a lab_ordenes_medicas
|
||||
-- Permite vincular múltiples órdenes a un mismo domicilio (multi-archivo).
|
||||
-- Se omite FK para evitar dependencia circular con lab_domicilios ↔ lab_ordenes_medicas.
|
||||
|
||||
ALTER TABLE `lab_ordenes_medicas`
|
||||
ADD COLUMN `domicilio_id` INT(11) DEFAULT NULL
|
||||
COMMENT 'FK a lab_domicilios.id — domicilio al que pertenece esta orden/archivo'
|
||||
AFTER `paciente_id`;
|
||||
|
||||
ALTER TABLE `lab_ordenes_medicas`
|
||||
ADD INDEX `idx_om_domicilio_id` (`domicilio_id`);
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* Migración: agrega domicilio_id a lab_ordenes_medicas
|
||||
* Ejecutar en el servidor: php migrations/run_20260324_ordenes_domicilio_id.php
|
||||
*/
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../classes/Database.php';
|
||||
|
||||
$db = Database::getInstance()->getConnection();
|
||||
$ok = 0;
|
||||
$err = 0;
|
||||
|
||||
$statements = [
|
||||
"ALTER TABLE `lab_ordenes_medicas`
|
||||
ADD COLUMN `domicilio_id` INT(11) DEFAULT NULL
|
||||
COMMENT 'FK a lab_domicilios.id — domicilio al que pertenece esta orden/archivo'
|
||||
AFTER `paciente_id`",
|
||||
"ALTER TABLE `lab_ordenes_medicas`
|
||||
ADD INDEX `idx_om_domicilio_id` (`domicilio_id`)",
|
||||
];
|
||||
|
||||
foreach ($statements as $sql) {
|
||||
try {
|
||||
$db->exec($sql);
|
||||
echo "✅ OK: " . substr(trim($sql), 0, 80) . "\n";
|
||||
$ok++;
|
||||
} catch (PDOException $e) {
|
||||
// Ignorar si la columna/índice ya existe
|
||||
if (str_contains($e->getMessage(), 'Duplicate column') || str_contains($e->getMessage(), 'Duplicate key')) {
|
||||
echo "⚠️ Ya existe (omitido): " . substr(trim($sql), 0, 60) . "\n";
|
||||
} else {
|
||||
echo "❌ ERROR: " . $e->getMessage() . "\n";
|
||||
$err++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nMigración completada: {$ok} OK, {$err} errores.\n";
|
||||
Reference in New Issue
Block a user