diff --git a/modules/turnero/api/chat_buscar_paciente.php b/modules/turnero/api/chat_buscar_paciente.php new file mode 100644 index 0000000..0701871 --- /dev/null +++ b/modules/turnero/api/chat_buscar_paciente.php @@ -0,0 +1,29 @@ + true, 'data' => []]); exit; } + +try { + $pdo = Database::getInstance()->getConnection(); + $like = '%' . $q . '%'; + $rows = $pdo->prepare( + "SELECT id, nombre_completo, telefono, numero_documento + FROM lab_pacientes + WHERE is_active = 1 + AND (nombre_completo LIKE ? OR numero_documento LIKE ? OR telefono LIKE ?) + ORDER BY nombre_completo ASC + LIMIT 20" + ); + $rows->execute([$like, $like, $like]); + echo json_encode(['ok' => true, 'data' => $rows->fetchAll(PDO::FETCH_ASSOC)]); +} catch (Throwable $e) { + http_response_code(500); + echo json_encode(['ok' => false, 'error' => $e->getMessage()]); +} diff --git a/modules/turnero/api/chat_start_conversation.php b/modules/turnero/api/chat_start_conversation.php new file mode 100644 index 0000000..a3c9ee2 --- /dev/null +++ b/modules/turnero/api/chat_start_conversation.php @@ -0,0 +1,81 @@ + false, 'error' => 'Método no permitido']); exit; +} + +try { + $input = json_decode(file_get_contents('php://input'), true) ?: []; + $phone = trim($input['phone'] ?? ''); + $templateName = trim($input['template_name'] ?? ''); + $lang = trim($input['lang'] ?? 'es_CO'); + $params = $input['params'] ?? []; + + if (!$phone || !$templateName) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'phone y template_name son requeridos']); exit; + } + + // Normalizar teléfono: solo dígitos, agregar 57 si es colombiano de 10 dígitos + $phoneClean = preg_replace('/\D/', '', $phone); + if (strlen($phoneClean) === 10 && $phoneClean[0] === '3') { + $phoneClean = '57' . $phoneClean; + } + if (strlen($phoneClean) < 10) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'Número de teléfono inválido']); exit; + } + + $db = Database::getInstance(); + $pdo = $db->getConnection(); + + // Buscar usuario existente (puede estar guardado con o sin prefijo 57) + $user = $pdo->prepare( + "SELECT id, name FROM users WHERE phone_number = ? OR phone_number = ? LIMIT 1" + ); + $user->execute([$phoneClean, ltrim($phoneClean, '57')]); + $userRow = $user->fetch(PDO::FETCH_ASSOC); + + if (!$userRow) { + // Buscar nombre en lab_pacientes por teléfono + $pac = $pdo->prepare( + "SELECT nombre_completo FROM lab_pacientes WHERE telefono LIKE ? AND is_active=1 LIMIT 1" + ); + $pac->execute(['%' . substr($phoneClean, -10) . '%']); + $pacRow = $pac->fetch(PDO::FETCH_ASSOC); + $nombre = $pacRow['nombre_completo'] ?? $phoneClean; + + // Crear usuario mínimo + $ins = $pdo->prepare( + "INSERT INTO users (name, phone_number, created_at) VALUES (?, ?, NOW())" + ); + $ins->execute([$nombre, $phoneClean]); + $userId = (int)$pdo->lastInsertId(); + } else { + $userId = (int)$userRow['id']; + } + + // Enviar plantilla + $operatorId = $_SESSION['admin_id'] ?? $_SESSION['user_id'] ?? null; + $meta = ['canal' => 'turnero']; + if ($operatorId) $meta['operator_id'] = $operatorId; + + $wa = new WhatsAppService('turnero'); + $wa->sendTemplateMessage($phoneClean, $templateName, $lang, $params, [], null, $meta); + + echo json_encode(['ok' => true, 'user_id' => $userId]); + +} catch (Throwable $e) { + error_log('chat_start_conversation: ' . $e->getMessage()); + http_response_code(500); + echo json_encode(['ok' => false, 'error' => $e->getMessage()]); +} diff --git a/modules/turnero/views/chat.php b/modules/turnero/views/chat.php index 2bb791f..9aa0413 100644 --- a/modules/turnero/views/chat.php +++ b/modules/turnero/views/chat.php @@ -37,6 +37,12 @@ html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMa .sidebar-header .badge-config { font-size: .7rem; background: rgba(0,0,0,.2); padding: 3px 8px; border-radius: 12px; } +.btn-nuevo-chat { + background: rgba(255,255,255,.2); border: none; color: #fff; border-radius: 8px; + padding: 5px 10px; cursor: pointer; font-size: .82rem; font-weight: 700; + display: flex; align-items: center; gap: 4px; transition: background .15s; flex-shrink: 0; +} +.btn-nuevo-chat:hover { background: rgba(255,255,255,.35); } .search-box { padding: 8px 12px; border-bottom: 1px solid #f0f0f0; } .search-box input { width: 100%; padding: 8px 12px; border: 1px solid #e0e0e0; @@ -271,6 +277,38 @@ html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMa .chat-window { position: absolute; inset: 0; z-index: 5; transform: translateX(100%); height: 100%; transition: transform .2s ease; } .chat-window.mobile-active { transform: translateX(0); } .back-btn { display: flex; } +/* ── Modal nuevo mensaje ── */ +#nuevo-modal { display:none; position:fixed; inset:0; z-index:1100; + background:rgba(0,0,0,.45); align-items:center; justify-content:center; padding:16px; } +#nuevo-modal.show { display:flex; } +.nv-dialog { background:#fff; border-radius:14px; width:100%; max-width:420px; + box-shadow:0 8px 32px rgba(0,0,0,.22); overflow:hidden; display:flex; flex-direction:column; max-height:90vh; } +.nv-hdr { display:flex; align-items:center; gap:8px; padding:13px 16px; + border-bottom:1px solid #f1f5f9; font-size:.92rem; font-weight:700; color:#1e293b; flex-shrink:0; } +.nv-hdr > span { flex:1; } +.nv-body { padding:14px 16px; overflow-y:auto; flex:1; } +.nv-footer { display:flex; gap:8px; justify-content:flex-end; padding:12px 16px; + border-top:1px solid #f1f5f9; flex-shrink:0; } +.nv-search { width:100%; padding:8px 12px; border:1.5px solid #e2e8f0; border-radius:8px; + font-size:.87rem; outline:none; box-sizing:border-box; margin-bottom:10px; } +.nv-search:focus { border-color:#25d366; } +.nv-pac-item { display:flex; flex-direction:column; gap:1px; padding:9px 10px; cursor:pointer; + border-radius:8px; transition:background .12s; border:1px solid transparent; } +.nv-pac-item:hover { background:#f0fdf4; border-color:#bbf7d0; } +.nv-pac-name { font-size:.87rem; font-weight:600; color:#1e293b; } +.nv-pac-sub { font-size:.75rem; color:#64748b; } +.nv-divider { display:flex; align-items:center; gap:8px; margin:10px 0; + font-size:.75rem; color:#94a3b8; } +.nv-divider::before,.nv-divider::after { content:''; flex:1; height:1px; background:#e2e8f0; } +.nv-destinatario { display:flex; align-items:center; gap:8px; padding:8px 10px; + background:#f0fdf4; border:1px solid #bbf7d0; border-radius:8px; margin-bottom:10px; font-size:.85rem; } +.nv-destinatario strong { flex:1; color:#15803d; } +.nv-back-btn { background:none; border:none; cursor:pointer; color:#64748b; + font-size:.85rem; padding:2px 7px; border-radius:4px; } +.nv-back-btn:hover { background:#f1f5f9; } +.nv-close-btn { background:none; border:none; cursor:pointer; color:#94a3b8; + font-size:.95rem; padding:2px 7px; border-radius:4px; } +.nv-close-btn:hover { color:#475569; background:#f1f5f9; } } @@ -295,6 +333,9 @@ html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMa Sin config + + +
+ +
+
+ Nuevo mensaje + +
+
+ +
+
o ingresa el número directamente
+ +
+
+ +
+ + + + +
+
@@ -415,7 +519,9 @@ const API_SEND = 'modules/turnero/api/chat_send_message.php'; const API_READ = 'modules/turnero/api/chat_mark_read.php'; const API_MEDIA = 'modules/turnero/api/chat_upload_media.php'; const API_REACT = 'modules/turnero/api/chat_react.php'; -const API_PLANTILLAS = 'modules/turnero/api/chat_get_plantillas.php'; +const API_PLANTILLAS = 'modules/turnero/api/chat_get_plantillas.php'; +const API_BUSCAR_PAC = 'modules/turnero/api/chat_buscar_paciente.php'; +const API_START_CONV = 'modules/turnero/api/chat_start_conversation.php'; let _tplSelected = null; const BASE = (function() { const s = window.location.pathname; @@ -910,6 +1016,189 @@ function cancelRecording() { document.getElementById('micBtn').style.color = ''; } +// ── Nuevo mensaje ──────────────────────────────────────────────────────────── +const nuevoMensaje = (() => { + let _phone = ''; + let _destLabel = ''; + let _tpl = null; + let _buscarTimer = null; + + function _show(stepId) { + ['nv-step-dest','nv-step-tpl','nv-step-vars'].forEach(id => { + document.getElementById(id).style.display = id === stepId ? '' : 'none'; + }); + } + + function abrir() { + _phone = ''; _destLabel = ''; _tpl = null; + document.getElementById('nv-pac-search').value = ''; + document.getElementById('nv-phone-input').value = ''; + document.getElementById('nv-phone-hint').textContent = ''; + document.getElementById('nv-pac-results').innerHTML = ''; + document.getElementById('nv-btn-siguiente').disabled = true; + _show('nv-step-dest'); + document.getElementById('nuevo-modal').classList.add('show'); + document.getElementById('nv-pac-search').focus(); + } + + function cerrar() { + document.getElementById('nuevo-modal').classList.remove('show'); + } + + function _setDest(phone, label) { + _phone = phone; _destLabel = label; + document.getElementById('nv-btn-siguiente').disabled = !phone; + } + + function onPhoneInput(val) { + const digits = val.replace(/\D/,''); + const hint = document.getElementById('nv-phone-hint'); + if (!digits) { _setDest('', ''); hint.textContent = ''; return; } + if (digits.length >= 10) { + hint.textContent = '✅ Número válido'; + hint.style.color = '#16a34a'; + _setDest(digits, '+' + (digits.length === 10 ? '57' : '') + digits); + } else { + hint.textContent = digits.length + ' dígitos — mínimo 10'; + hint.style.color = '#94a3b8'; + _setDest('', ''); + } + // Clear patient selection if typing a phone + document.getElementById('nv-pac-search').value = ''; + document.getElementById('nv-pac-results').innerHTML = ''; + } + + function buscarPaciente(q) { + document.getElementById('nv-phone-input').value = ''; + document.getElementById('nv-phone-hint').textContent = ''; + _setDest('', ''); + clearTimeout(_buscarTimer); + const el = document.getElementById('nv-pac-results'); + if (q.length < 2) { el.innerHTML = ''; return; } + el.innerHTML = '
Buscando…
'; + _buscarTimer = setTimeout(async () => { + try { + const res = await fetch(BASE + API_BUSCAR_PAC + '?q=' + encodeURIComponent(q)); + const json = await res.json(); + if (!json.ok || !json.data?.length) { + el.innerHTML = '
Sin resultados
'; + return; + } + el.innerHTML = ''; + json.data.forEach(p => { + const div = document.createElement('div'); + div.className = 'nv-pac-item'; + const tel = p.telefono || '—'; + div.innerHTML = + `${escHtml(p.nombre_completo)}` + + `${escHtml(p.numero_documento || '')} · 📱 ${escHtml(tel)}`; + div.onclick = () => { + const phone = (p.telefono || '').replace(/\D/g,''); + if (!phone) { alert('Este paciente no tiene teléfono registrado.'); return; } + _setDest(phone, p.nombre_completo + ' · ' + tel); + document.getElementById('nv-pac-search').value = p.nombre_completo; + el.innerHTML = ''; + }; + el.appendChild(div); + }); + } catch(e) { + el.innerHTML = '
Error al buscar
'; + } + }, 350); + } + + async function irAPlantilla() { + if (!_phone) return; + document.getElementById('nv-dest-label').textContent = _destLabel || _phone; + _show('nv-step-tpl'); + const listEl = document.getElementById('nv-tpl-list'); + listEl.innerHTML = '
Cargando…
'; + try { + const res = await fetch(BASE + API_PLANTILLAS); + const json = await res.json(); + if (!json.ok || !json.data?.length) { + listEl.innerHTML = '
No hay plantillas habilitadas.
Configúralas en Configuración → Sesión.
'; + return; + } + listEl.innerHTML = ''; + json.data.forEach(tpl => { + const div = document.createElement('div'); + div.className = 'tpl-item'; + div.innerHTML = + `${escHtml(tpl.name)}` + + `${escHtml(tpl.template_name)} · ${escHtml(tpl.language_code)}` + + (tpl.body_text ? `${escHtml(tpl.body_text.substring(0,80))}${tpl.body_text.length>80?'…':''}` : ''); + div.onclick = () => irAVariables(tpl); + listEl.appendChild(div); + }); + } catch(e) { + listEl.innerHTML = '
Error al cargar plantillas
'; + } + } + + async function irAVariables(tpl) { + _tpl = tpl; + document.getElementById('nv-vars-title').textContent = tpl.name; + const previewBox = document.getElementById('nv-preview-box'); + if (tpl.body_text) { previewBox.textContent = tpl.body_text; previewBox.style.display = ''; } + else previewBox.style.display = 'none'; + + const varsBody = document.getElementById('nv-vars-body'); + varsBody.innerHTML = '
Cargando…
'; + _show('nv-step-vars'); + + try { + const res = await fetch(BASE + 'api/get_template_details.php?id=' + tpl.id); + const json = await res.json(); + const vars = json.template?.variables ?? []; + if (!vars.length) { + varsBody.innerHTML = '
Esta plantilla no requiere variables.
'; + } else { + varsBody.innerHTML = ''; + vars.forEach(v => { + const row = document.createElement('div'); + row.className = 'tpl-var-row'; + const ph = v.example || v.placeholder || v.label; + row.innerHTML = `` + + ``; + varsBody.appendChild(row); + }); + } + } catch(e) { + varsBody.innerHTML = '
Error al cargar variables
'; + } + } + + function volverADest() { _tpl = null; _show('nv-step-dest'); } + function volverAPlantilla() { _tpl = null; _show('nv-step-tpl'); } + + async function enviar() { + if (!_phone || !_tpl) return; + const inputs = [...document.querySelectorAll('#nv-vars-body input[data-var-index]')]; + const params = inputs.map(el => ({ name: el.dataset.varIndex, value: el.value.trim() })); + const missing = params.findIndex(p => !p.value); + if (missing !== -1) { inputs[missing].focus(); return; } + + const templateName = _tpl.template_name; + const templateLang = _tpl.language_code; + cerrar(); + + try { + const res = await fetch(BASE + API_START_CONV, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ phone: _phone, template_name: templateName, lang: templateLang, params }) + }); + const json = await res.json(); + if (!json.ok) { alert('Error: ' + (json.error || 'No se pudo enviar')); return; } + // Abrir la conversación recién creada + await loadContacts(); + if (json.user_id) openChat(json.user_id); + } catch(e) { alert('Error de conexión al enviar el mensaje'); } + } + + return { abrir, cerrar, buscarPaciente, onPhoneInput, irAPlantilla, volverADest, volverAPlantilla, enviar }; +})(); + // ── Plantilla (2 pasos) ─────────────────────────────────────────────────────── async function openTemplatePicker() { if (!state.activeUserId) return;