up
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Subir archivo de orden médica desde el modal de agendamiento
|
||||
*
|
||||
* POST (multipart/form-data) con campo 'file'
|
||||
* Devuelve: { success, local_file, file_name, file_size, mime }
|
||||
*/
|
||||
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) {
|
||||
$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',
|
||||
];
|
||||
|
||||
$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) $origExt = 'bin';
|
||||
|
||||
$newName = 'orden_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $origExt;
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
$destPath = $uploadDir . DIRECTORY_SEPARATOR . $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,
|
||||
'local_file' => $newName,
|
||||
'file_name' => $file['name'],
|
||||
'file_size' => $file['size'],
|
||||
'mime' => $mime,
|
||||
]);
|
||||
+188
-37
@@ -838,6 +838,22 @@ if (!isset($_SESSION['user_id'])) {
|
||||
.conversation-item.unread { background: rgba(255, 248, 220, 0.9); }
|
||||
.unread-badge { margin-left: 8px; font-size: 12px; }
|
||||
|
||||
/* Tooltip «Copiado» al hacer doble clic en un mensaje */
|
||||
.copy-feedback {
|
||||
position: absolute;
|
||||
top: -26px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.72);
|
||||
color: #fff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px; font-weight: 600;
|
||||
white-space: nowrap; pointer-events: none;
|
||||
animation: cfadeIn .15s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
@keyframes cfadeIn { from { opacity:0; transform: translateX(-50%) translateY(4px); } to { opacity:1; transform: translateX(-50%) translateY(0); } }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -2100,6 +2116,34 @@ if (!isset($_SESSION['user_id'])) {
|
||||
// Expose small helper to other methods
|
||||
this._updateSendMicVisibility = updateSendMicVisibility;
|
||||
|
||||
// Doble clic en burbuja de mensaje → copiar texto al portapapeles
|
||||
const chatConvContainer = document.getElementById('chat-conversations');
|
||||
if (chatConvContainer) {
|
||||
chatConvContainer.addEventListener('dblclick', (e) => {
|
||||
const bubble = e.target.closest('.message-bubble');
|
||||
if (!bubble) return;
|
||||
const contentEl = bubble.querySelector('.message-content');
|
||||
if (!contentEl) return;
|
||||
const text = (contentEl.innerText || contentEl.textContent || '').trim();
|
||||
if (!text) return;
|
||||
const doFeedback = () => this._showCopyToast(bubble);
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(doFeedback).catch(() => {
|
||||
try { document.execCommand('copy'); doFeedback(); } catch(e2) {}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const sel = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(contentEl);
|
||||
sel.removeAllRanges(); sel.addRange(range);
|
||||
document.execCommand('copy');
|
||||
sel.removeAllRanges();
|
||||
doFeedback();
|
||||
} catch(e2) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// File input selected -> show send button
|
||||
const fileInput = document.getElementById('file-input');
|
||||
@@ -4002,7 +4046,7 @@ if (!isset($_SESSION['user_id'])) {
|
||||
<div class="message-actions mt-1">
|
||||
<button class="btn btn-sm btn-link" onclick="chat.promptReply('${window.escapeHtml(msg.message_id || msg.id || '')}')" title="Responder"><i class="fas fa-reply"></i></button>
|
||||
<button class="btn btn-sm btn-link" onclick="chat.openReactionPicker(event, '${window.escapeHtml(msg.message_id || msg.id || '')}')" title="Reaccionar"><i class="far fa-grin"></i></button>
|
||||
${(msg.message_type === 'image' && msg.direction !== 'outgoing') ? `<button class="btn btn-sm btn-link text-primary" data-msg-id="${msg.id||0}" data-local-file="${msg.local_file||msg.local_thumb||''}" data-conv-id="${chat ? chat.currentConversationId : 0}" onclick="labDomicilio.abrir(+this.dataset.msgId, this.dataset.localFile, +this.dataset.convId)" title="Agendar domicilio (con imagen)"><i class="fas fa-house-medical"></i></button>` : ''}${window._labAddrBtn ? window._labAddrBtn(msg, chat ? chat.currentConversationId : 0) : ''}
|
||||
${(['image','document','video','audio','sticker'].includes(msg.message_type) && msg.direction !== 'outgoing') ? `<button class="btn btn-sm btn-link text-primary" data-msg-id="${msg.id||0}" data-local-file="${msg.local_file||msg.local_thumb||''}" data-conv-id="${chat ? chat.currentConversationId : 0}" onclick="labDomicilio.abrir(+this.dataset.msgId, this.dataset.localFile, +this.dataset.convId)" title="Agendar domicilio"><i class="fas fa-house-medical"></i></button>` : ''}${window._labAddrBtn ? window._labAddrBtn(msg, chat ? chat.currentConversationId : 0) : ''}
|
||||
</div>
|
||||
<div class="message-time">${time} ${msg.direction === 'outgoing' ? `<span class="message-status">${statusIcon}</span>` : ''}</div>
|
||||
</div>
|
||||
@@ -5264,6 +5308,16 @@ if (!isset($_SESSION['user_id'])) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
_showCopyToast(bubble) {
|
||||
const existing = bubble.querySelector('.copy-feedback');
|
||||
if (existing) existing.remove();
|
||||
const tip = document.createElement('span');
|
||||
tip.className = 'copy-feedback';
|
||||
tip.textContent = '✓ Copiado';
|
||||
bubble.appendChild(tip);
|
||||
setTimeout(() => { if (tip.parentNode) tip.remove(); }, 1800);
|
||||
}
|
||||
|
||||
// Insertar mensaje saliente en la vista inmediatamente (simular envío)
|
||||
addMessageToView(content, type = 'outgoing', options = {}) {
|
||||
const msg = {
|
||||
@@ -6543,13 +6597,30 @@ if (!isset($_SESSION['user_id'])) {
|
||||
<!-- ── FORMULARIO ── -->
|
||||
<div id="labdom-form-panel" class="modal-body pb-2">
|
||||
|
||||
<!-- Imagen adjunta -->
|
||||
<div id="labdom-img-prev" class="mb-3 d-flex align-items-center gap-3 p-2 bg-light rounded border" style="display:none!important">
|
||||
<img id="labdom-img-el" src="" class="rounded border" style="max-height:80px;max-width:110px;object-fit:cover">
|
||||
<div>
|
||||
<p class="mb-0 fw-semibold small text-secondary">Orden médica adjunta</p>
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle"><i class="fas fa-image me-1"></i>Foto de la conversación</span>
|
||||
<!-- Orden médica (adjunta / agregar / cambiar) -->
|
||||
<div class="mb-3">
|
||||
<div id="labdom-img-prev" class="d-flex align-items-center gap-3 p-2 bg-light rounded border" style="display:none">
|
||||
<div id="labdom-file-thumb" class="flex-shrink-0">
|
||||
<img id="labdom-img-el" src="" class="rounded border" style="max-height:80px;max-width:110px;object-fit:cover">
|
||||
<div id="labdom-file-icon" class="rounded border bg-white text-center" style="width:80px;height:80px;display:none;align-items:center;justify-content:center"></div>
|
||||
</div>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<p class="mb-1 fw-semibold small text-secondary"><i class="fas fa-paperclip me-1"></i>Orden médica adjunta</p>
|
||||
<small id="labdom-file-name" class="text-muted d-block mb-2" style="max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></small>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2" onclick="labDomicilio._cambiarOrden()" title="Cambiar archivo">
|
||||
<i class="fas fa-exchange-alt me-1"></i>Cambiar
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger py-0 px-2" onclick="labDomicilio._eliminarOrden()" title="Eliminar orden">
|
||||
<i class="fas fa-trash me-1"></i>Eliminar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="labdom-btn-add-orden" class="btn btn-sm btn-outline-primary w-100 py-1" onclick="labDomicilio._cambiarOrden()" style="display:none">
|
||||
<i class="fas fa-paperclip me-1"></i>Adjuntar orden médica <span class="fw-normal text-muted">(opcional)</span>
|
||||
</button>
|
||||
<input type="file" id="labdom-orden-input" accept="image/*,.pdf,.doc,.docx" style="display:none">
|
||||
</div>
|
||||
|
||||
<!-- ── TIPO (particular/seguro) — PRIMERO y llamativo ── -->
|
||||
@@ -6783,13 +6854,14 @@ if (!isset($_SESSION['user_id'])) {
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
const labDomicilio = (() => {
|
||||
// Estado interno
|
||||
let _msgId = null;
|
||||
let _localFile = null;
|
||||
let _convId = null;
|
||||
let _pacienteId = null;
|
||||
let _pacNombre = '';
|
||||
let _histDirs = []; // direcciones únicas previas del paciente
|
||||
let _bsModal = null;
|
||||
let _msgId = null;
|
||||
let _localFile = null;
|
||||
let _newOrderFile = null; // nuevo archivo de orden cargado desde el modal
|
||||
let _convId = null;
|
||||
let _pacienteId = null;
|
||||
let _pacNombre = '';
|
||||
let _histDirs = []; // direcciones únicas previas del paciente
|
||||
let _bsModal = null;
|
||||
|
||||
// Sesiones minimizadas (múltiples chats en paralelo)
|
||||
let _sesiones = []; // [{ id, chatUserId, chatName, chatPhone, convId, pacienteId, pacNombre, histDirs, msgId, localFile, cardVisible, pacInfo, fields }]
|
||||
@@ -6887,12 +6959,7 @@ const labDomicilio = (() => {
|
||||
mostrarFormulario();
|
||||
labDomicilio._resetFull();
|
||||
|
||||
if (_localFile) {
|
||||
$('labdom-img-el').src = `uploads/media/${_localFile}`;
|
||||
$('labdom-img-prev').style.cssText = ''; // quita display:none!important
|
||||
} else {
|
||||
$('labdom-img-prev').style.display = 'none';
|
||||
}
|
||||
labDomicilio._updateOrdenPreview();
|
||||
|
||||
if (!_bsModal) _bsModal = new bootstrap.Modal('#modalAgendarDomicilio', { backdrop: true, keyboard: true });
|
||||
_bsModal.show();
|
||||
@@ -6903,9 +6970,10 @@ const labDomicilio = (() => {
|
||||
|
||||
/* Reset completo (nombre paciente + todos los campos) */
|
||||
_resetFull() {
|
||||
_pacienteId = null;
|
||||
_pacNombre = '';
|
||||
_histDirs = [];
|
||||
_pacienteId = null;
|
||||
_pacNombre = '';
|
||||
_histDirs = [];
|
||||
_newOrderFile = null;
|
||||
$('labdom-pac-busq').value = '';
|
||||
$('labdom-pac-list').style.display = 'none';
|
||||
$('labdom-pac-buscar').classList.remove('d-none');
|
||||
@@ -6922,6 +6990,73 @@ const labDomicilio = (() => {
|
||||
labDomicilio._resetDomicilio();
|
||||
},
|
||||
|
||||
/* Actualiza la previa de orden médica en el modal (imagen o icono de archivo) */
|
||||
_updateOrdenPreview() {
|
||||
const prevDiv = document.getElementById('labdom-img-prev');
|
||||
const addBtn = document.getElementById('labdom-btn-add-orden');
|
||||
const imgEl = document.getElementById('labdom-img-el');
|
||||
const iconEl = document.getElementById('labdom-file-icon');
|
||||
const nameEl = document.getElementById('labdom-file-name');
|
||||
if (!prevDiv) return;
|
||||
if (_localFile || _newOrderFile) {
|
||||
prevDiv.style.display = '';
|
||||
if (addBtn) addBtn.style.display = 'none';
|
||||
const fileName = _newOrderFile ? _newOrderFile.name : (_localFile || '');
|
||||
if (nameEl) nameEl.textContent = fileName;
|
||||
const isImage = /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(fileName);
|
||||
if (isImage) {
|
||||
if (imgEl) {
|
||||
imgEl.style.display = '';
|
||||
if (_newOrderFile) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = ev => { imgEl.src = ev.target.result; };
|
||||
reader.readAsDataURL(_newOrderFile);
|
||||
} else {
|
||||
imgEl.src = `uploads/media/${_localFile}`;
|
||||
}
|
||||
}
|
||||
if (iconEl) iconEl.style.display = 'none';
|
||||
} else {
|
||||
if (imgEl) imgEl.style.display = 'none';
|
||||
if (iconEl) {
|
||||
const ext = (fileName || '').split('.').pop().toLowerCase();
|
||||
const iconMap = { pdf: 'fas fa-file-pdf text-danger', doc: 'fas fa-file-word text-primary', docx: 'fas fa-file-word text-primary', mp4: 'fas fa-file-video text-warning', mkv: 'fas fa-file-video text-warning', mp3: 'fas fa-file-audio text-success', ogg: 'fas fa-file-audio text-success', webm: 'fas fa-file-video text-warning' };
|
||||
iconEl.innerHTML = `<i class="${iconMap[ext] || 'fas fa-file text-secondary'}" style="font-size:2rem"></i>`;
|
||||
iconEl.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prevDiv.style.display = 'none';
|
||||
if (addBtn) addBtn.style.display = '';
|
||||
}
|
||||
},
|
||||
|
||||
/* Abre el selector de archivo para cambiar o agregar la orden médica */
|
||||
_cambiarOrden() {
|
||||
const inp = document.getElementById('labdom-orden-input');
|
||||
if (!inp) return;
|
||||
const handler = (e) => {
|
||||
inp.removeEventListener('change', handler);
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (!file) return;
|
||||
_newOrderFile = file;
|
||||
_localFile = null;
|
||||
labDomicilio._updateOrdenPreview();
|
||||
inp.value = '';
|
||||
};
|
||||
inp.addEventListener('change', handler);
|
||||
inp.click();
|
||||
},
|
||||
|
||||
/* Elimina la orden médica adjunta */
|
||||
_eliminarOrden() {
|
||||
if (!confirm('¿Eliminar la orden médica adjunta?')) return;
|
||||
_localFile = null;
|
||||
_newOrderFile = null;
|
||||
_msgId = null;
|
||||
labDomicilio._updateOrdenPreview();
|
||||
},
|
||||
|
||||
/* Muestra/oculta el campo «nombre del seguro» según la selección */
|
||||
_toggleSeguro() {
|
||||
const esSeguro = document.getElementById('tc-seguro')?.checked;
|
||||
@@ -7163,8 +7298,21 @@ const labDomicilio = (() => {
|
||||
notas_admin: fv('labdom-notas') || null,
|
||||
};
|
||||
|
||||
// Si la ventana se abrió desde una imagen (orden médica), crear primero
|
||||
// la OrdenMedica y luego enlazarla al domicilio via orden_id
|
||||
// Subir nuevo archivo de orden si el usuario cambió o agregó uno desde el modal
|
||||
if (_newOrderFile) {
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', _newOrderFile);
|
||||
const upRes = await fetch('api/lab/upload_orden.php', { method: 'POST', body: fd });
|
||||
const upData = await upRes.json();
|
||||
if (upData.success && upData.local_file) {
|
||||
_localFile = upData.local_file;
|
||||
_newOrderFile = null;
|
||||
}
|
||||
} catch (_e) { /* silencioso — no bloquea el flujo */ }
|
||||
}
|
||||
|
||||
// Si hay orden médica (original o recién subida), crear registro y enlazar
|
||||
if (_localFile) {
|
||||
try {
|
||||
const convIdNum = (_convId && !String(_convId).startsWith('__user:'))
|
||||
@@ -7328,9 +7476,10 @@ const labDomicilio = (() => {
|
||||
pacienteId: _pacienteId,
|
||||
pacNombre: _pacNombre,
|
||||
histDirs: [..._histDirs],
|
||||
msgId: _msgId,
|
||||
localFile: _localFile,
|
||||
cardVisible: !document.getElementById('labdom-pac-card')?.classList.contains('d-none'),
|
||||
msgId: _msgId,
|
||||
localFile: _localFile,
|
||||
newOrderFile: _newOrderFile,
|
||||
cardVisible: !document.getElementById('labdom-pac-card')?.classList.contains('d-none'),
|
||||
pacInfo: {
|
||||
nombre: document.getElementById('labdom-pac-nombre')?.textContent || '',
|
||||
info: document.getElementById('labdom-pac-info')?.textContent || '',
|
||||
@@ -7406,18 +7555,19 @@ const labDomicilio = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Restaurar estado interno
|
||||
_convId = sesion.convId;
|
||||
_pacienteId = sesion.pacienteId;
|
||||
_pacNombre = sesion.pacNombre;
|
||||
_histDirs = sesion.histDirs || [];
|
||||
_msgId = sesion.msgId;
|
||||
_localFile = sesion.localFile;
|
||||
|
||||
// Mostrar formulario
|
||||
// Mostrar formulario y limpiar DOM
|
||||
mostrarFormulario();
|
||||
labDomicilio._resetFull(); // limpia el DOM
|
||||
|
||||
// Restaurar estado interno DESPUÉS del reset (para que no se sobreescriba)
|
||||
_convId = sesion.convId;
|
||||
_pacienteId = sesion.pacienteId;
|
||||
_pacNombre = sesion.pacNombre;
|
||||
_histDirs = sesion.histDirs || [];
|
||||
_msgId = sesion.msgId;
|
||||
_localFile = sesion.localFile;
|
||||
_newOrderFile = sesion.newOrderFile || null;
|
||||
|
||||
// Restaurar campos del formulario
|
||||
const f = sesion.fields || {};
|
||||
['labdom-fecha','labdom-hora','labdom-tipo','labdom-direccion','labdom-barrio',
|
||||
@@ -7461,6 +7611,7 @@ const labDomicilio = (() => {
|
||||
}
|
||||
|
||||
labDomicilio._calcTotal();
|
||||
labDomicilio._updateOrdenPreview();
|
||||
|
||||
// Abrir el modal
|
||||
if (!_bsModal) _bsModal = new bootstrap.Modal('#modalAgendarDomicilio', { backdrop: true, keyboard: true });
|
||||
|
||||
Reference in New Issue
Block a user